了解Angular中的嵌套组件以及如何使用它们

672 阅读7分钟

在本教程中,我们将了解Angular中的嵌套组件以及如何使用它们。为什么要嵌套组件?它有助于组织用户界面的布局,此外还允许将特定的功能打包并在其他组件中重复使用。这意味着一个嵌套的组件需要一种方式来与它的父级或包含的组件沟通。如果你愿意,组件可以嵌套多次。你几乎可以认为是多个盒子,最大的盒子在外面,小盒子包含在里面。由于每个组件都是完全封装的,Angular利用输入和输出来创建自己边界之外的通信路径。


创建一个要嵌套的组件

首先,让我们在Angular app文件夹中创建一个共享目录。
angular shared directory

在这个文件夹中,我们可以添加我们将用于构建组件的文件。这将是一个拇指组件,根据游戏的评级显示一些大拇指的图标。我们需要一个thumb.component.html、thumb.component.css和thumb.component.ts文件来实现。
nested component in shared directory

一般来说,一个嵌套组件应该利用一个模板来管理一个大界面的一小部分。该组件应该有一个定义的选择器,以便它可以嵌套在一个包含组件中。此外,它通常会有一个使用@Input()和@Output装饰器与它的父类沟通的方法。对于我们的目的,我们将创建一个显示拇指的组件,并可以嵌套在game-list.component.html模板中。我们的结构看起来有点像这样。
angular nested component diagram

在拇指组件的模板中,我们可以从以下标记开始。它利用了font-awesome来显示大拇指的符号。为了让该组件显示各种数量的大拇指,父类需要以@Input()的形式向拇指组件提供评级数量。另外,我们将使用@Output()和事件发射器在嵌套组件中添加引发事件的能力。


thumb.component.html

<div class="crop"
     [style.width.px]="thumbWidth"
     [title]="rating">
    <div class="thumb-container">
        <span class="fa fa-thumbs-up"></span>
        <span class="fa fa-thumbs-up"></span>
        <span class="fa fa-thumbs-up"></span>
        <span class="fa fa-thumbs-up"></span>
        <span class="fa fa-thumbs-up"></span>
    </div>
</div>

该组件将显示5个大拇指,但我们可以根据游戏的评级,裁剪该组件以显示少于5个大拇指。所以我们可以看到一些属性绑定,所以现在让我们在相关的Typescript文件中定义拇指组件及其绑定。

rating属性是一个由游戏数组中的数据提供的数字。该 thumbWidth是根据评级的值计算的。这发生的方式是使用ngOnChanges()生命周期钩。


thumb.component.ts

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

@Component({
    selector: 'game-thumb',
    templateUrl: './thumb.component.html',
    styleUrls: ['./thumb.component.css']
})
export class ThumbComponent implements OnChanges {
    rating = 3;
    thumbWidth: number;

    ngOnChanges(): void {
        this.thumbWidth = this.rating * 95 / 5;
    }
}

组件样式表中的这个CSS样式将给我们带来我们想要的裁剪效果。


thumb.component.css

.crop {
    overflow: hidden;
}

div {
    cursor: pointer;
}

span {
    margin: 2px;
}

.thumb-container {
    width: 100px;
}

嵌套组件为指令

为了使用嵌套组件,我们首先需要把它添加到一个模块的声明中。


app.module.ts

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

import {AppComponent} from './app.component';
import {GameListComponent} from './games/game-list.component';
import {ThumbComponent} from './shared/thumb.component';

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

game-list.component.html

现在我们可以深入到父组件模板中,使用它的选择器添加嵌套组件。既然我们已经有了所有的管道,看看IDE是如何以类似IntelliSense的方式发现新的game-thumb组件的。
angular intellisense for components

注意突出显示的那一行显示了嵌套组件被引用的地方。

