Angular5-项目教程-二-

75 阅读27分钟

Angular5 项目教程(二)

原文:Angular 5 Projects

协议:CC BY-NC-SA 4.0

八、组件介绍

Angular 组件类似于 AngularJS 控制器。组件基本上是标记、元数据和一个类(包含数据和代码),它们组合在一起创建一个 UI 小部件。组件是我们用来构建交互式 UI 的主要工具。所有 Angular 应用都有一个根组件,通常称为应用组件。

Angular 为组件之间传递数据和响应彼此的事件提供了方法。我们将在第十二章讨论组件输入和输出。

您可以编写一个组件,并在其他几个组件中将它作为子组件使用——出于这个目的,它们被设计为自包含和松散耦合的。每个组件都包含关于自身的有价值的数据:

  • 它需要什么数据作为输入
  • 它可能向外界发射什么事件
  • 怎么画自己
  • 它的依赖关系是什么

通常当你开发组件时,三个文件中的每一个都有一个组件,因为一个组件有三个部分:模板、类和样式。默认情况下,CLI 就是这样工作的。例如,当您使用命令ng new [project name]在 CLI 中创建一个应用时,CLI 会为该应用组件生成三个文件(如果您包括. spec.ts 测试文件,则会生成更多文件):

  • app.component.css:样式
  • app.component.html:模板
  • 应用组件:类

然而,这不是你唯一的选择。以下是更多选项:

  • 将样式包含在。ts 类文件:这被称为内联样式,它使你不必为组件准备一个样式文件。如前一章所述,使用 CLI --inline-style参数生成具有内联样式的组件。
  • 将模板包含在。ts 类文件:这被称为内联模板,它使你不必为组件准备模板文件。如前一章所述,使用 CLI --inline-template参数生成具有内联样式的组件。
  • 在同一个文件中包含多个组件类:您可以在同一个文件中组合多个组件,如下所示:
import { Component } from '@angular/core';

@Component({
  selector: 'Paragraph',
  template: `
  <p><ng-content></ng-content></p>
  `,
  styles: ['p { border: 1px solid #c0c0c0; padding: 10px }']
})
export class Paragraph {
}

@Component({
  selector: 'app-root',
  template: `
  <p>
  <Paragraph>Lorem ipsum dolor sit amet, consectetur adipiscing elit. </Paragraph>
  <Paragraph>Praesent eget ornare neque, vel consectetur eros. </Paragraph>
  </p>
  `,
  styles: ['p { border: 1px solid black }']
})
export class AppComponent {
  title = 'welcome to app!';
}

您可能会在同一个文件中找到包含多个组件的代码示例。这样做是有意的,以便您可以将更多的代码复制并粘贴到更少的文件中。

当您在应用中使用组件时,您需要确保每个组件都在模块中声明。第九章更详细地介绍了模块。以下是声明两个组件的模块示例:AppComponent 和 Paragraph:

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

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

组件的剖析

注释提供了将所有部分组合成一个组件的元数据。模板通常是 HTML 标记,用于在浏览器中呈现组件,即模型-视图-控制器(MVC)中的视图。它可以包含嵌套组件的标记。一个类有添加元数据的注释并包含数据(was$scope)——MVC 中的模型。它包含行为代码 MVC 中的控制器。

@组件注释

注释位于类的顶部附近,是类中最重要的元素。这是一个将类标记为组件并接受对象的函数。它使用对象向 Angular 提供关于组件以及如何运行它的元数据。注释也称为装饰器。

如果您使用 CLI 生成一个项目,并检查生成的组件 app.component.ts,您将看到下面的@Component注释:

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})

表 8-1 显示了可以添加到@Component注释中的基本元素。

表 8-1

Basic Elements for the @Component Annotation

| 注释元素 | 笔记 | | :-- | :-- | | `selector` | 该组件对应的标记、元素。 | | `template/templateUrl` | 指定包含组件标记的模板。您有两种选择:您可以使用`template`在引号块中指定内联模板。这对简单的模板非常有用。或者您可以使用`templateUrl`来指定外部模板文件的相对路径。这对于更大或更复杂的模板更好。如果模板长度超过 10 行,我通常会将其放在外部模板文件中。 | | `styles/styleUrls` | 指定模板标记的 CSS 信息。您有两种选择:您可以使用`styles`来指定一个内联样式数组。这对于一些样式定义来说非常有用。或者您可以使用`styleUrls`来指定样式定义文件的相对路径数组。当你使用多种风格时,这样更好。如果组件中使用的样式超过 5 种,我通常会将它们放在一个外部样式文件中。 |

selector语法类似于 JQuery 选择器,如表 8-2 所示。

表 8-2

selector Syntax

| 类型 | 例子 | 所选标记的示例 | 笔记 | | :-- | :-- | :-- | :-- | | 名字 | `welcome` | `` | 这是使用选择器最常见的方式。只要确保这个标签是唯一的,并且永远不会被 HTML 使用。为您的项目和其中的所有组件使用一个公共前缀通常是一个好主意。例如,奖励计划项目可以有前缀`rp_`。 | | 身份 | `#welcome` | `
` |   | | CSS 类 | `.welcome` | `'
` |   |
选择器和 DSL

在 Angular 中,您可以创建映射到特定标签(或属性)的 can 组件和指令。例如,如果你正在创建一个销售汽车的应用,你可以像这样使用标签和属性:<CarSearch></CarSearch><CarList></CarList><CarDetail></CarDetail>等等。实际上,通过 Angular 组件和指令,我们正在为我们的应用创建 DSL(特定领域语言)。DSL 是一种专用于特定应用领域的计算机语言。DSL 非常强大,因为它们允许代码特定于应用的领域(它的使用),并以语言形式表示所代表的业务实体。

其他元素

表 8-3 显示了你可以添加到@Component注释中的其他更高级的元素。我们将在后面详细讨论其中的许多内容。

表 8-3

Advanced Elements

| 注释元素 | 笔记 | | :-- | :-- | | `animations` | 该组件的动画列表 | | `changeDetection` | 此组件使用的更改检测策略 | | `encapsulation` | 此组件使用的样式封装策略 | | `entryComponents` | 动态插入到该组件视图中的组件列表 | | `exportAs` | 在模板中导出组件实例时使用的名称 | | `hosts` | 将类属性映射到事件、属性和特性的宿主元素绑定 | | `Inputs` | 要作为组件输入进行数据绑定的类属性名称列表 | | `interpolation` | 此组件模板中使用的自定义插值标记 | | `moduleId` | 定义该组件的文件的 ES/CommonJS 模块 ID | | `outputs` | 公开其他人可以订阅的输出事件的类属性名称列表 | | `providers` | 此组件及其子组件可用的提供程序列表 | | `queries` | 配置可以注入组件的查询 | | `viewProviders` | 此组件及其视图子级可用的提供程序列表 |

组件模板

该模板包含在 web 浏览器中显示组件的标记代码。关于组件模板的信息由注释提供。

模板位置

模板标记可以包含在与Component类相同的文件中,也可以包含在单独的文件中:

以下是@ Component注释中内嵌的模板标记:

@Component({
  selector: 'app-root',
  template: `
  <div class="app">
  [app]
  <app-customer-list>
  </app-customer-list>
  </div>
  `,
  styles: ['.app {background-color:#d5f4e6;margin:10px;padding:10px;}']
})

下面是包含在一个单独文件中的模板标记:

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})

脚本标签

组件模板中不允许使用<script>标记。为了消除脚本注入攻击的风险,这是被禁止的。实际上,<script>被忽略,浏览器控制台中会出现一条警告。

换句话说,永远不要这样做:

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

@Component({
  selector: 'app-root',
  template: `
  <h1>
    {{title}}
  </h1>
  <script>
    alert('app works');
  </script>
  `,
  styles: []
})
export class AppComponent {
  title = 'welcome to app!';
}

Elvis 操作员

该操作员也被称为“安全导航操作员”。

Angular 经常出现空值问题,尤其是模板表达式。通常你会有一个模板突然停止工作,因为你添加的代码引用了一个未初始化的变量。例如,假设我们有一个为空的对象x,我们有以下代码:

Total {{x.totalAmt}}

