Angular2 ngFor, <template> 的用法

https://toddmotto.com/angular-ngfor-template-element#lttemplategt-element



Angular ngFor, <template> and the compiler

Angular ngFor is a built-in Directive that allows us to iterate over a collection. This collection is typically an array, however can be “array-like”. To demonstrate this, we’ll be usingRx.Observable.of() to initialise our collection with an Observable instead of a static array.

We’ll also be exploring some other under-the-hood properties of ngFor, as well as looking at how Angular expands our ngFor to a <template> element and composes our view.

Table of contents

Using the ngFor directive

In this section, we’ll be taking a look at the featureset ngFor provides, as well as some use case examples.

NgModule import

First of all, to use ngFor, you need to import theCommonModule. However, if you’re using theBrowserModule in the root module, you won’t need to import it, as theBrowserModule exposes the CommonModule for us - so you only need to import the CommonModule when creating further modules with @NgModule.

Our @NgModule will look like this:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule],
  bootstrap: [AppComponent]
})
export class AppModule {}

For this article, we’ll be including a further ContactCardComponent component in our @NgModule:

// ...
import { ContactCardComponent } from './contact-card.component';

@NgModule({
  declarations: [
    AppComponent,
    ContactCardComponent
  ],
  // ...
})
export class AppModule {}

Our ContactCardComponent simply looks like this, taking a single@Input of contact:

import { Component, Input } from '@angular/core';

import { Contact } from './models/contact.interface';

@Component({
  selector: 'contact-card',
  template: `
    <div class="contact-card">
      <p>{{ contact.name }} ( {{ contact.age }} )</p>
      <p>{{ contact.email }}</p>
    </div>
  `
})
export class ContactCardComponent {
  
  @Input()
  contact: Contact;
  
}

So now we’re all setup, what’s next?

Iterating collections

Now that our ContactCardComponent is included in our module, we can setup ourAppComponent to use this dataset:

@Component({...})
export class AppComponent implements OnInit {
  contacts: Observable<Contact[]>;
  ngOnInit() {
    this.contacts = Observable.of([
      {
        "id": 1,
        "name": "Laura",
        "email": "lbutler0@latimes.com",
        "age": 47
      },
      {
        "id": 2,
        "name": "Walter",
        "email": "wkelley1@goodreads.com",
        "age": 37
      },
      {
        "id": 3,
        "name": "Walter",
        "email": "wgutierrez2@smugmug.com",
        "age": 49
      },
      {
        "id": 4,
        "name": "Jesse",
        "email": "jarnold3@com.com",
        "age": 47
      },
      {
        "id": 5,
        "name": "Irene",
        "email": "iduncan4@oakley.com",
        "age": 33
      }
    ]);
  }
}

As mentioned in the introduction, I’m using Observable.of here from RxJS to give me an Observable stream from the results, this is a nice way to mimic an Observable response, such as when using the AngularHttp module to return data from your API.

Using ngFor

Now we’re setup, we can look into our AppComponent template:

@Component({
  selector: 'app-root',
  template: `
    <div class="app">
      <ul>
        <li>
          <contact-card></contact-card>
        </li>
      </ul>
    </div>
  `
})

You can see I’m declaring <contact-card> inside of here, as we want to iterate our dataset and populate each contact via the@Input setup inside our ContactCardComponent.

One way we could do this is using ngFor on the component itself, however for simplicity we’ll use the unordered list. Let’s addngFor:

<ul>
  <li *ngFor="let contact of contacts">
    <contact-card></contact-card>
  </li>
</ul>

There are a few things happening here, the first you’ll notice a * character at the beginning of the ngFor, we’ll come onto what this means in the next section when we look at the<template> element. Secondly, we’re creating a context calledcontact, using a “for of” loop - just like in ES6.

The ngFor Directive will clone the <li> and the child nodes. In this case, the <contact-card> is a child node, and a card will be “stamped out” in the DOM for each particular item inside ourcontacts collection.

So, now we have contact available as an individual Object, we can pass the individualcontact into the <contact-card>:

<ul>
  <li *ngFor="let contact of contacts">
    <contact-card [contact]="contact"></contact-card>
  </li>
</ul>

If you’re using a static array, or binding the result of an Observable to the template, you can leave the template as it currently is. However, we can optionally bind the Observable directly to the template, which means we’ll need theasync pipe here to finish things off:

<ul>
  <li *ngFor="let contact of contacts | async">
    <contact-card [contact]="contact"></contact-card>
  </li>
</ul>

You can check a live output of what we’ve covered so far here:

Using trackBy for keys

If you’re coming from an Angular 1.x background, you’ll have likely seen “track by” when using anng-repeat, and similarly in React land, usingkey on a collection item.

So what do these do? They associate the objects, or keys, with the particular DOM nodes, so should anything change or need to be re-rendered, the framework can do this much more efficiently. Angular’sngFor defaults to using object identity checking for you, which is fast, but can befaster!

This is where trackBy comes into play, let’s add some more code then explain:

<ul>
  <li *ngFor="let contact of contacts | async; trackBy: trackById;">
    <contact-card [contact]="contact"></contact-card>
  </li>
</ul>

Here we’ve added trackBy, then given it a value oftrackById. This is a function that we’ll add in the component class:

trackById(index, contact) {
  return contact.id;
}

All this function does is use a custom tracking solution for our collection. Instead of using object identity, we’re telling Angular here to use the uniqueid property that each contact object contains. Optionally, we can use the index (which is the index in the collection of each item, i.e. 0, 1, 2, 3, 4).

If your API returns unique data, then using that would be a preferable solution overindex - as the index may be subject to change if you reorder your collection. Using a unique identifier allows Angular to locate that DOM node associated with the object much faster, and it will reuse the component in the DOM should it need to be updated - instead of destroying it and rebuilding it.

You can check a live output of what we’ve covered here:

Capturing “index” and “count”

The ngFor directive doesn’t just stop at iteration, it also provides us a few other niceties. Let’s exploreindex and count, two public properties exposed to us on each ngFor iteration.

Let’s create another variable called i, which we’ll assign the value ofindex to. Angular exposes these values under-the-hood for us, and when we look at the next section with the<template> element, we can see how they are composed.

To log out the index, we can simply interpolate i:

<ul>
  <li *ngFor="let contact of contacts | async; let i = index;">
    Index: {{ i }}
    <contact-card [contact]="contact"></contact-card>
  </li>
</ul>

This will give us every index, starting from 0, for each item in our collection. Let’s also exposecount:

<ul>
  <li *ngFor="let contact of contacts | async; let i = index; let c = count;">
    Index: {{ i }}
    Count: {{ c }}
    <contact-card [contact]="contact"></contact-card>
  </li>
</ul>

The count will return a live collection length, equivalent ofcontacts.length. These can optionally be bound and passed into each<contact-card>, for example you may wish to log out the total length of your collection somewhere, and also pass theindex of the particular contact into a function@Output:

<ul>
  <li *ngFor="let contact of contacts | async; let i = index; let c = count;">
    <contact-card 
      [contact]="contact"
      [collectionLength]="c"
      (update)="onUpdate($event, i)">
    </contact-card>
  </li>
</ul>

You can check a live output of what we’ve covered here:

Accessing first, last, odd, even

Four more properties exposed by ngFor (well, actually underneath it usesNgForRow, a class which generates each ngFor item internally). Let’s quickly look at the source code for this:

export class NgForRow {
  constructor(public $implicit: any, public index: number, public count: number) {}
  get first(): boolean { return this.index === 0; }
  get last(): boolean { return this.index === this.count - 1; }
  get even(): boolean { return this.index % 2 === 0; }
  get odd(): boolean { return !this.even; }
}