<div class='card'>
    <div class='card-header'>
        {{pageTitle}}
    </div>
    <div class='card-body'>
        <div class="row">
            <div class="col-3">
                <input [(ngModel)]="listFilter" type="text" class="form-control" id="filterInput" placeholder="Type to filter">
            </div>
            <div class="col">
                <div *ngIf='listFilter' class="form-text text-muted">Filtered by: {{listFilter}}</div>
            </div>
        </div>
        <div class='table-responsive'>
            <table class='table' *ngIf='games && games.length'>
                <thead>
                <tr>
                    <th>
                        <button class='btn btn-primary'
                                (click)='toggleImage()'>
                            {{showImage ? 'Hide ' : 'Show'}} Image
                        </button>
                    </th>
                    <th style="color:firebrick">Game</th>
                    <th>Part#</th>
                    <th>Release Date</th>
                    <th>Cost</th>
                    <th>5 Thumb Rating</th>
                </tr>
                </thead>
                <tbody>
                <tr *ngFor='let game of filteredGames'>
                    <td>
                        <img *ngIf='showImage'
                             [src]='game.imageUrl'
                             [title]='game.gameName'
                             [style.width.px]='imageWidth'
                             [style.margin.px]='imageMargin'>
                    </td>
                    <td>{{ game.gameName }}</td>
                    <td>{{ game.gameCode | lowercase }}</td>
                    <td>{{ game.releaseDate }}</td>
                    <td>{{ game.price | currency:'USD':'symbol':'1.2-2' }}</td>
                    <td><game-thumb></game-thumb></td>
                </tr>
                </tbody>
            </table>
        </div>
    </div>
    <div class="card-footer text-muted text-right">
        <small>Powered by Angular</small>
    </div>
</div>

在这一点上,嵌套组件似乎在工作。它的功能并不全面,因为无论游戏的评分是多少,它都只是显示5个拇指。其原因是ngOnChanges()没有启动,因为它只观察输入属性的变化。我们还没有进行配置,但接下来会这样做。
angular component as directive example


使用@Input的父子沟通

为了让一个嵌套组件从它的父辈那里接收输入,它可以暴露一个属性来这样做。这是用**@Input()装饰器**完成的。这个装饰器可以用来装饰嵌套组件类中的任何属性。在下面的标记中,我们指定要把 **rating**值传递到嵌套组件中。


thumb.component.ts

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

@Component({
    selector: 'game-thumb',
    templateUrl: './thumb.component.html',
    styleUrls: ['./thumb.component.css']
})
export class ThumbComponent implements OnChanges {
    @Input() rating: number;
    thumbWidth: number;

    ngOnChanges(): void {
        this.thumbWidth = this.rating * 95 / 5;
    }
}

现在在父组件的模板中,属性绑定被用来向嵌套组件传递数据。注意突出显示的代码中方括号的使用。绑定源被设置为父组件想要发送给子组件的数据。所以这个模板是将game.thumbRating的值传递给拇指组件。


game-list.component.html

<div class='card'>
    <div class='card-header'>
        {{pageTitle}}
    </div>
    <div class='card-body'>
        <div class="row">
            <div class="col-3">
                <input [(ngModel)]="listFilter" type="text" class="form-control" id="filterInput"
                       placeholder="Type to filter">
            </div>
            <div class="col">
                <div *ngIf='listFilter' class="form-text text-muted">Filtered by: {{listFilter}}</div>
            </div>
        </div>
        <div class='table-responsive'>
            <table class='table' *ngIf='games && games.length'>
                <thead>
                <tr>
                    <th>
                        <button class='btn btn-primary'
                                (click)='toggleImage()'>
                            {{showImage ? 'Hide ' : 'Show'}} Image
                        </button>
                    </th>
                    <th style="color:firebrick">Game</th>
                    <th>Part#</th>
                    <th>Release Date</th>
                    <th>Cost</th>
                    <th>5 Thumb Rating</th>
                </tr>
                </thead>
                <tbody>
                <tr *ngFor='let game of filteredGames'>
                    <td>
                        <img *ngIf='showImage'
                             [src]='game.imageUrl'
                             [title]='game.gameName'
                             [style.width.px]='imageWidth'
                             [style.margin.px]='imageMargin'>
                    </td>
                    <td>{{ game.gameName }}</td>
                    <td>{{ game.gameCode | lowercase }}</td>
                    <td>{{ game.releaseDate }}</td>
                    <td>{{ game.price | currency:'USD':'symbol':'1.2-2' }}</td>
                    <td>
                        <game-thumb [rating]="game.thumbRating"></game-thumb>
                    </td>
                </tr>
                </tbody>
            </table>
        </div>
    </div>
    <div class="card-footer text-muted text-right">
        <small>Powered by Angular</small>
    </div>