这将导致 JavaScript 和 Zone 问题(稍后会有更多关于 Zone 的内容),您的组件将突然无法呈现。我希望我每次遇到这种事都能得到一块钱。

幸运的是,“猫王”接线员帮助了我们。简单来说,Elvis 操作符就是模板表达式中可能为空的变量旁边的一个问号。一旦发现该变量为空,Elvis 操作符就告诉代码退出,留下一个空白。这将停止对属性的评估,并绕过 JavaScript 问题:

Total {{x?.totalAmt}}

有时在模板表达式中需要多个 Elvis 运算符:

Total {{x?.amt?.total}}

组件样式

样式包含更改组件样式所需的 CSS 规则。关于组件模板的信息由style注释提供。您可以在元件或外部文件中指定元件的样式。创建 Angular CLI 项目时,其样式文件在。angular-cli.json 文件。

样式可以包含在与Component类相同的文件中,也可以包含在单独的文件中。

以下是@ Component注释中内联包含的样式标记:

@Component({
  selector: 'app-root',
  template: `
  <div class="app">
  [app]
  <app-customer-list>
  </app-customer-list>
  </div>
  `,
  styles: ['.app {background-color:#d5f4e6;margin:10px;padding:10px;}']
})

下面是包含在一个单独文件中的样式标记:

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})

组件类

此 TypeScript 类包含组件的数据和代码。数据包含在变量中,变量可以绑定到模板中的 HTML 标记。代码可以响应用户事件(比如单击按钮)或者调用自身来开始做事情。

别担心,这只是一个介绍——我们将在第十二章更详细地讨论组件类。

MVC: Model View Controller

MVC 是一种编写程序的方式,主要用于在计算机上实现用户界面。它将给定的软件应用分成三个相互联系的部分:模型(数据)、视图(用户看到的)和控制器(更新模型的命令)。在 Angular 的上下文中,可以说模型是Component类中的数据,视图是组件模板,控制器可以是Component类中的代码。

数据绑定简介

数据绑定是 Angular 如此受欢迎的原因——组件 UI 小部件的元素与组件类中的数据的同步,反之亦然,由 Angular 为您管理。您在组件类中设置变量来存储数据,并在组件模板中编辑 HTML 来添加数据绑定。现在,您的 HTML 不再是静态的—它会随着您的数据而变化!如果您希望您的组件与用户交互,您必须在其中使用数据绑定。

就 MVC 而言,在运行时 Angular 使用变化检测来确保组件视图总是反映组件模型。使用数据绑定,您可以通过更改变量来控制组件的用户界面,并且可以接受用户输入,允许他们更改某些变量的值。数据绑定可以控制用户界面的每个方面:隐藏东西、关闭东西、显示结果、接受用户输入等等。它非常强大而且易于使用。

示例:登录组件中的数据绑定

假设你有一个带有两个字段的登录表单,如图 8-1 所示。每个字段在组件模板的 HTML 中都有一个文本框,每个字段在Component类中都有一个对应的实例变量。文本框和实例变量相互绑定。如果有人输入用户名,username实例变量会用新值更新。当开发人员编写提交按钮时,他们从实例变量中获得用户名和密码,而不是从 HTML 中提取它们。

A458962_1_En_8_Fig1_HTML.jpg

图 8-1

Login form with two fields

示例:数据绑定和客户数据输入

您可以拥有一个表单,该表单允许您使用字段输入客户信息。每个字段在组件模板的 HTML 中都有一个文本框,每个字段在Component类中都有一个对应的实例变量。Angular 数据绑定使您能够在用户向 HTML 字段输入信息时自动更新实例变量。它还使您能够在更改实例变量的值时自动更新 HTML 字段,以及在用户向文本框中键入内容时自动更新实例变量。当开发人员想要默认输入字段的值时,他们需要做的就是设置实例变量值。HTML 文本框将自动更新。

有两种主要类型的数据绑定—单向和双向:

A458962_1_En_8_Fig2_HTML.jpg

图 8-2

Two-way data binding

  1. 单向数据绑定:当模板(视图)自动与类实例变量(模型)中的最新值保持同步时,就会出现这种情况。更新只向一个方向流动。当类实例变量(模型)自动与从模板(视图)输入的值保持同步时,也会发生单向数据绑定。更新仍然是单向的。
  2. 双向数据绑定:这是类实例变量(模型)和模板(视图)互相保持最新的时候。更新流向两个方向,如图 8-2 所示。

单向数据绑定

本节重点介绍 Angular 中单向数据绑定的各个方面。

与{{和}}的单向数据绑定

那些双花括号也被称为小胡子或插值。双花括号用于单向绑定模板表达式,根据模型中的可用数据进行计算,并将其包含在视图中。表达式产生一个值,它包含在视图中(来自组件模板的标记)。模型——即Component类中的数据——从不更新。

模板表达式通常是一个简单的 JavaScript 表达式。通常模板表达式只是模型中一个属性的名称(即Component类中的一个实例变量)。Angular 用属性的字符串值(实例变量的字符串值)替换该属性名称。

有时模板表达式会变得更复杂。Angular 试图对表达式(可以包含数学、属性名、方法调用等等)求值,并将求值结果转换为字符串。然后用结果替换内容和花括号。

以下是花括号和模板表达式的一些示例:

  • {{2+2}}
  • {{firstName}}
  • {{1 + 1 + getVal()}}

单向数据绑定:示例代码组件-ex100

下面的讨论将是关于示例组件-ex100:

  1. 使用 CLI 构建应用:输入以下命令,这将在一个名为 Start 的文件夹中创建一个新的 Angular 应用,还将创建并输出大量文件:

    ng new components-ex100 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd components-ex100
    ng serve
    
    
  3. Open app: Launch your web browser and browse to localhost:4200. You should see the text “welcome to app!” as shown in Figure 8-3. That means your project is running.

    A458962_1_En_8_Fig3_HTML.jpg

    图 8-3

    Your project is running  

  4. 编辑组件:编辑文件 src/app/app.component.ts,并将其更改为:

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `
      <h1>
        {{title}}
      </h1>
      <p>
        Length: {{title.length}}
      </p>
      <p>
        Reversed: {{getReversed(title)}}
      </p>
      `,
      styles: []
    })
    export class AppComponent {
      title = 'welcome to app!';
    
      getReversed(str: string){
        let reversed = '';
        for (let i=str.length-1;i>=0;i--){
          reversed += str.substring(i,i+1);
        }
        return reversed;
      }
    }
    
    

你的应用应该工作在本地主机:4200。请注意模板如何使用两个表达式:一个显示标题的长度,另一个使用类中的方法反转标题。

A458962_1_En_8_Figa_HTML.jpg

使用[ and ]或*的单向数据绑定

方括号可用于单向绑定。通过这些,您可以绑定一个模板表达式,根据模型中的可用数据进行计算,并将其包含在数据绑定目标中。

您也可以使用前缀*来代替双方括号:

[Data Binding Target] = "Template Expression"

或者:

*Data Binding Target = "Template Expression"

数据绑定目标是 DOM 中的一些东西(包括元素属性、组件属性和指令属性),可以绑定到目标右侧的表达式的结果,如表 8-4 所示。

表 8-4

Data Binding Target Markup

| 利润 | 描述 | | :-- | :-- | | `` | 将图像源设置为模型中的属性`imageUrl`。 | | `
` | 根据模型中的属性`isSelected`设置 CSS 类。 | | `` | 将模型中`car-detail`的`car`属性设置为属性`selectedCar`。`car-detail`可以是一个组件,它会使用`car`属性将信息从当前模板传递给那个组件。 | | `` | 根据模型中的属性`isSpecial`设置`button`颜色。 |

模板表达式用于根据模型中的可用数据计算值。

单向数据绑定:示例代码组件-ex200

下面的讨论将是关于示例组件-ex200:

  1. 使用 CLI 构建应用:输入以下命令:

    ng new components-ex200 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd components-ex200
    ng serve
    
    
  3. 打开应用:启动 web 浏览器,浏览到 localhost:4200。您应该看到文本“欢迎使用应用!”

  4. 编辑组件:编辑文件 src/app/app.component.ts,并将其更改为:

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `
      <h1>Doesnt work:</h1>
      <img src="starUrl">
      <h1>Works:</h1>
      <img [src]="starUrl">
      `,
      styles: []
    })
    export class AppComponent {
      starUrl = 'https://developer.mozilla.org/samples/cssrimg/starsolid.gif';
    }
    
    