As I mentioned above, the NgForRow is what constructs ourngFor items, and you can see in the constructor we’ve already taken a look at index and count! The last things we need to look at are the getters, which we can explain from the source code above:

  • first: returns true for the first item in the collection, matches the index with zero

  • last: returns true for the last item in the collection, matches the index with the total count, minus one to shift the “count” down one to cater for zero-based indexes

  • even: returns true for even items (e.g. 2, 4) in the collection, uses% modulus operator to calculate based off index

  • odd: returns true for odd items (e.g. 1, 3), simply invertsthis.even result

Using this, we can add conditionally apply things such as styling, or hook into thelast property to know when the collection has finished rendering.

For this quick demo, we’ll use ngClass to add some styles to each<li> (note how we create more variables, just likeindex):

<ul>
  <li
    *ngFor="let contact of contacts | async; let o = odd; let e = even;"
    [ngClass]="{
      'odd-active': o,
      'even-active': e
    }">
    <contact-card 
      [contact]="contact"
      (update)="onUpdate($event, index)">
    </contact-card>
  </li>
</ul>

And some styles:

@Component({
  selector: 'app-root',
  styles: [`
    .odd-active { background: purple; color: #fff; }
    .even-active { background: red; color: #fff; }
  `],
  template: `
    <div class="app">
      <ul>
        <li
          *ngFor="let contact of contacts | async; let o = odd; let e = even;"
          [ngClass]="{
            'odd-active': o,
            'even-active': e
          }">
          <contact-card 
            [contact]="contact"
            (update)="onUpdate($event, index)">
          </contact-card>
        </li>
      </ul>
    </div>
  `
})

We won’t demonstrate first and last, as it’s fairly obvious from the above how we can hook those up!

You can check a live output of what we’ve covered here:

<template> element

We mentioned earlier in this article that we’d look at understanding what the * meant in our templates. This also shares the same syntax as *ngIf, which you’ve likely also seen before.

So in this next section, we’ll take a deeper dive on ngFor, * and the <template> element to explain in more detail what’s really happening here.

When using an asterisk (*) in our templates, we are informing Angular we’re using a structural directive, which is also sugar syntax (a nice short hand) for using the<template> element.

<template> and Web Components

So, what is the <template> element? First, let’s take a step back. We’ll roll back to showing some Angular 1.x code here, perhaps you’ve done this before or done something similar in another framework/library:

<script id="myTemplate" type="text/ng-template">
  <div>
    My awesome template!
  </div>
</script>

This overrides the type on the <script> tag, which prevents the JavaScript engine from parsing the contents of the<script> tag. This allows us, or a framework such as Angular 1.x, to fetch the contents of the script tag and use it as some form of HTML template.

Web Components introduced something similar to this idea, called the <template>:

<template id="myTemplate">
  <div>
    My awesome template!
  </div>
</template>

To grab our above template and instantiate it, we’d do this in plain JavaScript:

<div id="host"></div>
<script>
  let template = document.querySelector('#myTemplate');
  let clone = document.importNode(template.content, true);
  let host = document.querySelector('#host');
  host.appendChild(clone);
</script>

Note how we have id=host, which is our “host” Node for the template to be injected into.

You may have seen this term floating around Angular in a few ways, such as _nghost prefixes on Nodes (ng-host) or the host property in directives.

ngFor and <template>

So how does the above <template> explanation tell us more aboutngFor and the *? The asterisk is shorthand syntax for using the <template> element.

Let’s start from the basic ngFor example:

<ul>
  <li *ngFor="let contact of contacts | async">
    <contact-card [contact]="contact"></contact-card>
  </li>
</ul>

And demonstrate the <template> equivalent:

<template ngFor let-contact [ngForOf]="contacts | async">
  <li>
    <contact-card [contact]="contact"></contact-card>
  </li>
</template>

That’s a lot different! What’s happening here?

When we use *ngFor, we’re telling Angular to essentially treat the element the* is bound to as a template.

Note: Angular’s <template> element is not a true Web Component, it merely mirrors the concepts behind it to allow you to use<template> as it’s intended in the spec. When we Ahead-of-Time compile, we will see no<template> elements. However, this doesn’t mean we can’t use things like Shadow DOM, as they are stillcompletely possible.