</div>

现在,拇指是动态的这是因为拇指组件正在接收正确的值,以根据评级来显示拇指的数量。
angular input decorator binding


用@Output从子代到父代进行交流

在上面的片段中,我们现在有了从父类到子类组件的通信。现在,我们希望有能力进行反向通信。例如,如果有东西在子组件中被点击,我们可以将该事件传递给父组件。这可以通过与**@Output()装饰器**和EventEmitter结合引发一个事件来实现。事实上,一个嵌套组件将数据传递给其父级的唯一方法是使用一个事件。要传递的数据就是事件的有效载荷

首先,我们可以指定当拇指组件被点击时,onClick()方法将运行。


thumb.component.html

<div class="crop"
     [style.width.px]="thumbWidth"
     [title]="rating"
     (click)="onClick()">
    <div class="thumb-container">
        <span class="fa fa-thumbs-up"></span>
        <span class="fa fa-thumbs-up"></span>
        <span class="fa fa-thumbs-up"></span>
        <span class="fa fa-thumbs-up"></span>
        <span class="fa fa-thumbs-up"></span>
    </div>
</div>

然后在组件中,我们必须定义 **onClick()**方法。此外,我们还通过使用 **ratingClicked**作为一个事件属性,使用@Output()装饰器。这个属性使用EventEmitter来发射一个事件。


thumb.component.ts

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

@Component({
    selector: 'game-thumb',
    templateUrl: './thumb.component.html',
    styleUrls: ['./thumb.component.css']
})
export class ThumbComponent implements OnChanges {
    @Input() rating: number;
    thumbWidth: number;
    @Output() ratingClicked: EventEmitter<string> =
        new EventEmitter<string>();

    ngOnChanges(): void {
        this.thumbWidth = this.rating * 95 / 5;
    }

    onClick(): void {
        this.ratingClicked.emit(`This game has a ${this.rating} thumb rating!`);
    }
}

下面是包含组件,我们可以在这里设置事件绑定。我们使用*(ratingClicked)='onRatingClicked(event)表示的事件绑定到从拇指组件引发的事件。这意味着我们正在监听从拇指组件发出的评级点击事件。当该事件被触发时,我们要运行onRatingClicked()方法,并将event)'*表示的事件绑定到从拇指组件引发的事件。这意味着我们正在监听从拇指组件发出的*评级点击*事件。当该事件被触发时,我们要运行 **`onRatingClicked()`**方法,并将**event**作为有效载荷。这个方法接下来将在game-list.component.ts中定义。


game-list.component.html

