[Angular] Day14. Built-in directives - structural

在上一章中介绍了 attribute directive 的用法,接着要来介绍另一种 Angular 中内建的 directive 那就是 sturctural directive。

它的用途主要是用来改变 DOM 的结构,他会塑造或重构 DOM 的结构,通常是添加、删除和操作他所附加的 element,本章中将会介绍介绍最常见的三种 structural directive :

  1. NgIf: 有条件的从 tempate 装创建或处置 sub-template,就跟 javascript 的 if 一样,当满足某个条件时显示、隐藏或操作一个新的 sub-template。
  2. NgFor: 为列表中的每个项目重复一个节点,和 javascript 的 for loop 一样,利用迭代的方式创建多个 DOM 的 node。
  3. NgSwitch: 一组在各个 view 中切换的指令,简单来说和 javascript 的 switch case 一样,接收到各种不同的 case 而显示对应的 view

https://ithelp.ithome.com.tw/upload/images/20210821/20124767vgqzFOQpcc.png


Adding or removing an element with NgIf

可以利用 NgIf directive 为宿主 element 添加或删除 element,简单来说当 NgIf 为 false 时从 DOM 中移除一个 element 与他的所有子层并且将它所使用到的所有 component 从内存中释放,反之会新增一个的 element,来举一个简单的例子,当我们点击画面中的按钮时会将 child component 显示出来。

  1. 在 app.component.ts 中新增一个 boolean 的 property 用於决定是否显示 child.component,与一个 method 用来改变这个 property 的状态

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
    })
    export class AppComponent {
      isActive = false;
    
      onClick() {
        this.isActive = !this.isActive;
      }
    }
    
  2. 在 app.component.html 中引用 child.component 的 selector 并使用 *ngIf来控制是否显示他

    <!-- app.component.html -->
    
    <app-child *ngIf="isActive"></app-child>
    
    <button (click)="onClick()">{{ isActive ? 'not display' : 'display' }}</button>
    

img

在画面中可以看到当我们点击了画面中的按钮时,会更改 isActive 的状态,如果 *ngIf 为 true 的话则会将 child.component 显示出来,反之会在 DOM 中将他移除掉。

而要注意如果要使用 structural directive 需要在前面都加上米字号(*)作为前缀字喔!

NgIf and NgIfElse

既然提到了 if 不免俗的也要介绍 else 毕竟他们通常是一组出现的,他与 javascript 的规则一样,当 不满足 if 中的表达式时就会进到 else 中,举个例子吧

  1. 保持上一个例子的 app.component.ts 内容

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
    })
    export class AppComponent {
      isActive = false;
    
      onClick() {
        this.isActive = !this.isActive;
      }
    }
    
  2. 要把 app.component.ts 中的内容稍微改一下

    <!-- app.component.html -->
    
    <app-child-1 *ngIf="isActive; else otherTemplate"></app-child-1>
    
    <ng-template #otherTemplate>
        <app-child-2></app-child-2>
    </ng-template>
    
    <button (click)="onClick()">{{ isActive ? 'not display' : 'display' }}</button>
    

img

在画面中可以看到,当我们更改了 isActive 的状态时,在画面中所呈现的 component 也会不一样,还记得在 template variable 中提到的 哈希符号 # 吗,这边可以把它看成当 *ngIf 为 true 时会显示 <app-child-1> 这个 component,而当为 false 时就会显示 <ng-template> 所包住的 <app-child-2>,记得当要使用 NgIfElse 时需要在绑定的 property 或表达式後面加上分号(;)喔!


Listing items with NgFor

在前几天我们介绍 Text interpolation 时就有使用到 NgFor 的技巧,他就是利用迭代的方式将 component 中 arr 型态的 property 的内容一一呈现在画面中。

<div *ngFor="let item of items">{{item.name}}</div>