Let’s continue, and understand what ngFor,let-contact and ngForOf are doing above.

ngFor and embedded view templates

First thing’s first, ngFor is a directive! Let’s check some of the source code:

@Directive({selector: '[ngFor][ngForOf]'})
export class NgFor implements DoCheck, OnChanges {...}

Here, Angular is using attribute selectors as the value of selector to tell the @Directive decorator what attributes to look for (don’t confuse this with property binding such as<input [value]="foo">).

The directive uses [ngFor][ngForOf], which implies there are two attributes as a chained selector. So, how doesngFor work if we’re not using ngForOf?

Angular’s compiler transforms any <template> elements and directives used with a asterisk (*) into views that are separate from the root component view. This is so each view can be created multiple times.

During the compile phase, it will take let contact of contacts and capitalise theof, and create a custom key to create ngForOf.

In our case, Angular will construct a view that creates everything from the <li> tag inwards:

<!-- view -->
<li>
  <contact-card [contact]="contact"></contact-card>
</li>
<!-- /view -->

It also creates an invisible view container to contain all instances of the template, acting as a placeholder for content. The view container Angular has created essentially wraps the “views”, in our case this is just inside the<ul> tags. This houses all the templates that are created byngFor (one for each row).

A pseudo-output might look like this:

<ul>
<!-- view container -->
  <!-- view -->
  <li>
    <contact-card [contact]="contact"></contact-card>
  </li>
  <!-- /view -->
  <!-- view -->
  <li>
    <contact-card [contact]="contact"></contact-card>
  </li>
  <!-- /view -->
  <!-- view -->
  <li>
    <contact-card [contact]="contact"></contact-card>
  </li>
  <!-- /view -->
<!-- /view container -->
</ul>

ngFor creates an “embedded view” for each row, passing through the view it has created and the context of the row (the index and the row data). This embedded view is then inserted into the view container. When the data changes, it tracks the items to see if they have moved. If they’ve moved, instead of recreating the embedded views, it moves them around to be in the correct position, or destroys them if they no longer exist.

Context and passing variables

The next step is understanding how Angular passes the context to each <template>:

<template ngFor let-contact [ngForOf]="contacts | async">
  <li>
    <contact-card [contact]="contact"></contact-card>
  </li>
</template>

So now we’ve understood ngFor and ngForOf, how does Angular associate let-contact with the individualcontact that we then property bind to?

Because let-contact has no value, it’s merely an attribute, this is where Angular provides an “implied” value, or$implicit as it’s called under-the-hood.

Whilst Angular is creating each ngFor item, it uses anNgForRow class alongside an EmbeddedViewRef, and passes these properties in dynamically. Here’s a small snippet from the source code:

changes.forEachIdentityChange((record: any) => {
  const viewRef = <EmbeddedViewRef<NgForRow>>this._viewContainer.get(record.currentIndex);
  viewRef.context.$implicit = record.item;
});

Alongside this section of code, we can also see how our aforementioned index and count properties are kept updated:

for (let i = 0, ilen = this._viewContainer.length; i < ilen; i++) {
  const viewRef = <EmbeddedViewRef<NgForRow>>this._viewContainer.get(i);
  viewRef.context.index = i;
  viewRef.context.count = ilen;
}

You can dig through the directive source code in more detail here.

This is how we can then access the index andcontext like this:

<ul>
  <template ngFor let-i="index" let-c="count" let-contact [ngForOf]="contacts | async">
    <li>
      <contact-card [contact]="contact"></contact-card>
    </li>
  </template>
</ul>

Note how we’re supplying let-i and let-c values which are exposed from the NgForRow instance, unlike let-contact.

You can check out the <template> version of the things we’ve covered here:

Todd Motto

I'm Todd, I teach the world Angular through @UltimateAngular.Conference speaker and Developer Expert at Google.

Award-winning Angular 1.x and Angular 2 courses

Become an expert with my comprehensive Angular 1.5+ and Angular 2 courses


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值