你的应用应该工作在本地主机:4200。注意以下事项(并参见图 8-4 ):

A458962_1_En_8_Fig4_HTML.jpg

图 8-4

Author please add caption

  • 第一个图像标签失败,因为它没有换行src。它从字面上理解startUrl,而不是从一个实例变量中将它作为一个表达式来计算。
  • 第二个 image 标签可以工作,因为它将src放在方括号中,这意味着这是一个需要计算startUrl实例变量的值的表达式。

有时,您需要在模板生成的 HTML 元素中动态创建属性。如果您希望在 HTML 中包含数据,稍后可以使用 JavaScript 代码提取这些数据,这将非常有用。例如:

<li
  id="12345"
  data-make="bmw"
  data-model="m3"
  data-parent="cars">
...
</li>

在这种情况下,id标签用于标识元素(对 JavaScript 和 CSS 非常有用),还有各种存储信息的数据元素。

在 Angular 中,你可以使用[attr.***name***]语法在生成的 HTML 中设置属性。

单向数据绑定:示例代码组件-ex250

这个组件会列出一些汽车,让你点击查看按钮查看一篇关于汽车的文章,如图 8-5 所示。

A458962_1_En_8_Fig5_HTML.jpg

图 8-5

Component that lists cars

该组件有趣的地方在于它在每个<li>元素中存储属性数据。这将是关于示例组件-ex250:

  1. 使用 CLI 构建应用:输入以下命令:

    ng new components-ex250 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd components-ex250
    ng serve
    
    
  3. 打开应用:打开 web 浏览器,浏览到 localhost:4200。您应该看到文本“欢迎使用应用!”这意味着您的项目正在运行。

  4. 编辑组件:编辑文件 src/app/app.component.ts,并将其更改为:

    import { Component } from '@angular/core';
    import { Car } from './car';
    @Component({
      selector: 'app-root',
      template: `
        <ul>
          <li *ngFor="let car of _cars">
            <span [attr.id]="car.id" [attr.data-desc]="car.make + ' ' + car.model" [attr.data-article]="car.article">
              {{car.year}}&nbsp;{{car.make}}&nbsp;{{car.model}}&nbsp;<button (click)="showCar($event)">View</button></span>
          </li>
        </ul>
      `,
      styles: []
    })
    export class AppComponent {
      _cars = [
        new Car('car1', 2002, 'bmw', 'm3', 'https://en.wikipedia.org/wiki/BMW_M3'),
        new Car('car2', 2017, 'acura', 'nsx', 'https://en.wikipedia.org/wiki/Honda_NSX'),
        new Car('car3', 2016, 'chevy', 'camaro', 'https://en.wikipedia.org/wiki/Chevrolet_Camaro')
      ];
    
      showCar(event){
        const desc = event.target.parentElement.dataset.desc;
        if (window.confirm('If you click "ok" you would be redirected to an article about the ' +
            desc + '. Cancel will load this website '))
          {
          window.location.href=event.target.parentElement.dataset.article;
          };
      }
    }
    
    
  5. 创建类:创建文件 src/app/car.ts,并将其更改为:

    export class Car {
        constructor(
            private _id: string,
            private _year: number,
            private _make: string,
            private _model: string,
            private _article: string){
        }
    
        public get id() : string {
            return this._id;
        }
    
        public get year() : number {
            return this._year;
        }
    
        public get make() : string {
            return this._make;
        }
    
        public get model() : string {
            return this._model;
        }
    
        public get article() : string {
            return this._article;
        }
    
    }
    
    

您已经到达练习的末尾。请注意以下几点:

  • desc数据属性是如何产生的:

    [attr.data-desc]="car.make + ' ' + car.model"
    
    
  • 如何使用 JavaScript 获取desc数据属性:

    const desc = event.target.parentElement.dataset.desc;
    
    

双向数据绑定

本节重点介绍双向数据绑定。

使用[(和)]的双向数据绑定

[()]又称盒中香蕉。实际上,你已经看过了。[()]格式用于双向绑定属性——换句话说,从模型中读取和写入属性。格式如下:

[(Data Binding Target)] = "Property"

“数据绑定目标”是 DOM(包括ComponentDirective标签)中可以绑定到目标右侧表达式的属性的东西。对于输入框,数据绑定目标是ngModel,它对应于输入框中的文本。

这是模型中的一个属性(Component类中的一个实例变量)。

双向数据绑定:示例代码组件-ex300

这是一个在用户改变输入时改变前景色和背景色的组件,如图 8-6 所示:

  1. 使用 CLI 构建应用:输入以下命令:

    ng new components-ex300 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd components-ex300
    ng serve
    
    
  3. 打开应用:打开 web 浏览器,浏览到 localhost:4200。您应该看到文本“欢迎使用应用!”这意味着您的项目正在运行。

  4. 编辑模块:编辑文件 src/app/app.module.ts,并将其更改为:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    
    import { AppComponent } from './app.component';
    import { FormsModule } from '@angular/forms';
    
    @NgModule({
      declarations: [
        AppComponent
      ],
      imports: [
        BrowserModule,
        FormsModule
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    
  5. Edit component: Edit the file src/app/app.component.ts and change it to the following:

    A458962_1_En_8_Fig6_HTML.jpg

    图 8-6

    Changing foreground and background colors

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `
      <p>
      Foreground: <input [(ngModel)]="fg" />
      </p>
      <p>
      Background: <input [(ngModel)]="bg" />
      </p>
      <div [ngStyle]="{'color': fg, 'background-color': bg, 'padding': '5px'}">
      Test
      </div>
      `,
      styles: []
    })
    export class AppComponent {
      fg = "#ffffff";
      bg = "#000000";
    }
    
    

你的应用应该工作在本地主机:4200。当用户更改颜色值时,这会更新模型,然后更新模板的 HTML:

  • 从输入字段到模型发生绑定(当用户改变颜色值时)。当输入字段改变时,模型更新以匹配。
  • 从模型到模板的 HTML 发生绑定。当模型更新时,模板的 HTML 也随之更新。

事件处理

用户界面需要响应用户输入。这就是为什么我们在组件模板中有事件处理。我们指定一个目标事件,以及当该事件发生时应该发生哪个语句。格式是这样的:

(Target Event) = "Template Statement"

“目标事件”是括号中事件的名称。“模板语句”是关于目标事件发生时该做什么的指令。通常,这是对Component类中的一个方法的调用,该方法执行某些操作——通常,修改绑定到模板的实例变量,导致 UI 发生变化。事件信息在$event变量中可用,它可能被利用也可能不被利用。例如,如果您正在观察文本框中的输入,您可以使用来自$event的信息将文本框中的文本值传递给该方法。您将在下一个示例中看到这一点。

事件处理:示例代码组件-ex400

该组件接受文本框中的输入,捕获输入事件,并以大写和小写显示输入,如图 8-7 所示:

  1. 使用 CLI 构建应用:输入以下命令:

    ng new components-ex400 --inline-template --inline-style
    
    
  2. 开始ng serve:使用以下代码:

    cd components-ex400
    ng serve
    
    
  3. 打开应用:启动 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. Edit class: Edit app.component.ts and change it to the following:

    A458962_1_En_8_Fig7_HTML.jpg

    图 8-7

    Displaying the input

    import { Component, AfterViewInit, ViewChild } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `
      <input #input type="text" (input)="textInput($event)" value=""/>
      <hr>
      Upper-Case: {{upperCase}}
      <br/>
      Lower-Case: {{lowerCase}}
    
      `,
      styles: []
    })
    export class AppComponent implements AfterViewInit{
      upperCase: string= '';
      lowerCase: string = '';
      @ViewChild('input') inputBox;
    
      textInput(event){
        this.upperCase = event.target.value.toUpperCase();
        this.lowerCase = event.target.value.toLowerCase();
      }
    
      ngAfterViewInit() {
        this.inputBox.nativeElement.focus()
      }
    }
    
    

