Home > Blockchain >  How to avoid logic in templates in Angular for ngFor loop over item?
How to avoid logic in templates in Angular for ngFor loop over item?

Time:01-06

Recently I read some articles that mentioned to we should avoid logic in template in Angular 2 html file because "Having logic in the template means that it is not possible to unit test it and therefore it is more prone to bugs when changing template code."

However, I do not know how to avoid putting logic in the html when there are multiple items that looped from an array. Here is the code example, if someone could give me some idea would be great.

<ng-container *ngFor="let option of options">
    <div
        
        [ngClass]="{
            'item__option--disabled': disabled || !!option.disabled,
            'item__option--selected': selectedOption === option.value && !option.disabled,
            'item__option--disabled-selected': selectedOption === option.value && option.disabled
        }"
        (click)="onClickHandler(option)"
        tabindex="0"
    ></div>
</ng-container>

Attempted solution (did not work): Moved all of the class condition to a function in ts file, but whenever a value change, the function will get called (number of options, for example 5) times, which does not help much.

CodePudding user response:

A possible solution for this type of issues can be to simply add a property in objects that are being iterated that represents the css class that they should have.

Consider the following example:

Start by creating an interface with that represents the contract that each option needs to be implement. In this interface, the property klass represents the css class that will be assigned.

interface Option { 
  disabled: boolean;
  value: string;
  klass?: string;
}

The component code is as follows:

export class FooComponent implements OnInit {
  disabled = false;
  selectedOption ='Alfa';

  options: Option[] = [
    { disabled: false, value: 'Alfa' },
    { disabled: true, value: 'Beta' },
    { disabled: false, value: 'Gamma' },
    { disabled: true, value: 'Alfa' },
    { disabled: false, value: 'Tetha' }
  ]

  ngOnInit(): void {
    this.options.forEach(option => this.updateClass(option));
  }

  private updateClass(option: Option): void {
    if (option.disabled) {
      if (option.value === this.selectedOption) {
        option.klass = 'item__option--disabled-selected';
      } else {
        option.klass = 'item__option--selected'
      }
    } else {
      option.klass = 'item__option--disabled'
    }
  }

  onClickHandler(option: Option): void {
    this.disabled = option.disabled;
    this.selectedOption = option.value;
    this.updateClass(option) 
  }
}

Going over the methods:

  • ngOnInit: since we don't know if the options are hardcoded or come from an API, we iterate over each of them and only then do we assign the initial class.
  • updateClass: performs a series of if's to ensure we apply the correct css class in the correct options.
  • onClickHandler: update the selected values and triggers the update of the option css. Depending on the case, we may need to update all the options, similarly to how we do in the ngOnInit (but this depends on use case).

The html code is as follows:

<ng-container *ngFor="let option of options">
  <div
      
      [ngClass]="option.klass"
      (click)="onClickHandler(option)"
      tabindex="0"
  >{{option.value}}</div>
</ng-container>

The class item__option is assigned by default html mechanisms. Our dynamic class is assigned based on the value that we provide via our property, relying on the ngClass different types of acceptable values.

CodePudding user response:

There are different ways to avoid using complex logic in the template. To choose one this depends on your application architecture.

In general you should wrap all your complex logic in a getter in your component class and pass the getter property in the template instead. Try not to do heavy statments in the getter. For instance do not do math, create new array, or use function call in the getter. Just try to return a variable in the getter.

Avoid using methods to pass values in the template. Because each time the change detection is triggered the method will be called. A method call is more effort on execution than using property values. This matters for the performance in large and scalable applications.

Child Component Solution

Put each item of your array in an item component. This will allow the use of getter in your component ts file and then you could use the getter to pass the class value to ngClass like:

In ItemComponent .ts

@Input() arrayItem; 

get itemClass() {
    return {
        'item-disabled-class': this.arrayItem.disabled
    };
}

In ItemComponent template:

<div [ngClass]="itemClass"></div>

Here is my full stackblitz example

Pros

  • More control over the item data in the component class.
  • Handle actions on changes when the item component @Input property change

Cons

  • Extra effort for the component communication is required

Property Value Solution

You could cache the boolean values used for the ngClass at the time, when a property of the condition statment has changes. This need more attention on when each value is set. For instance this.disabled will be set based on user interaction, but could also be set based on different event. To do this you need to define a setter for the disabled property and then pass the property value to the ngClass.

Pros

  • Good performance with change detection
  • Less components rendered

Cons

  • Extra effort for handling the cached values update

Here is my full stackblitz example

Other possible solution

For your ngClass case you could create a directive that use the option as an input and sets the class property of the ElementRef by Renderer2 like ngClass originally does.

Pros

  • Easy to debug since the Directive has the methods that applies the change in the class.

Cons

  • does not have direct access to the ngFor item
  • Related