1. 程式人生 > 實用技巧 >[轉]3 Ways to Pass Async Data to Angular 2+ Child Components

[轉]3 Ways to Pass Async Data to Angular 2+ Child Components

原文:https://www.digitalocean.com/community/tutorials/3-ways-to-pass-async-data-to-angular-2-child-components

——————————————————————

3 Ways to Pass Async Data to Angular 2+ Child Components

While this tutorial has content that we believe is of great benefit to our community, we have not yet tested or edited it to ensure you have an error-free learning experience. It's on our list, and we're working on it! You can help us out by using the "report an issue" button at the bottom of the tutorial.

Let’s start with a common use case. You have some data you get from external source (e.g. by calling API). You want to display it on screen.

However, instead of displaying it on the same component, you would like to pass the data to a child component to display.

The child component might has some logic to pre-process the data before showing on screen.

Our Example

For example, you have ablogger componentthat will display blogger details and her posts. Blogger component will gets the list of posts from API.

Instead of writing the logic of displaying the posts in the blogger component, you want to reuse theposts componentthat is created by your teammate, what you need to do is pass it the posts data.

The posts component will thengroup the postsby category and display accordingly, like this:

Isn’t That Easy?

It might look easy at the first glance. Most of the time we will initiate all the process during our component initialization time - duringngOnInitlife cycle hook (referherefor more details on component life cycle hook).

In our case, you might think that we should run the post grouping logic duringngOnInitof the posts component.

However, because thepostsdata is coming from server, when the blogger component passes thepostsdata to posts component, the posts componentngOnInit is already firedbefore the data get updated. Your post grouping logic will not be fired.

How can we solve this? Let’s code!

Our Post Interfaces and Data

Let’s start with interfaces.

// post.interface.ts

// each post will have a title and category
export interface Post {
    title: string;
    category: string;
}

// grouped posts by category
export interface GroupPosts {
    category: string;
    posts: Post[];
}

Here is our mock posts dataassets/mock-posts.json.

[
    { "title": "Learn Angular", "type": "tech" },
    { "title": "Forrest Gump Reviews", "type": "movie" },
    { "title": "Yoga Meditation", "type": "lifestyle" },
    { "title": "What is Promises?", "type": "tech" },
    { "title": "Star Wars Reviews", "type": "movie" },
    { "title": "Diving in Komodo", "type": "lifestyle" }
]

Blogger Component

Let’s take a look at our blogger component.

// blogger.component.ts

import { Component, OnInit, Input } from '@angular/core';
import { Http } from '@angular/http';
import { Post } from './post.interface';

@Component({
    selector: 'bloggers',
    template: `
        <h1>Posts by: {{ blogger }}</h1>
        <div>
            <posts [data]="posts"></posts>
        </div>
    `
})
export class BloggerComponent implements OnInit {

    blogger = 'Jecelyn';
    posts: Post[];

    constructor(private _http: Http) { }

    ngOnInit() {
        this.getPostsByBlogger()
            .subscribe(x => this.posts = x);
    }

    getPostsByBlogger() {
        const url = 'assets/mock-posts.json';
        return this._http.get(url)
            .map(x => x.json());
    }
}

We will get our mock posts data by issuing aHTTP GETcall. Then, we assign the data topostsproperty. Subsequently, we bindpoststo posts component in our view template.

Please take note that, usually we will perform HTTP call in service. However, since it’s not the focus of this tutorial (to shorten the tutorial), we will do that it in the same component.

Posts Component

Next, let’s code out posts component.

// posts.component.ts

import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Post, GroupPosts } from './post.interface';

@Component({
    selector: 'posts',
    template: `
    <div class="list-group">
        <div *ngFor="let group of groupPosts" class="list-group-item">
            <h4>{{ group.category }}</h4>
            <ul>
                <li *ngFor="let post of group.posts">
                    {{ post.title }}
                </li>
            </ul>
        <div>
    </div>
    `
})
export class PostsComponent implements OnInit, OnChanges {

    @Input()
    data: Post[];

    groupPosts: GroupPosts[];

    ngOnInit() {
    }

    ngOnChanges(changes: SimpleChanges) {
    }

    groupByCategory(data: Post[]): GroupPosts[] {
        // our logic to group the posts by category
        if (!data) return;

        // find out all the unique categories
        const categories = new Set(data.map(x => x.category));

        // produce a list of category with its posts
        const result = Array.from(categories).map(x => ({
            category: x,
            posts: data.filter(post => post.category === x)
        }));

        return result;
    }
}

We have an input calleddatawhich will receive the posts data from parent component. In our case, blogger component will provide that.

You can see that we implement two interfacesOnInitandOnChanges. These are the lifecycle hooks that Angular provide to us. We have not done anything in bothngOnInitandngOnChangesjust yet.

ThegroupByCategoryfunction is our core logic to group the posts by category. After the grouping, we will loop the result and display the grouped posts in our template.

Remember to import these components in you module (e.g.app.module.ts) and add it underdeclarations.

Save and run it. You will see a pretty empty page with the blogger name only. That’s because we have not code our solution yet.

Solution 1: Use*ngIf