在 *ngFor 後面的字串表达式 let item of items, Angular 会对他做以下的处理:

  • 讲 items 阵列中的每一个 item 储存在本地项目循环的变量(使用 let 宣告个变量)
  • 使每个 item 可用於每次迭代的 template
  • let item of items 转换成围绕宿主 element 的
  • 对 list 中的每一个 item 重复

Local variables

了解了 *ngFor 与用法後,这边要介绍一些在使用 *ngFor 时特别好用的技巧,其实在使用 *ngFor 时他不只会将阵列中的值存在本地变量中,你还可以储存别的东西到别的变量中:

  • index: number: 可迭代的 item 中当前的索引值,简单来说就是 javascript 中 array 的 index
  • count: number: 可迭代的长度,等於 javascript 中的 array.length
  • first: boolean: 当 item 是整个 array 中第一个的时候为 true
  • last: boolean: 当 item 是整个 array 中最後一个的时候为 true
  • even: boolean: 当 item 的 index 是偶数时为 true
  • odd: boolean: 当 item 的 index 是基数时为 true

可能你会想说,啊这些有什麽用? 这可是非常好用的啊!

当你在开发专案时,一个列表中可能会需要在他的最後一行加上一个 + 的符号用於新增内容,或对偶数的 item 显示不同颜色等等,这边来搭配着前几天讲的内容做一个小小的例子:

  1. 在 app.component.ts 中新增一个 Heros 里面装着所有英雄的名称,与两个 method

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent {
      isShowAddHero!: boolean;                    // (1)
    
      Heros = [                                   // (2)
        { id: 11, name: 'Dr Nice' },
        { id: 12, name: 'Narco' },
        { id: 13, name: 'Bombasto' },
        { id: 14, name: 'Celeritas' },
        { id: 15, name: 'Magneta' },
        { id: 16, name: 'RubberMan' },
        { id: 17, name: 'Dynama' },
        { id: 18, name: 'Dr IQ' },
        { id: 19, name: 'Magma' },
        { id: 20, name: 'Tornado' },
      ];
    
      onOpenAddHero() {                          // (3)
        this.isShowAddHero = true;
      }
    
      onAddHero(heroName: string) {              // (4)
        this.Heros.push({ id: Math.random(), name: heroName })
        this.isShowAddHero = false;
      }
    }
    
    • (1): 新增一个 property 用於决定是否要开启新增 hero 的介面
    • (2): 新增英雄列表
    • (3): 新增一个 method 当使用者点击按钮时变更 isShowAddHero 状态,用於打开新增 hero 的介面
    • (4): 新增一个 method 当使用者点击新增 hero 介面的按钮,将使用者新增的 hero 名称 push 进英雄列表并将介面关闭。
  2. 在 app.component.html 中新增内容

    <!-- app.component.html -->
    
    <h2 class="title">Hero List</h2>
    
    <div class="heroList" *ngFor="let hero of Heros; last as isLast; odd as isOdd">
        <div class="heroItem" [ngClass]="isOdd ? 'oddClass' : ''">
            {{hero.name}}
            <span *ngIf="isLast">
                <button (click)="onOpenAddHero()" class="openBtn">+</button>
            </span>
        </div>
    </div>
    
    <div *ngIf="isShowAddHero" class="addContent">
        <input type="text" #hero>
        <button (click)="onAddHero(hero.value)">Add hero</button>
    </div>
    

https://ithelp.ithome.com.tw/upload/images/20210808/201247671DRpxMz0Gn.png