你的应用应该工作在本地主机:4200。请注意以下几点:

  • 模板变量#inputviewChild用于获取对输入框的引用。在视图初始化之后(生命周期方法ngAfterViewInit被触发),焦点被设置到输入框。

  • 该模板使用下面的代码来监听input事件,当事件发生时触发方法textInput(传入事件对象):

    (input)="textInput($event)"
    
    
  • 该类有一个由input事件触发的方法textInput。它计算用户输入的大写和小写版本,并将其设置为从类绑定(单向)到模板的实例变量。

CDK

Angular CDK(组件开发套件)于 2017 年与 Angular 5 一起发布。其目的是使开发人员能够创建高质量的 Angular 定制组件。CDK 包含服务、指令、组件、类和模块。CDK 包含组件可访问性、文本方向性、平台检测和动态组件实例化的代码。如果你真的想开始构建自己的定制可重用组件库,那么你需要安装“@angular/cdk”节点模块并开始。

摘要

本章涵盖了一些重要的概念,所以我强烈建议在继续学习之前先学习这些例子。阅读完本章后,你应该对组件有一些基本的了解,包括它们是由什么组成的。您还知道组件由注释、模板和类组成。我们还讨论了事件处理。

既然我们可以编写用户界面组件,我们将在下一章把注意力转向模块化我们的 Angular 代码。如果您还没有完全理解组件,请不要担心——后面的章节将会详细介绍。

九、模块简介

模块这个词指的是独立的、可重用的代码的小单元。典型的模块是专用于单一目的的内聚代码块。模块在代码中导出一些有价值的东西,通常是一样东西,比如一个对象。

本章主要介绍不同模块的概念。它没有包括很多编码示例——您将在后面编写模块。

JavaScript 给了你做很多糟糕事情的自由——你没有义务写可重用的代码。你可以把你的代码散布在任何地方。随着 JavaScript 及其环境的成熟,这种情况必须改变。您需要通过隐藏对象的内部工作方式并让公共接口从外部可用来简化对象。您需要能够将代码打包成可重用的块,这些块可以彼此独立地打包和部署。你还需要能够按需加载它们,而不是在应用启动时(缓慢地)加载所有内容。

不同类型的模块

本章介绍 AngularJS、Angular 和 JavaScript 模块化代码的三种方式:

A458962_1_En_9_Fig1_HTML.gif

图 9-1

Angular applications are made up of Angular modules and JavaScript modules

  • Angular 的原始版本中包含的 AngularJS 模块系统:这使您能够在粗粒度级别模块化代码。
  • ES6 和 TypeScript 中现在可用的 JavaScript 模块:这些模块使您能够在细粒度级别模块化代码。记住,每个源代码文件都有一个模块。ts 或者。js)。
  • Angular 模块系统:这使您能够在粗粒度级别模块化代码。您可以将 Angular 代码单元捆绑到模块中。例如,如果你正在用 Angular 编写一个系统,其中包含一个销售应用、一个人力资源应用和一个税务应用,你可以将这三个应用分割成独立的功能模块和一个共享公共代码的共享模块,如图 9-1 所示。

AngularJS 模块系统

AngularJS 有自己的模块系统,非常简单。您有一个 Angular 模块,其中可能包含 Angular 控制器、指令等。

在图 9-2 中,我们声明了模块 xxx,它依赖于许多其他模块:ngCookies、ngRoute、ngResource、ngSanitize、angularSpinner、ui.bootstrap.demo、ui.bootstrap、ui.select、wj 和 angularModalService。在代码之后,我们将声明这个模块 xxx 中的项目。

A458962_1_En_9_Fig2_HTML.jpg

图 9-2

Declaring module xxx

JavaScript 模块

JavaScript 过去常常使用库,这些库对于开发人员在某个领域的开发非常有用。例如,JQuery 曾经帮助开发人员进行 UI 开发。这些库写得很好,但没有作为模块实现。相反,它们是作为 JavaScript 脚本实现的(如。js 脚本文件),它将创建 JavaScript 对象来做事情。这是在 JavaScript 模块系统存在之前。

现在 ES6 和更高版本支持模块。在 Javascript 模块中,每个文件都是一个模块。你可以自己编模块,也可以用别人的模块。您可以使用 Node 将依赖模块拉入您的项目中(进入 node_modules 文件夹)。

当我们在 TypeScript 中用 Angular 5 编码时,我们使用两个 JavaScript 模块关键字:

  • Export:导出模块代码
  • Import:导入模块代码

导出代码

您将应用编写为小模块的集合。您的代码使用export关键字将对象从模块导出到外部世界。例如,下面的代码用于告诉 TypeScript 您正在导出类App以便在其他地方使用:

export class App {...}

下面是如何从模块中导出默认对象:

module "foo" {
  export default function() { console.log("hello!") }
}

导入代码

语句告诉 TypeScript 从某处获取模块代码。某处可以来自其他人的模块,也可以来自同一项目中的本地代码。

从其他人的模块中导入代码

当您使用import语句从其他人的模块中获取代码时,您需要标识模块名和您想要导入的项的名称,在from之后指定模块名。这通常是从节点模块导入代码的方式。例如,从 Angular 导入元件:

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

以下是如何从 ngx-bootstrap 导入日期选择器:

import { DatepickerModule } from 'ngx-bootstrap/datepicker';

导入您的项目代码

当从同一项目中的本地代码导入代码时,需要指定该代码的相对路径。下面的例子指定了一个相对路径(./)。这告诉 TypeScript 该代码与将要使用该模块的代码在同一个文件夹中:

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

以下是一些更重要的语法:

  • 全部导入:

    import * as myModule from 'my-module';
    
    
  • 使用命名导入时,名称需要与模块中导出的对象名称完全匹配:

    import { myMember } from 'my-module';
    
    
  • 对于来自一个模块的多个命名导入,名称需要与模块中导出的对象名称完全匹配:

    import { foo, bar } from 'my-module';
    
    
  • 对于从模块的默认导入,名称不需要与模块中导出的任何对象匹配。它可以是一个别名。它知道它必须从模块

    import myDefault from 'my-module';
    
    

    导入默认对象

Angular 模块系统

Angular 模块系统是 Angular bundles 编码成可重用模块的方式。使用该模块系统,Angular 系统代码本身被模块化。许多第三方使用模块为 Angular 提供了额外的功能,您可以轻松地将这些功能包含到您的应用中。

Angular 为什么不直接用 JavaScript 模块?为什么强迫开发者使用自己的模块系统?首先,它确实使用了标准的 JavaScript 模块,但是还不够。它们使得 Angular 不容易声明由不同对象捆绑在一起组成的长块 Angular 代码——例如,组件、服务和管道。在 Angular 2 的早期,开发人员没有 Angular 模块的选项,开发人员使用模块加载器来加载和启动应用(System.js)。对我来说,它在实践中并不奏效。它很难学,很容易坏,而且太复杂了。

当 Angular2 处于测试阶段时,我参与了它的工作,并喜欢这个产品,但我讨厌它在模块加载方面的复杂性,因为我不得不学习 System.js 等。我后来回到它身边,发现您可以快速而简单地使用 Angular CLI 来构建一个使用 Webpack 进行部署的模块化应用。我欢迎 Angular 模块系统,并认为它与 CLI 和 Webpack 配合得很好。

开始项目中的模块

你已经用过角模块系统了,即使你没有意识到。如果您打开使用 CLI 创建的启动项目,您将看到已经有一个文件 app.module.ts。模块。让我们打开来看看:

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

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

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