Solution one is the easiest. Use*ngIfin blogger component to delay the initialization of posts components. We willbind the post component only if the posts variable has a value. Then, we are safe to run our grouping logic in posts componentngOnInit.

Our blogger component:

// blogger.component.ts

...
    template: `
        <h1>Posts by: {{ blogger }}</h1>
        <div *ngIf="posts">
            <posts [data]="posts"></posts>
        </div>
    `
...

Our posts component.

// posts.component.ts

...
    ngOnInit() {
        // add this line here
        this.groupPosts = this.groupByCategory(this.data);
    }
...

A few things to note:

  • Since the grouping logic runs inngOnInit, that means it will run only once. If there’s any future updates ondata(passed in from blogger component), it won’t trigger again.
  • Therefore, if someone change theposts: Post[]property in the blogger component toposts: Post[] = [], that means our grouping logic will be triggered once with empty array. When the real data kicks in, it won’t be triggered again.

Solution 2: Use ngOnChanges

ngOnChangesis a lifecycle hook that runwhenever it detects changes to input properties. That means it’s guaranteed that everytime data input value changed, our grouping logic will be triggered if we put our code here.

Please revert all the changes in previous solution

Our blogger component, we don’t need*ngIfanymore.

// blogger.component.ts

...
    template: `
        <h1>Posts by: {{ blogger }}</h1>
        <div>
            <posts [data]="posts"></posts>
        </div>
    `
...

Our posts component

// posts.component.ts

...
    ngOnChanges(changes: SimpleChanges) {
        // only run when property "data" changed
        if (changes['data']) {
            this.groupPosts = this.groupByCategory(this.data);
        }
    }
...

Please notes thatchangesis a key value pair object. The key is the name of theinputproperty, in our case it’sdata. Whenever writing code inngOnChanges, you may want to make sure that the logic run only when the target data changed, because you might have a few inputs.

That’s why we run our grouping logic only if there are changes indata.

One thing I don’t like about this solution is that we lose the strong typing and need to use magic string “data”. In case we change the property namedatato something else, we need to remember to change this as well.

Of course we can defined another interface for that, but that’s too much work.

Solution 3: Use RxJs BehaviorSubject

We can utilize RxJsBehaviorSubjectto detect the changes. I suggest you take a look at the unit test of the official documentherebefore we continue.

Just assume thatBehaviorSubjectis like a property withgetandsetabilities, plus an extra feature; you can subscribe to it. So whenever there are changes on the property, we will be notified, and we can act on that. In our case, it would be triggering the grouping logic.

Please revert all the changes in previous solution

There are no changes in our blogger component:

// blogger.component.ts

...
    template: `
        <h1>Posts by: {{ blogger }}</h1>
        <div>
            <posts [data]="posts"></posts>
        </div>
    `
...

Let’s update our post component to useBehaviorSubject.

// posts.component.ts

...
    // initialize a private variable _data, it's a BehaviorSubject
    private _data = new BehaviorSubject<Post[]>([]);

    // change data to use getter and setter
    @Input()
    set data(value) {
        // set the latest value for _data BehaviorSubject
        this._data.next(value);
    };

    get data() {
        // get the latest value from _data BehaviorSubject
        return this._data.getValue();
    }

    ngOnInit() {
        // now we can subscribe to it, whenever input changes,
        // we will run our grouping logic
        this._data
            .subscribe(x => {
                this.groupPosts = this.groupByCategory(this.data);
            });
    }
...

First of all, if you are not aware, Javacript supportsgetterandsetterlike C# and Java, checkMDNfor more info. In our case, we split thedatato usegetterandsetter. Then, we have a private variable_datato hold the latest value.

To set a value toBehaviorSubject, we use.next(theValue). To get the value, we use.getValue(), as simple as that.

Then during component initialization, we subscribe to the_data, listen to the changes, and call our grouping logic whenever changes happens.

Take a note forobservableandsubject, you need to unsubscribe to avoid performance issues and possible memory leaks. You can do it manually inngOnDestroyor you can use some operator to instruct theobservableandsubjectto unsubscribe itself once it meet certain criteria.

In our case, we would like to unsubscribe once thegroupPostshas value. We can add this line in our subscription to achieve that.

// posts.component.ts

...
    ngOnInit() {
        this._data
            // add this line
            // listen to data as long as groupPosts is undefined or null
            // Unsubscribe once groupPosts has value
            .takeWhile(() => !this.groupPosts)
            .subscribe(x => {
                this.groupPosts = this.groupByCategory(this.data);
            });
    }
...

With this one line.takeWhile(() => !this.groupPosts), it will unsubscribe automatically once it’s done. There are other ways to unsubscribe automatically as well, e.gtake,take Util, but that’s beyond this topic.

By usingBehaviorSubject, we get strong typing, get to control and listen to changes. The only downside would be you need to write more code.

Which One Should I Use?

The famous question comes with the famous answer:It depends.

Use*ngIfif you are sure that your changes run only once, it’s very straightforward. UsengOnChangesorBehaviorSubjectif you want to listen to changes continuously or you want guarantee.

That’s it. Happy Coding!