这边我们详细的介绍一下,你会看到很多以前都介绍过的内容:

  1. 使用 *ngFor 将 component 中的英雄列表显示在画面中
  2. 使用 last as isLast 来获得最後一项
  3. 使用 odd as isOdd 来获得偶数项
  4. 透过 [ngClass] 动态的利用 isOdd 决定是否要多绑定一个 CSS class
  5. 使用 Text interpolation 将 hero 名称显示在画面中
  6. 使用 *ngIf 判断是否为最後一项,用於决定是否显示新增英雄的按钮
  7. <button>+</button> 中使用 event binding 绑定 component 中的 method
  8. 在下方的 <div> 利用 *ngIf 决定是否要显示在画面上
  9. <input> 中设置为 template variable 让其他地方的 element 获得他的数据
  10. <button>Add hero</button> 中使用 event binding 绑定 component 中的 method 并从 <input> 中透过 template variable 拿到数据做为参数

img

在画面中可以看到,列表中的偶数部分因为符合 [ngClass]="isOdd ? 'oddClass' : ''" 所以多添加了一个变成蓝色的 CSS class,而最下面的 item 因为符合 *ngIf="isLast" 所以只有他有 + 的按钮,而当点击了+後符合 *ngIf="isShowAddHero" 所以会打开新增 hero 的介面,当按下新增 hero 的介面的按钮後将输入在 <input> 的内容 push 近英雄表单中,而+会一直保持出现在最後一个 item 的右边。

这个例子用了很多之前介绍的技巧,如果有看不懂的建议回去复习一下,真的有问题的话有欢迎在下方留言喔!


Hosting a directive without a DOM element

在 Angular 中有一个特别的 HTML element,他就是 <ng-container>,之所以会说他特别是因为他是一个不会干扰样式或布局的分组元素,因为 Angular 不会将他放进 DOM 里面,所以当没有单个 element 可以乘载 directive 时,就可以使用这个特别的 <ng-container>,通常都会将它搭配 structural directive 使用,举个例子吧

<p>
  I turned the corner
  <ng-container *ngIf="hero">
    and saw {{hero.name}}. I waved
  </ng-container>
  and continued on my way.
</p>

上面例子中可以看到,我们希望在 <p> 的段落中透过使用 *ngIf 决定是否要显示英雄名称,如果这时候我们使用其他的 HTML element 来乘载 *ngIf 的话,会造成画面样式跟布局的错误,所以就可以使用 <ng-container>,因为他不会被放进 DOM 里面,作为乘载 structural directive 再好不过。

提到了 <ng-container> 不免俗的要来介绍一下他跟 <ng-template> 的差别:

  • <ng-template> : 是用於呈现 HTML 的 Angular element,他不会直接显示在画面上,需要透过 structural directive 控制它是否要显示在画面中,如果没有被显示在画面中时会以注释的形式呈现在 DOM 中,比较常会用在写结构性变化的时候,比如

    <div *ngIf="show; else notshow">
       当 show = true 时,显示这些内容
    </div>
    <ng-template #notshow>
       当 show = false 时,显示这些内容
    </ng-template>
    
  • <ng-container> : 他是 Angular 解析器识别的语法 element,他不是 directive, component, class 或 interface,他更像是 javascript 的花括号,常被用在不想要多写不必要的 HTML element 时但又想将一个区块的 HTML 包起来处理,就会使用到 <ng-container> 常用来做 structural directive 的载体。

    <p>
      I turned the corner
      <ng-container *ngIf="hero">
        and saw {{hero.name}}. I waved
      </ng-container>
      and continued on my way.
    </p>
    

Switching cases with NgSwitch

最後要来介绍最後一种常见的 structural directive 那就是 NgSwitch,他其实与 Javascript 的 switch case 一样,NgSwitch 会根据 switch 跳健从几个可能的元素中显示其中一个,Angular 只会将符合条件的元素放进 DOM 中,而 NgSwitch 是由三种 directive 组成的:

  • NgSwitch : 一个 attribute directive,
  • NgSwitchCase:structural directive,当他的绑定值等於 switch 的值时将其底下的 element 添加到 DOM 中,当他不等於绑定值时删除他的绑定值。
  • NgSwitchDefault:structural directive,当没有符合任何一个 NgSwitchCase 的值时会将其底下的 element 加入到 DOM 中