让我们看看这里的一些元素:

  • @NgModule注解:这个注解是这个类最重要的部分。这是一个函数,它接受一个对象,并使用该对象向 Angular 提供关于模块的元数据:如何编译它以及如何运行它。所以,@NgModule是 Angular 的声明方式,让你告诉 Angular 如何把这些部分组合在一起。注意,@NgModule本身需要从顶部的@angular/core导入。
  • declarations:这应该是你的模块使用的 Angular 组件、指令和管道的数组,而不是其他任何东西——没有 ES6 类或其他任何东西。当您使用 CLI 命令ng generate component添加一个组件时,它会导入该组件并将其添加到这个声明列表中。如果您添加并使用一个组件,而没有在这里声明它,您将在浏览器控制台收到一条错误消息。
  • import:这应该是这里应用需要的 Angular 模块数组。这些模块必须使用@NgModule来定义。Angular 本身有许多有用的系统模块,默认情况下,CLI 为您提供了其中的几个模块,包括浏览器模块、表单模块和 http 模块。
  • providers:这应该是应用所需的 Angular 对象的数组provider。这些provider对象是服务类和值,它们通过依赖注入被注入到你的类中。如果您有一个公共服务对象供组件用来与服务器通信,那么您可以在这里将其添加为提供者。
  • 你可以使用模块来包含你的应用的代码。要运行,您的应用需要知道如何启动以及应该从哪个组件启动(根)。这是您指定根组件的地方,当应用启动时,它将被创建并装载到 HTML 中。这个根组件通常被称为 AppComponent。

根模块

您的 Angular 应用可以包含多个模块。但是它总是有一个起点,一个用来引导自己的模块。这是根模块,通常称为 AppModule。

路由模块

我们稍后将讨论路由,但路由对于 Angular 应用非常重要。它允许用户将组件映射到 URL 并导航用户界面。当我们使用 CLI 构建 Angular 应用时,它会为您的应用路由构建一个单独的模块,通常在 app-routing.ts 文件中。这看起来可能是多余的,但它非常巧妙地将 Angular 路由对象与您的应用的路由设置打包到一个模块中,该模块为您的应用处理所有路由。

功能模块

领域驱动设计(DDD)是一种软件开发方法,通过将实现与演进的模型相联系来解决复杂的需求。DDD 经常不得不处理非常大、复杂的业务需求建模,它的方法是将这些需求分解到不同的环境中。有界上下文是业务需求中可以逻辑分离的领域,如图 9-3 所示。

A458962_1_En_9_Fig3_HTML.jpg

图 9-3

Bounded contexts

如你所见,在图 9-3 中有两个上下文:销售和支持。每一个都可以是你 Angular 应用的一个独立部分。事实上,每一个都可以包含在它自己独立的模块中,称为特性模块。每个模块可以包含特定的代码,以满足其他地方不需要的特定需求。例如,销售模块可以包含一个 Angular UI 来管理销售渠道,这在其他任何地方都不会使用。因此,功能模块通常包含不打算在该模块之外使用的代码。

需要时,根模块可以包括所需数量的功能模块。例如,当用户点击销售菜单时,特征模块甚至可以按需加载。

共享模块

您可以将功能模块视为不共享的代码块。共享模块则相反——它们包含最常用的模块化代码,因此可以尽可能多地重用。当需要时,根模块可以包括所需数量的共享模块。

Angular 模块系统:示例模块-ex100

这个例子是一个关于如何一起使用根模块、特性模块和共享模块的非常基本的练习。

该示例的组件顶部有两个链接:销售和支持。您可以单击每个链接,在两个组件之间浏览应用。销售和支持这两个组件都是独立的功能模块。

这个例子有一个根模块,App,特性模块(已经提到过),以及一个来自共享模块的组件 shared,如图 9-4 所示。

A458962_1_En_9_Fig4_HTML.gif

图 9-4

Example of Angular module system

让我们看一下这个例子:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new modules-ex100
    
    
  2. 开始ng serve:使用以下代码:

    cd modules-ex100
    ng serve
    
    
  3. 打开应用:启动 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 生成模块:让我们使用 CLI 来生成附加模块:

    ng generate module shared
    ng generate module routing --routing
    ng generate module sales
    ng generate module support
    
    
  5. 生成组件:让我们使用 CLI 来生成附加组件:

    ng generate component sales
    ng generate component support
    ng generate component shared
    
    
  6. 编辑组件样式:编辑文件 sales.component.css 并将其更改为以下内容:

    div {
        background-color: #bdcebe;
        border: 1px solid #000000;
        padding: 10px;
        margin: 10px;
    }
    
    

    编辑文件 support.component.css 并将其更改为以下内容:

    div {
        background-color: #eca1a6;
        border: 1px solid #000000;
        padding: 10px;
        margin: 10px;
    }
    
    

    编辑文件 shared.component.css 并将其更改为以下内容:

    div {
        background-color: #d6cbd3;
        border: 1px solid #000000;
        padding: 10px;
        margin: 10px;
    }
    
    

    编辑文件 app.component.css 并将其更改为以下内容:

    div {
        background-color: #e3eaa7;
        border: 10px;
        padding: 10px;
    }
    
    
  7. 编辑组件模板:编辑文件 sales.component.html,更改为:

    <div>
      sales module!
      <app-shared></app-shared>
    </div>
    
    

    编辑文件 support.component.html,更改为:

    <div>
      support module!
      <app-shared></app-shared>
    </div>
    
    

    编辑文件 shared.component.html,更改为:

    <div>
      shared module!
    </div>
    
    

    编辑文件 app.component.html,更改为:

    <div style="text-align:center">
      <h1>
        Welcome!!
      </h1>
      <a [routerLink]="['sales']">Sales</a>
      <a [routerLink]="['support']">Support</a>
       <router-outlet></router-outlet>
    </div>
    
    
  8. 编辑路由模块:编辑 routing.module.ts 文件,并将其更改为:

    import { NgModule } from '@angular/core';
    import { CommonModule } from '@angular/common';
    import { Routes, RouterModule } from '@angular/router';
    import { SalesComponent } from '../sales/sales.component';
    import { SupportComponent } from '../support/support.component';
    
    const routes: Routes = [
      {
        path: 'sales',
        component: SalesComponent
      },
      {
        path: 'support',
        component: SupportComponent
      },
      {
        path: '**',
        component: SalesComponent
      }
    ];
    
    @NgModule({
      imports: [RouterModule.forRoot(routes)],
      exports: [RouterModule],
      providers: []
    })
    export class RoutingModule { }
    
    
  9. 编辑销售模块:编辑文件 sales.module.ts,并将其更改为:

    import { NgModule } from '@angular/core';
    import { CommonModule } from '@angular/common';
    import { SalesComponent } from './sales.component';
    
    @NgModule({
      imports: [
        CommonModule
      ],
      declarations: [SalesComponent]
    })
    export class SalesModule { }
    
    
  10. 编辑共享模块:编辑 shared.module.ts 文件,并将其更改为:

```ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedComponent } from './shared.component';

@NgModule({
  imports: [
    CommonModule
  ],
  exports: [
    SharedComponent
  ],
  declarations: [SharedComponent]
})
export class SharedModule { }

```

11. 编辑支持模块:编辑 support.module.ts 文件,并将其更改为:

```ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SupportComponent } from './support.component';

@NgModule({
  imports: [
    CommonModule
  ],
  declarations: [SupportComponent]
})
export class SupportModule { }

```

12. 编辑 App 模块:编辑 app.module.ts 文件,修改为:

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

import { AppComponent } from './app.component';
import { RoutingModule } from './routing/routing.module';
import { SalesComponent } from './sales/sales.component';
import { SupportComponent } from './support/support.component';
import { SharedModule } from './shared/shared.module';

