How to create variable in ngFor loop?
Asked Answered
S

8

59

I am trying to find out how to create a variable in an ngFor loop.

I have a loop like this:

<td *ngFor="#prod of products">
  <a href="{{getBuild(branch,prod)?.url}}">
    {{getBuild(branch,prod)?.status}}
   </a>
</td>

You can see the the getBuild call has to be repeated multiple times. (Many more times in my actual example). I would like a way to create this variable in the template once inside the loop and simply reuse it.

Is there a way to do this?

Shiftless answered 7/2, 2016 at 18:38 Comment(1)
Essentially the same question: #35163039Macassar
B
19

I think local variables (defined with the # character) don't apply for your use case.

In fact, when you define a local variable on an HTML element it corresponds to the component if any. When there is no component on the element, the variable refers to the element itself.

Specifying a value for a local variable allows you to select a specific directive associated with the current element. For example:

<input #name="ngForm" ngControl="name" [(ngModel)]="company.name"/>

will set the instance of the ngForm directive associated with the current in the name variable.

So local variables don't target what you want, i.e. setting a value created for the current element of a loop.

If you try to do something like that:

<div *ngFor="#elt of eltList" >
  <span #localVariable="elt.title"></span>
  {{localVariable}}
</div>

You will have this following error:

Error: Template parse errors:
There is no directive with "exportAs" set to "elt.title" ("
  <div *ngFor="#elt of eltList" >
    <span [ERROR ->]#localVariable="elt.title"></span>
    {{localVariable}}
  </div>
"): AppComponent@2:10

Angular2 actually looks for a directive matching the provided name elt.title here)... See this plunkr to reproduce the error: https://plnkr.co/edit/qcMGr9FS7yQD8LbX18uY?p=preview

See this link: http://victorsavkin.com/post/119943127151/angular-2-template-syntax, section "Local variables" for more details.

In addition to the current element of the iteration, ngFor only provides a set of exported values that can be aliased to local variables: index, last, even and odd.

See this link: https://angular.io/docs/ts/latest/api/common/NgFor-directive.html

What you could do is to create a sub component to display elements in the loop. It will accept the current element as parameter and create your "local variable" as attribute of the component. You will be able then to use this attribute in the template of the component so it will be created once per element in the loop. Here is a sample:

@Component({
  selector: 'elt',
  template: `
    <div>{{attr}}</div>
  `
})
export class ElementComponent {
  @Input() element;

  constructor() {
    // Your old "localVariable"
    this.attr = createAttribute(element.title);
  }

  createAttribute(_title:string) {
    // Do some processing
    return somethingFromTitle;
  }
}

and the way to use it:

<div *ngFor="#elt of eltList" >
  <elt [element]="elt></elt>
</div>
Brenda answered 7/2, 2016 at 20:15 Comment(3)
I think you are correct. I was hoping for an easier way but it doesn't look possible right now. IMHO it would be very nice if there was a syntax for setting locally variables to results of template expressions. It could make this type of usage a bit easier.Shiftless
Yes, agreed! I posted an issue in the Angular github to know if there is something planned regarding this: github.com/angular/angular/issues/6947.Brenda
It's important to note the slight change on the ngFor: *ngFor="let myVar of list". Check it out: (angular.io/docs/ts/latest/api/common/NgFor-directive.html)Coherence
C
17

I think this is a classic case for making a subcomponent.

<td *ngFor="#prod of products">
    <subComp [prod]=prod></subComp>
</td>

Your component would then have a prod input and run the required function once in OnInit.

Simple example plunk here

Convulsant answered 7/2, 2016 at 20:4 Comment(1)
I could create a sub component that takes branch and prod as inputs. Just seems heavy weight for something that is a few lines of HTML that could be done in the "parent" component template. I was hoping for some way to assign a variable inside the ngFor and use it in the body of the loop.Shiftless
B
15

With angular 4 you can do this:

<td *ngFor="#prod of products">
  <div *ngIf="getBuild(branch,prod); let build">
    <a href="{{build.url}}">
     {{build.status}}
    </a>
  </div>
</td>
Brassard answered 30/11, 2018 at 13:30 Comment(0)
S
6

Although arguments can be made about the advantages of simply writing a new subcomponent. I think that the following solution is the most correct answer to the question asked.

<div *ngFor="let person of listOfPeople">
  <ng-container
    *ngTemplateOutlet="introText; context: { title: getTitle(person), color: person.color }"
  ></ng-container>
</div>

<ng-template #introText let-title="title" let-color="color">
  <p [style.color]="color">
    {{title}} loves the color {{color}}
  </p>
</ng-template>

Check out a codesandbox of this.
This will allow for defining multiple template variables, and reusing the output of a calculation an unlimited number of times.

Reasons to use ng-template:

  • As OP mentioned, this is lighter weight than a whole new component. Therefore this may be a good option if you have a simple template that reuses calculated logic
  • In some cases the template NEEDS to be in the same file - I ran into this with a 'mat-stepper' (Material library) component for example

Reasons to avoid ng-template:

  • If the code should be reusable in other components
  • If the template gets too complex, it will become far more confusing than helpful
Stifling answered 24/6, 2021 at 9:42 Comment(0)
D
4

Improving on the *ngIf solution, you can have always true condition to leave no culprits

<ng-container *ngFor="let item of items">
    <ng-container *ngIf="getMyVariableValue(item.id) || true; let myVariable">
        {{ content here }}
    </ng-container>
</ng-container>

One observation I want to note, Angular doesn't put the exact return value of the function (here, "getMyVariableValue"). If it returns null or undefined then Angular will put true in "myVariable".

Doley answered 9/6, 2021 at 13:47 Comment(0)
M
2

No, just cache the result in getBuild for each branch/prod combination or as long as it is called with the same values as before.

Memorable answered 7/2, 2016 at 19:55 Comment(2)
Understood that I could cache the results. The issue isn't that the computation takes a long time (it is already cached). It is just that I don't want to repeat the call multiple times in the body of the for loop. I was hoping for a way to conceptually do: "build = getBuild(branch,prod)" and then just use build inside the loop.Shiftless
I'm pretty sure this is not possible. If the computation isn't expensive I wouldn't bother anyway.Semasiology
J
2

Though not ideal, another approach could be using *ngIf

<td *ngFor="#prod of products">
  <ng-container *ngIf="{build:getBuild(branch,prod)}; let state">
    <a href="{{state.build?.url}}">
      {{state.build?.status}}
    </a>
  </ng-container>
</td>

Since the *ngIf expression returns an object, it will always be thruthy so the content inside ng-container will always be rendered. The resulting object gets assigned to the state variable which can be reused later on. The ng-container does not insert an dom element.

Jeremiah answered 3/4, 2021 at 11:2 Comment(2)
This is essentially the same answer as another already posted. https://mcmap.net/q/328553/-how-to-create-variable-in-ngfor-loopMethadone
the original answer does not take into account if the getBuild(branch, prod) method returns a non-truthy value. Using an object allows you to always ensure that the container is rendered.Hambletonian
A
0

If you're ok with a bit of a hack, you can also loop over a single-item list:

<td *ngFor="#prod of products">
  <a *ngFor="let build of [getBuild(branch,prod)]" href="{{build?.url}}">
    {{build?.status}}
   </a>
</td>
Afton answered 24/3, 2022 at 6:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.