一样举个例子

  1. 首先先在 app.component.ts 中新增一个 property 与 method,用於改变 property 状态与接收 event

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
    })
    export class AppComponent {
      showArea = 0;
    
      onClick(number: number) {
        switch (number) {
          case 1:
            this.showArea = 1;
            break;
          case 2:
            this.showArea = 2;
            break;
          case 3:
            this.showArea = 3;
            break;
          default:
            this.showArea = 0;
            break;
        }
      }
    }
    
  2. 在 app.component.html 中新增三个 <button> 用来让使用者点击要显示哪一个内容,并在下方使用 NgSwitch 来判断使用者的选项

    <!-- app.component.html -->
    
    <ul>
        <li>
            <label>Display number-1 area: </label>
            <button (click)="onClick(1)">Area 1</button>
        </li>
        <li>
            <label>Display number-2 area: </label>
            <button (click)="onClick(2)">Area 2</button>
        </li>
        <li>
            <label>Display number-3 area: </label>
            <button (click)="onClick(3)">Area 3</button>
        </li>
        <li>
            <label>Display default area: </label>
            <button (click)="onClick(99)">default</button>
        </li>
    </ul>
    
    <div [ngSwitch]="showArea">
        <div *ngSwitchCase="1">
            <h2>This is area 1</h2>
            <p>Hello world</p>
        </div>
        <div *ngSwitchCase="2">
            <h2>This is area 2</h2>
            <p>Learning Angular</p>
        </div>
        <div *ngSwitchCase="3">
            <h2>This is area 3</h2>
            <p>Know how to use ngSwitch</p>
        </div>
        <div *ngSwitchDefault>
            <h2>This is default area</h2>
            <p>Please select a checkbox</p>
        </div>
    </div>
    

img

在画面中可以看到当我们点击画面中的不同按钮时,会改变 showArea 的值,进而改变下方显示的区域内容,我特意在 html 与 ts 档中都有使用 switch case 的语法,可以互相比对一下他们的用法,基本上都是差不多的,所以等於说只要会 javascript 的 switch case 语法就同时也学会了 ngSwitch!


结论

本章中介绍了常见的 structural directive 与他们各自的用法,其实 structural directive 的使用方法基本上都跟 Javascript 类似,所以等於说之前知道 Javascript 的 if else, for loop, switch case 的用法的话这一章会学得非常快,不过要注意的是 <ng-container><ng-template> 的差别与使用场景,除此之外其他的应该都相对简单。

虽然本章介绍了常见的内建 structural directive,不过再开发专案时一定会遇到内建 structural directive 无法处理的问题,这时候就跟 pipe 一样需要客制化了,下一章将会介绍如何客制化自己的 attribute directive 用於处理内建 attribute directive 无法处理的问题,那我们就明天见吧


Reference


<<:  [Day06] swift & kotlin 入门篇!(4) 基础语法-转型与合并使用

>>:  Day2:AWS Shared Responsibility Model

新手要如何开始做B2C电商? 如何在开店平台架设品牌官网?

我认为想要做电商的新手,必须要掌握以下几点: 1. 确定产品和货源 成立电商第一步就是要确认自己所要...

D27. 学习基础C、C++语言

D27. 跳脱字元 跳脱字元指的是脱离原字元的意思,例如 " 原本在C++中是用来当作字串...

Windows event 59 sidebyside invalid

工作上遇到了些问题纪录一下 因作业关系,修改了服务中的设定档 服务启动发生错误: 错误14001 使...

【Day 15】- 汇率什麽的。爬! (实战汇率爬虫 on chrome)

前情提要 前一篇带各位实作了爬取 Ubuntu ISO 映像档的爬虫,并存在 JSON 档。 开始之...

Day 4 - 条件运算式

大家好,我是长风青云。今天是铁人赛第四天。 今天我真的有点赶,我觉得我以後还是不要当天再开始好了。 ...