@NgModule({
  declarations: [
    AppComponent,
    SalesComponent,
    SupportComponent
  ],
  imports: [
    BrowserModule,
    RoutingModule,
    SharedModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

```

你的应用应该工作在本地主机:4200。请注意以下几点:

  • 路由模块提供应用路由的代码。根应用导入这个模块,所有路由代码都准备好了,可以使用了。
  • 共享模块提供共享组件。根应用只需要导入这个模块就可以访问它的组件。
  • 销售和支持模块不必导入共享模块或共享组件,即使它在销售和支持组件中使用。
  • 销售和支持模块只导入 Angular 公共模块。这个公共模块与我们的代码无关。这是 Angular 提供基本 Angular 指令代码的方式,如NgIfNgFor等等。

部署:独立模块

从一个根模块访问特性模块、共享模块等等的能力听起来很棒,但是问题是您可能需要单独更新特性模块,特别是如果您有一个单独的团队在处理每个特性。例如,“销售”人员可能与“支持”人员有不同的发布日期。不幸的是,这个例子被部署在一组 Webpack 模块中。

如果您希望销售可以与支持分开部署,那么每一个都应该在自己的文件夹中有自己的单页应用。这使得部署工作变得更加轻松。

部署:使用节点管理对公共代码的依赖

部署的另一个问题是公共代码。销售人员可能需要与支持人员不同的通用代码版本。一个可能使用新的公共对象,另一个可能不使用。这是考虑使用 Node 来管理每个项目对公共代码的依赖的好时机。

您可以从 Angular 项目创建节点模块。记住,Angular 有通过节点模块部署的模块。这有点超出了本书的范围,但我自己已经做到了这一点,这要感谢下面这篇高超的文章:https://medium.com/@cyrilletuzi/how-to-build-and-publish-an-angular-module-7ad19c0b4464

要做到这一点,您需要从公共代码库中设置一些代码,比如 GitHub。这里有一个我放在 GitHub 上的非常简单的例子: https://github.com/markclowisg/sharedcomponents

有用的节点命令

当一起使用 Angular 和 Node 时,您可能还想考虑使用节点包管理器命令npm linknpm scope:

  • 当构建节点模块时,这非常有用。它允许您设置一个链接,这样依赖项目就可以使用您的节点代码,而不必不断地重新构建和重新部署到您的存储库中。在本地做所有事情并在以后复制到您的存储库中要容易得多。
  • 当你有几个 npm 公共代码项目,并且你想把它们分组在一个名字前缀下时,这是很有用的。Angular 用它的@angular npm 包前缀来做这件事。你可以考虑一下这个。如果您为“abc”公司工作,并且您有两个用于组件和服务的通用 npm 包,您可能想要使用范围,这样它们可以是@abc/components@abc/services

摘要

本章涵盖了与模块化相关的广泛主题。它向您介绍了模块化的概念以及如何在 JavaScript 和 Angular 中实现它。

如果你觉得这一章太难了,不要担心。您可以跳过“部署”部分,稍后再回到这个部分。模块化可能看起来很复杂,有点迟钝,但是它有价值,可以使你的项目更容易维护。

下一章将介绍 Webpack,Angular CLI 使用它将您的代码打包成可部署的文件。

十、Webpack 简介

如今,你可以在现代浏览器中做更多的事情,这在未来会增加更多。多亏了 Angular 5 这样的技术,页面重载会更少,每个页面会有更多的 JavaScript 代码,客户端会有很多代码。您需要一种方法来有效地部署所有这些代码,以便快速加载。

复杂的客户端应用可能包含模块,有些模块可以同步加载,有些可以异步加载。因此,我们如何打包并最有效地部署它–我们使用 Webpack!

Webpack 和 Angular CLI

Angular CLI 使用 Webpack 来传输、编译和部署项目代码。默认情况下,它还使用 webpack-dev-server 作为它的 web 服务器。在这一章的后面,我会谈到 Webpack 配置和 webpack.config.js。你会在你的项目中寻找它,并注意到它不见了。这是故意的,因为编写 Angular CLI 的人希望隐藏尽可能多的配置细节,以使事情变得更简单,这包括 Webpack 配置。

以下 Angular CLI 命令使 Webpack 配置文件可用:

ng eject

但是,使用这个命令时要小心,因为可能会有一些意想不到的副作用。参见 https://github.com/angular/angular-cli/wiki/ejecthttp://stackoverflow.com/questions/39187556/angular-cli-where-is-webpack-config-js-file-new-2017-feb-ng-eject 了解更多关于这个主题的信息。

Webpack 是一个模块捆绑器。它获取具有依赖关系的模块,并生成表示这些模块的静态资产。图 10-1 所示。

A458962_1_En_10_Fig1_HTML.jpg

图 10-1

Webpack generates static assets representing modules

模块和依赖关系

如果您使用 Node 进行开发,Webpack 将读取您的节点配置文件 packages.json,并自动将您的依赖项作为静态资产包含在构建中。这消除了配置模块加载和部署的痛苦——您不需要解决任何问题。我在参与的每个 Angular 5 项目中都使用了 Webpack,因为它让生活变得更简单。

Webpack 适用于大型项目,因为它支持开发和生产模式。开发模式可以利用像 JavaScript 这样的非最小化资产,使您的应用可以在这种模式下进行调试。生产模式可以使用最少的资产,因此占用空间更小。

您的代码库可以分成多个块,这些块可以按需加载,从而减少应用的初始加载时间。结果:更快的装载时间。作为一名开发人员,您还可以控制这些块的配置(稍后将详细介绍)。

开发过程是这样的:

  1. 编写项目代码。
  2. 将 Webpack 作为构建过程的一部分运行(或者通过 CLI 为您运行)。
  3. 构建完成后,您的静态资产就可以部署到服务器上了。

安装和配置 Webpack

如果您正在使用 CLI,则不需要安装 Webpack。Webpack 在节点下运行。但是如果您想单独试验 Webpack,您可以使用下面的命令来安装它(从项目的根文件夹中):

npm install webpack -g

如果您运行前面提到的ng eject命令,您的 Webpack 选项将包含在项目根文件夹的 webpack.config.js 文件中。在该文件中,您会发现以下内容:

  • 输出路径:您可以指定捆绑资产的放置位置——输出路径。

  • 入口点:你的应用可以在不同的地方使用不同的代码启动。Webpack 将打包用于部署的代码,以便它可以从这些不同的代码开始,但共享公共的打包块。

  • Loaders: A loader is a Node function that takes a type of file and converts files of this type into a new source for bundling (see Figure 10-2). Loaders are Node packages used by Webpack.

    A458962_1_En_10_Fig2_HTML.jpg

    图 10-2

    Loaders

  • Plugins: I use the CommonsChunk plugin in the book’s example project to split our code into deployable chunks that can be loaded separately. The CommonsChunk plugin checks which chunks of code (modules) you use the most and puts them in a file. This gives you a common file that has the CSS and JavaScript needed by every page in your application.

    A458962_1_En_10_Figa_HTML.jpg

图 10-3 中的代码用于创建以下内容:

  • app.js
  • app.map
  • common.js
  • common.map
  • 供应商. js
  • 供应商.地图

摘要

这个简短的章节向您介绍了 Webpack 的基础知识。Webpack 为开发人员提供了惊人的控制能力,我们可以花大量的时间来研究它的可配置性。

但是我们需要继续学习 Angular。第十一章介绍了另一个非常重要的元素:指令。

十一、指令简介

指令是 DOM 元素(如属性)上的标记,它告诉 Angular 将指定的行为附加到现有元素上。

自安古拉吉斯以来,指令就一直存在。它们使用起来相当复杂,尽管在 Angular 中使用起来要容易得多,尤其是在将数据传递到指令中的时候。指令曾经是在 AngularJS 应用中创建自定义标签的主要方式;现在它已经被指令和组件取代了。

Angular 本身提供了许多指令来帮助你编码。你也可以自己编码。

正如第八章所述,组件有三个主要元素:

  • 注释为 Angular 提供了元数据,以便将所有部分组合成一个组件。
  • 模板包含用于在浏览器中呈现组件的标记(通常是 HTML)。
  • 该类包含组件的数据和代码。代码实现组件的预期行为。

如您所见,模板用于为组件的显示生成标记。该标记可以包括用于其他 Angular 组件的标签(或其他选择器),从而允许从其他组件合成组件。这种标记还可以包括实现某些行为的指令。

例如,您可能有一个显示升级请求的升级详细信息的组件。但是,想要查看升级请求的人可能没有查看信息的权限,这意味着某些元素应该隐藏。您可以使用 Angular ngIf指令来评估用户的权限,并基于这些权限隐藏或显示元素。

指令的类型

现在我们知道组件模板使用指令,但是它们可能以不同的方式影响模板的输出。有些指令可能会完全改变模板输出的结构。这些指令可以通过添加和删除视图 DOM 元素来更改 DOM 布局。让我们称这些为结构指令。有些指令可能只是改变模板输出的项目的外观。让我们称这些为非结构化指令。

Angular 包括几个可供您在模板中使用的结构指令:

  • NgIf
  • NgFor
  • NgSwitchNgSwitchWhenNgSwitchDefault

Angular 还包括几个在模板中使用的非结构化指令:

  • NgClass
  • NgStyle
  • NgControlName
  • NgModel

ngIf

这是您添加到标记中的元素的指令,通常是添加到类似于div的容器元素。如果ngIf的模板表达式为真,那么在绑定完成后,元素内部的内容将包含在视图 DOM 中。如果ngIf的模板表达式为 false,那么在绑定完成后,元素内部的内容将从视图 DOM 中排除。因此,ngIf指令用于包含或排除 UI 的元素,包括元素的子元素。被 ngIf 排除的标记不会是不可见的,它只是根本不在 DOM 中。

在本例中(指令-ex100),我们在显示姓名和地址之间切换,如图 11-1 所示。

A458962_1_En_11_Fig1_HTML.jpg

图 11-1

Toggling between name and address

让我们使用ngIf来隐藏和显示元素:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new directives-ex100
    
    
  2. 开始ng serve:使用以下代码:

    cd directives-ex100
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts,修改为:

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styles: ['div.box { width: 200px;padding:20px;margin:20px;border:1px solid black;color:white;background-color:green }']
    })
    export class AppComponent {
      showName: boolean = true;
    
      toggle(){
        this.showName = !this.showName;
      }
    }
    
    
  5. 编辑模板:编辑 app.component.html,更改如下:

    <div *ngIf="this.showName" class="box">
      Name: Mark
    </div>
    <div *ngIf="!this.showName" class="box">
      Address: Atlanta
    </div>
    <button (click)="this.toggle()">Toggle</button>
    
    

为了什么

这是一个处理 iterable 对象的每一项的指令,为每一项输出一个标记。这被称为结构化指令,因为它可以通过添加和删除视图 DOM 元素来更改 DOM 布局。

ndFor对于生成重复的内容很有用,比如客户列表、下拉列表的元素等等。

iterable 的每个处理项在其模板上下文中都有可用的变量,如表 11-1 所示。

表 11-1

ngFor Variables

| 可变的 | 描述 | | :-- | :-- | | 项目本身 | 例子:`ngFor="#name of names"`。在这种情况下,项目具有变量`name`。 | | `Index` | 每个模板上下文的当前循环迭代。 | | `last` | 布尔值,指示该项是否是迭代中的最后一项。 | | `even` | 指示此项是否有偶数索引的布尔值。 | | `odd` | 指示此项是否有奇数索引的布尔值。 |

这将是示例指令-ex200,如图 11-2 所示。

A458962_1_En_11_Fig2_HTML.jpg

图 11-2

ngFor showing a list

让我们使用ngFor来显示一个列表:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new directives-ex200
    
    
  2. 开始ng serve :

    cd directives-ex200
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts,修改为:

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent {
      names = [
        'Peter Falk', 'Mary-Ann Blige', 'Eminem'];
    }
    
    
  5. 编辑模板:编辑 app.component.html,更改如下:

    <div *ngFor="let name of names; let i = index;">
      <div>{{i}}:&nbsp;{{name}}</div>
    </div>
    
    

ngSwitch、ngSwitchWhen 和 ngSwitchDefault

ngSwitch是当 DOM 元素匹配switch表达式时添加或删除 DOM 元素的指令。它被称为结构化指令,因为它可以通过添加和删除视图 DOM 元素来更改 DOM 布局。

这将是示例指令-ex300,如图 11-3 所示。

A458962_1_En_11_Fig3_HTML.jpg

图 11-3

ngSwitch hiding and showing elements

让我们根据您的选择使用ngSwitch来隐藏和显示元素:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new directives-ex300
    
    
  2. 开始ng serve:使用以下代码:

    cd directives-ex300
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts,修改为:

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styles: ['.block1 {background-color:#d5f4e6;margin:10px;padding:10px;}',
      '.block2 {background-color:#d5f4ff;margin:10px;padding:10px;}',
      '.block3 {background-color:#d5cce6;margin:10px;padding:10px;}']
    })
    export class AppComponent {
      selection = 'name';
      options = ['name','address','other'];
    }
    
    
  5. 编辑模板:编辑 app.component.html,更改如下:

    <select [(ngModel)]="selection">
      <option *ngFor="let option of options">{{option}}</option>
    </select>
    <div [ngSwitch]="selection">
      <div class="block1" *ngSwitchCase="options[0]">name</div>
      <div class="block2" *ngSwitchCase="options[1]">address</div>
      <div class="block3" *ngSwitchDefault>other</div>
    </div>
    
    
  6. 编辑模块:编辑 app.module.ts,修改为:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { FormsModule } from '@angular/forms'
    import { AppComponent } from './app.component';
    
    @NgModule({
      declarations: [
        AppComponent
      ],
      imports: [
        BrowserModule, FormsModule
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    

ngClass

我们可以通过使用这个指令添加或删除类来改变 DOM 元素的外观。它的参数是一个对象,该对象包含下列对象对:

  • CSS 类名
  • 一种表达

如果表达式为真,CSS 类名将被添加到目标 DOM 元素中,否则将被忽略。它不仅对设置 CSS 类有用。使用如下代码可能更容易:

<div [class]="classNames">Customer {{name}}.</div>

在下一个例子中,n gClass让用户单击动物列表中的动物来选择它。选定的动物以红色突出显示。这将是示例指令-ex400,如图 11-4 所示。

A458962_1_En_11_Fig4_HTML.jpg

图 11-4

ngClass highlighting in a list

让我们来做指令示例-ex400:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new directives-ex400
    
    
  2. 开始ng serve:使用以下代码:

    cd directives-ex400
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts,修改为:

    import { Component } from '@angular/core';
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styles: [
        '.selected { color: white; background-color:red; padding: 10px; margin: 10px }',
        '.unselected { background-color: white; padding: 10px; margin: 10px}'
        ]
    })
    export class AppComponent {
      selectedAnimal = 'cat';
      animals = ['cat', 'dog', 'zebra', 'giraffe'];
    
      onAnimalClicked(event:Event){
        const clickedAnimal = event.srcElement.innerHTML.trim();
        this.selectedAnimal = clickedAnimal;
      }
    }
    
    
  5. 编辑模板:编辑 app.component.html,更改如下:

    <div *ngFor="let animal of animals">
      <div [ngClass]="{'selected': animal === selectedAnimal, 'unselected' : animal !== selectedAnimal}"
      (click)="onAnimalClicked($event)">{{animal}}</div>
    </div>
    
    

你的应用应该工作在本地主机:4200。

ngStyle

这是用于设置元素的 CSS 样式的指令。如果您只想设置一种样式,使用如下代码可能更容易:

<div [style.fontSize]="selected ? 'x-large' : 'smaller'" >
  Some text.
</div>

但是如果你想设置多种风格,ngStyle是正确的选择。此指令需要一个计算结果为包含样式属性的对象的表达式。该表达式可以是如下所示的内联代码:

[ngStyle]="{'color': 'blue', 'font-size': '24px', 'font-weight': 'bold'}"

或者像这样的函数调用:

[ngStyle]="setStyles(animal)"

... later on in the class ...

setStyles(animal:String){
    let styles = {
      'width' : '50px'
    }
    return styles;
  }

它让用户点击动物列表中的动物来选择它。选中的动物以红色高亮显示,如图 11-5 所示。

A458962_1_En_11_Fig5_HTML.jpg

图 11-5

ngStyle highlighting in a list

让我们做指令示例-ex500:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new directives-ex500
    
    
  2. 开始ng serve:使用以下代码:

    cd directives-ex500
    ng serve
    
    
  3. 打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”

  4. 编辑类:编辑 app.component.ts,修改为:

    import { Component } from '@angular/core';
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html'
    })
    export class AppComponent {
      selectedAnimal = 'cat';
      animals = ['cat', 'dog', 'zebra', 'giraffe'];
    
      onAnimalClicked(event:Event){
        const clickedAnimal = event.srcElement.innerHTML.trim();
        this.selectedAnimal = clickedAnimal;
      }
    
      getAnimalStyle(animal){
        const isSelected = (animal === this.selectedAnimal);
        return {
          'padding' : '10px',
          'margin' : '10px',
          'color' : isSelected ? '#ffffff' : '#000000',
          'background-color' : isSelected ? '#ff0000' : '#ffffff',
        }
      }
    }
    
    
  5. 编辑模板:编辑 app.component.html,更改如下:

    <div *ngFor="let animal of animals">
      <div [ngStyle]="getAnimalStyle(animal)" (click)="onAnimalClicked($event)">{{animal}}</div>
    </div>
    
    

你的应用应该工作在本地主机:4200。

Note

Angular 还使用其他指令来处理表单。我将在后面的章节中介绍这些内容。

创建指令

指令和组件都是有 Angular 的对象,对应于标记中的元素,可以修改生成的用户界面。它们都有选择器。选择器用于标识与网页或模板中的标记相关联的组件或指令。对于组件,通常使用标签名——例如CustomerList。对于指令,通常使用一个标记属性名,它使用方括号——例如,[tooltip]

指令和组件都有注释。指令有@Directive注释,组件有@Component注释。它们都有类,这些类可以通过构造函数以相同的方式使用依赖注入。

然而,整流罩和组件并不完全相同。例如,组件需要视图,而指令不需要。指令没有模板。没有用于呈现元素的捆绑 HTML 标记。

指令向现有 DOM 元素添加行为。例如,您可以为工具提示添加指令。您创建了指令,将指令选择器添加到 HTML 或使用它的模板中,它就交付了功能(您还需要添加导入)。

创建指令类似于创建组件:

  1. 导入Directive装饰器。
  2. 添加@Directive注释,包括一个 CSS 属性选择器(在方括号中),它将指令标识为一个属性。您还可以向@Directive注释添加其他元素,包括输入属性和主机映射。
  3. 指定用于绑定的公共输入属性的名称(如果需要)。
  4. Directive类。这个类将使用构造函数注入,并可能操作注入的元素和渲染器。
  5. 将装饰器应用于将要使用它的组件或指令。

如前所述,指令是 DOM 元素(如属性)上的标记,它告诉 Angular 将指定的行为附加到现有元素上。这意味着我们需要一种方法来访问指令所应用到的 DOM 元素,以及修改 DOM 元素的方法。

Angular 为我们提供了两个非常有用的对象:ElementRefRenderer

  • 通过nativeElement属性,ElementRef对象让您可以直接访问指令的 DOM 元素。小心使用ElementRef对象。允许直接访问 DOM 会使您的应用更容易受到 XSS 攻击。
  • Renderer对象给了我们许多帮助方法,使我们能够修改 DOM 元素。

我们可以把两者都注入到我们的课堂中。下面的代码通过构造函数接受ElementRef(允许您使用 DOM 元素的nativeElement属性访问 DOM 元素)和Renderer,并使每个变量成为私有实例变量:

constructor(private element: ElementRef, private renderer: Renderer) {
}

创建简单指令:示例指令-ex600

这是一个简单的指令,用于更改它所添加到的 HTML 元素的大小:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new directives-ex600
    
    
  2. 导航到目录;使用以下代码:

    cd directives-ex600
    
    
  3. 使用 CLI 创建指令:使用 CLI 创建文件并修改模块 app.module.ts:

    ng generate directive sizer
    
    

    这将生成一些文件,包括 sizer.directive.ts。

  4. 编辑 sizer.directive.ts:将其改为:

    import { Directive, Input, Component, ElementRef, Renderer, |OnInit } from '@angular/core';
    
    @Directive({
      selector: '[sizer]'
    })
    export class SizerDirective implements OnInit {
      @Input() sizer : string;
    
      constructor(private element: ElementRef, private renderer: Renderer) {
      }
    
      ngOnInit() {
        this.renderer.setElementStyle(this.element.nativeElement, 'font-size', this.sizer);
      }
    }
    
    

    注意该指令如何在初始化后触发的ngOnInit方法中工作。如果您要将setElementStyle代码移动到构造函数中,这是可行的,因为sizer输入变量没有立即设置它的值——它是在 app 组件初始化时设置的。

  5. 编辑模板:编辑 app.component.html,更改如下:

    <div sizer="72px">
      {{title}}
    </div>
    
    
  6. 查看应用:打开 web 浏览器并导航到 localhost:4200。它应该显示“应用工程”在大文本。

你的应用应该工作在本地主机:4200。请注意如何使用渲染器来更新样式和更改大小。

在指令中访问 DOM 事件

我们可能还需要一种方法来访问链接到指令的元素的 DOM 事件。Angular 提供了不同的方法来访问这些事件。

使用指令元素宿主

这可用于指定与host元素相关的事件、动作、属性和特性。它可用于将事件绑定到类中的代码:

@Directive({
  selector: 'input',
  host: {
    '(change)': 'onChange($event)',
    '(window:resize)': 'onResize($event)'
  }
})
class InputDirective {
  onChange(event:Event) {
    // invoked when the input element fires the 'change' event
  }
  onResize(event:Event) {
    // invoked when the window fires the 'resize' event

  }
}

主机监听器

Angular HostListener是允许您将类中的方法绑定到 DOM 事件的注释:

@HostListener('mouseenter') onMouseEnter() {
  this.highlight('yellow');
}

@HostListener('mouseleave') onMouseLeave() {
  this.highlight(null);
}

private highlight(color: string) {
  this.el.nativeElement.style.backgroundColor = color;
}

在指令中访问 DOM 属性

您可能希望修改链接到指令的元素的属性。您可以使用元素ref来做到这一点。然而,还有另一种方法。您可以使用@HostBinding指令将元素的 DOM 属性绑定到 Angular 指令中的实例变量。然后您可以更新变量的值,DOM 属性将自动更新以匹配。

例如,在下面的代码中,您可以通过修改backgroundColor实例变量的值来控制元素的背景颜色:

@Directive({
    selector: '[myHighlight]',
})
class MyDirective {
  @HostBinding('style.background-color') backgroundColor:string = 'yellow';
}

创建带有事件的指令:示例指令-ex700

这是一个处理宿主事件的示例指令。主机事件映射到host元素中的 DOM 事件。当您需要一个指令来响应 DOM 上发生的事情时,它们很有用:

  1. 使用 CLI 构建应用:使用以下命令:

    ng new directives-ex700
    
    
  2. 导航到目录:使用此命令:

    cd directives-ex700
    
    
  3. 使用 CLI 创建指令:使用 CLI 创建文件并修改模块 app.module.ts:

    ng generate directive hoverer
    
    

    这将生成一些文件,包括 hoverer.directive.ts。

  4. 编辑 hoverer.directive.ts:改为如下:

    import { Directive, Input, ElementRef, Renderer } from '@angular/core';
    
    @Directive({
      selector: '[hoverer]',
      host: {
        '(mouseenter)': 'onMouseEnter()',
        '(mouseleave)': 'onMouseLeave()'
      }
    })
    
    export class HovererDirective {
      @Input() hoverer;
    
      constructor(
        private elementRef:ElementRef,
        private renderer:Renderer) { }
    
      onMouseEnter(){
        this.renderer.setElementStyle(
          this.elementRef.nativeElement, 'color', this.hoverer);
      }
    
      onMouseLeave(){
        this.renderer.setElementStyle(
          this.elementRef.nativeElement, 'color', 'black');
      }
    }
    
    
  5. 编辑模板:编辑 app.component.html,更改如下:

    <h1 hoverer="red">{{title}}</h1>
    
    
  6. 查看应用:打开 web 浏览器并导航到 localhost:4200。当你悬停在“欢迎使用应用!”上时,它应该会变成红色

你的应用应该工作在本地主机:4200。

摘要

学习完这一章后,你应该知道如何编写指令,并理解它们与组件的不同之处。

指令在被重用来向用户界面添加通用行为时非常有用。它们通常被放在共享模块中,以便可以跨应用重用。例如,您可以编写一个指令,根据用户的设置(或其他状态)在整个应用中启用或禁用按钮。该指令可以由元素属性指定。您可以将该指令添加到共享模块(或主模块)中,然后修改应用的模板以在按钮上包含该指令的属性。

我们暂时完成了指令。下一章回到组件,更详细地看它们。