<div class='card'>
    <div class='card-header'>
        {{pageTitle}}
    </div>
    <div class='card-body'>
        <div class="row">
            <div class="col-3">
                <input type="text" [(ngModel)]='listFilter' class="form-control" id="filterInput"
                       placeholder="Type to filter">
            </div>
            <div class="col">
                <div *ngIf='listFilter' class="form-text text-muted">Filtered by: {{listFilter}}</div>
            </div>
        </div>
        <div class='table-responsive'>
            <table class='table'
                   *ngIf='games && games.length'>
                <thead>
                <tr>
                    <th>
                        <button class='btn btn-primary'
                                (click)='toggleImage()'>
                            {{showImage ? 'Hide ' : 'Show'}} Image
                        </button>
                    </th>
                    <th>Game</th>
                    <th>Part#</th>
                    <th>Release Date</th>
                    <th>Cost</th>
                    <th>5 Thumb Rating</th>
                </tr>
                </thead>
                <tbody>
                <tr *ngFor='let game of filteredGames'>
                    <td>
                        <img *ngIf='showImage'
                             [src]='game.imageUrl'
                             [title]='game.gameName'
                             [style.width.px]='imageWidth'
                             [style.margin.px]='imageMargin'>
                    </td>
                    <td>
                        <a [routerLink]="['/games', game.gameId]">
                            {{ game.gameName }}
                        </a>
                    </td>
                    <td>{{ game.gameCode | lowercase | convertToColon: '-' }}</td>
                    <td>{{ game.releaseDate }}</td>
                    <td>{{ game.price | currency:'USD':'symbol':'1.2-2' }}</td>
                    <td>
                        <game-thumb [rating]='game.thumbRating'
                                    (ratingClicked)='onRatingClicked($event)'>
                        </game-thumb>
                    </td>
                </tr>
                </tbody>
            </table>
        </div>
    </div>
    <div class="card-footer text-muted text-right">
        <small>Powered by Angular</small>
    </div>
</div>
<div *ngIf='errorMessage'
     class='alert alert-danger'>
    Error: {{ errorMessage }}
</div>

这里是onRatingClicked()方法,它只是根据事件的有效载荷将页面标题设置为新的内容。


game-list.component.ts

import { Component, OnInit } from '@angular/core';
import { IGame } from './game';

@Component({
    selector: 'game-list',
    templateUrl: './game-list.component.html',
    styleUrls: ['./game-list.component.css']
})
export class GameListComponent {
    pageTitle = 'Dynamic! Game List';
    imageWidth = 45;
    imageMargin = 1;
    showImage = true;
    _listFilter = '';
    get listFilter(): string {
        return this._listFilter;
    }

    set listFilter(value: string) {
        this._listFilter = value;
        this.filteredGames = this.listFilter ? this.doFilter(this.listFilter) : this.games;
    }

    filteredGames: IGame[] = [];
    games: IGame[] = [...];

    constructor() {
        this.filteredGames = this.games;
        this.listFilter = '';
    }

    onRatingClicked(message: string): void {
        this.pageTitle = 'Game List: ' + message;
    }

    doFilter(filterBy: string): IGame[] {
        filterBy = filterBy.toLocaleLowerCase();
        return this.games.filter((game: IGame) =>
            game.gameName.toLocaleLowerCase().indexOf(filterBy) !== -1);
    }

    toggleImage(): void {
        this.showImage = !this.showImage;
    }
}

看起来不错


总结

嵌套组件的关键点

  • 嵌套组件的构建步骤与你可能在Angular中构建的其他组件相同。
  • @Input()装饰器可以在嵌套组件的属性上使用,任何时候它都需要从它的父组件或包含组件中获得输入数据。
  • 任何类型的属性都可以用@Input()装饰器进行装饰。
  • @Output装饰器是用来装饰一个嵌套组件属性的,当它需要引发事件来传递给父类时。
  • 只有类型EventEmitter的属性才能被@Output 装饰器使用。
  • 使用通用参数来指定事件有效载荷的类型。
  • 使用 new关键字来创建一个EventEmitter的实例。

含有组件的关键点

  • 在包含组件的模板中,使用嵌套组件作为指令。
  • 指令的名称是基于选择器属性的。
  • 使用属性绑定来传递数据给嵌套组件。
  • 嵌套组件上用 @Input()装饰器装饰的属性可以被用作绑定目标。
  • 使用事件绑定来响应来自嵌套组件的事件。
  • 一个在嵌套组件上用@Output()装饰器装饰的属性可以被用作绑定目标。
  • 为了访问事件的有效载荷,你可以使用从嵌套组件传递过来的$event变量。