Angular学习笔记(二)

以下内容基于Angular 文档中文版的学习

目录

生命周期钩子

视图封装(CSS影响范围)

组件之间的交互

  通过输入型绑定把数据从父组件传到子组件

  父组件监听子组件的事件

  父组件与子组件通过本地变量互动

  父级调用 @ViewChild()

   父组件和子组件通过服务来通讯

组件样式

内容投影

  单插槽内容投影

  多插槽内容投影

  有条件的内容投影

动态组件

Angular 元素

  使用动态加载组件的方式

  使用自定义元素的方式

指令

  内置指令

    内置属性型指令

    内置结构型指令

  自定义属性型指令

  自定义结构性指令

  指令组合 API

    向组件/指令添加指令

    指令的执行顺序

表单

  响应式表单

  模板驱动表单

  创建响应式表单

  验证表单输入

    在模板驱动表单中验证输入

    在响应式表单中验证输入

    为模板驱动表单中添加自定义验证器

    表示控件状态的 CSS 类

    跨字段交叉验证

    创建异步验证器


生命周期钩子

  ngOnChanges()
    用途:当 Angular 设置或重新设置数据绑定的输入属性时响应。该方法接受当前和上一属性值的 SimpleChanges 对象
          注意:这发生得比较频繁,所以你在这里执行的任何操作都会显著影响性能。
    时机:如果组件绑定过输入属性,那么在 ngOnInit() 之前以及所绑定的一个或多个输入属性的值发生变化时都会调用。
          注意:如果你的组件没有输入属性,或者你使用它时没有提供任何输入属性,那么框架就不会调用 ngOnChanges()。
  ngOnInit()
    用途:在 Angular 第一次显示数据绑定和设置指令/组件的输入属性之后,初始化指令/组件。
    时机:在第一轮 ngOnChanges() 完成之后调用,只调用一次。而且即使没有调用过 ngOnChanges(),也仍然会调用 ngOnInit()(比如当模板中没有绑定任何输入属性时)。
  ngDoCheck()
    用途:检测,并在发生 Angular 无法或不愿意自己检测的变化时作出反应。
    时机:紧跟在每次执行变更检测时的 ngOnChanges() 和 首次执行变更检测时的 ngOnInit() 后调用。
  ngAfterContentInit()
    用途:当 Angular 把外部内容投影进组件视图或指令所在的视图之后调用。
    时机:第一次 ngDoCheck() 之后调用,只调用一次。
  ngAfterContentChecked()
    用途:每当 Angular 检查完被投影到组件或指令中的内容之后调用。
    时机:ngAfterContentInit() 和每次 ngDoCheck() 之后调用。
  ngAfterViewInit()
    用途:当 Angular 初始化完组件视图及其子视图或包含该指令的视图之后调用。
    时机:第一次 ngAfterContentChecked() 之后调用,只调用一次。
  ngAfterViewChecked()
    用途:每当 Angular 做完组件视图和子视图或包含该指令的视图的变更检测之后调用。
    时机:ngAfterViewInit() 和每次 ngAfterContentChecked() 之后调用。
  ngOnDestroy()
    用途:Angular 每次销毁指令/组件之前调用并清扫。取消订阅可观察对象和 DOM 事件/停止 interval 计时器/反注册该指令在全局或应用服务中注册过的所有回调,以防内存泄漏。
    时机:在 Angular 销毁指令或组件之前立即调用。

  第一次:ngOnChanges -> ngOnInit -> ngDoCheck -> ngAfterContentInit -> ngAfterContentChecked -> ngAfterViewInit -> ngAfterViewChecked
  数据变更:ngOnChanges -> ngDoCheck -> ngAfterContentChecked -> ngAfterViewChecked

  AfterView 钩子所关心的是 ViewChildren,这些子组件的元素标签会出现在该组件的模板里面
  AfterContent 钩子所关心的是 ContentChildren,这些子组件被 Angular 投影进该组件中
  ngAfterViewInit/ngAfterViewChecked函数中如果要更新显示相关的属性值,必须延后到下一个节拍执行,不然会报错。
  ngAfterContentInit/ngAfterContentChecked函数中如果要更新显示相关的属性值,可以直接更新。
 

视图封装(CSS影响范围)

  Component 的装饰器提供了 encapsulation 选项,可用来控制如何基于每个组件应用视图封装。
  ViewEncapsulation.ShadowDom
    Angular 使用浏览器内置的 Shadow DOM API 将组件的视图包含在 ShadowRoot(用作组件的宿主元素)中,并以隔离的方式应用所提供的样式。
    ViewEncapsulation.ShadowDom 仅适用于内置支持 shadow DOM 的浏览器。并非所有浏览器都支持它,这就是为什么 ViewEncapsulation.Emulated 是推荐和默认模式的原因。
    组件样式仅添加到 shadow DOM 宿主中,确保它们仅影响各自组件视图中的元素。
  ViewEncapsulation.Emulated
    Angular 会修改组件的 CSS 选择器,使它们只应用于组件的视图,不影响应用程序中的其他元素(模拟 Shadow DOM 行为)。
    组件的样式会添加到文档的 <head> 中,使它们在整个应用程序中可用,但它们的选择器只会影响它们各自组件模板中的元素。
  ViewEncapsulation.None
    Angular 不应用任何形式的视图封装,这意味着为组件指定的任何样式实际上都是全局应用的,并且可以影响应用程序中存在的任何 HTML 元素。这种模式本质上与将样式包含在 HTML 本身中是一样的。
    组件的样式会添加到文档的 <head> 中,使它们在整个应用程序中可用,因此是完全全局的,会影响文档中的任何匹配元素。

  ViewEncapsulation.Emulated 和 ViewEncapsulation.None 组件的样式也会添加到每个 ViewEncapsulation.ShadowDom 组件的 shadow DOM 宿主中。

组件之间的交互

  通过输入型绑定把数据从父组件传到子组件

    子: 

	    @Input() hero!: Hero;
	    @Input('master') masterName = '';
		// 通过 setter 截听输入属性值的变化
		  @Input()
          get name(): string { return this._name; }
          set name(name: string) {
            this._name = (name && name.trim()) || '<no name set>';
          }
          private _name = '';
		// 通过 ngOnChanges() 来截听输入属性值的变化
          ngOnChanges(changes: SimpleChanges) {
            const log: string[] = [];
            for (const propName in changes) {
              const changedProp = changes[propName];
              const to = JSON.stringify(changedProp.currentValue);
              if (changedProp.isFirstChange()) {
                log.push(`Initial value of ${propName} set to ${to}`);
              } else {
                const from = JSON.stringify(changedProp.previousValue);
                log.push(`${propName} changed from ${from} to ${to}`);
              }
            }
            this.changeLog.push(log.join(', '));
          }

    父: 

<app-name-child [name]="name"></app-name-child>

  父组件监听子组件的事件

    子: 

@Output() voted = new EventEmitter<boolean>();
this.voted.emit(agreed);

    父: 

<app-voter [name]="voter" (voted)="onVoted($event)"></app-voter>
onVoted(agreed: boolean) {
}

  父组件与子组件通过本地变量互动

    父: 

        <button type="button" (click)="timer.start()">Start</button>
        <button type="button" (click)="timer.stop()">Stop</button>
        <div class="seconds">{{timer.seconds}}</div>
        <app-countdown-timer #timer></app-countdown-timer>

  父级调用 @ViewChild()

    父: 

        @ViewChild(CountdownTimerComponent)
        private timerComponent!: CountdownTimerComponent;
        seconds() { return 0; }

        ngAfterViewInit() {
          // Redefine `seconds()` to get from the `CountdownTimerComponent.seconds` ...
          // but wait a tick first to avoid one-time devMode
          // unidirectional-data-flow-violation error
          setTimeout(() => this.seconds = () => this.timerComponent.seconds, 0);
        }

        start() { this.timerComponent.start(); }
        stop() { this.timerComponent.stop(); }

        ngAfterViewInit() 生命周期钩子是非常重要的一步。被注入的计时器组件只有在 Angular 显示了父组件视图之后才能访问,所以它先把秒数显示为 0。
        然后 Angular 会调用 ngAfterViewInit 生命周期钩子,但这时候再更新父组件视图的倒计时就已经太晚了。
        Angular 的单向数据流规则会阻止在同一个周期内更新父组件视图。应用在显示秒数之前会被迫再等一轮。
        使用 setTimeout() 来等下一轮,然后改写 seconds() 方法,这样它接下来就会从注入的这个计时器组件里获取秒数的值。
 

   父组件和子组件通过服务来通讯

     服务: 

           // Observable string sources
           private missionAnnouncedSource = new Subject<string>();
           private missionConfirmedSource = new Subject<string>();

           // Observable string streams
           missionAnnounced$ = this.missionAnnouncedSource.asObservable();
           missionConfirmed$ = this.missionConfirmedSource.asObservable();

           // Service message commands
           announceMission(mission: string) {
             this.missionAnnouncedSource.next(mission);
           }

           confirmMission(astronaut: string) {
             this.missionConfirmedSource.next(astronaut);
           }

      组件: 

            constructor(private missionService: MissionService) {
              missionService.missionConfirmed$.subscribe(
                astronaut => {
                  this.history.push(`${astronaut} confirmed the mission`);
                });
            }
            announce() {
              const mission = this.missions[this.nextMission++];
              this.missionService.announceMission(mission);
              this.history.push(`Mission "${mission}" announced`);
              if (this.nextMission >= this.missions.length) { this.nextMission = 0; }
            }

      组件2:

            constructor(private missionService: MissionService) {
              this.subscription = missionService.missionAnnounced$.subscribe(
                mission => {
                  this.mission = mission;
                  this.announced = true;
                  this.confirmed = false;
              });
            }
            confirm() {
              this.confirmed = true;
              this.missionService.confirmMission(this.astronaut);
            }

组件样式

  默认情况下,组件的CSS只影响组件本身,不会影响到下级组件。
  有几种方式把样式加入组件:
    设置 styles 或 styleUrls 元数据
    内联在模板的 HTML 中
    通过 CSS 文件导入
  :host 每个组件都会关联一个与其组件选择器相匹配的元素。这个元素称为宿主元素,模板会渲染到其中。:host 伪类选择器可用于创建针对宿主元素自身的样式,而不是针对宿主内部的那些元素。
        下面的样式将以组件的宿主元素为目标。应用于此选择器的任何规则都将影响宿主元素及其所有后代(在这种情况下,将所有包含的文本斜体)
          :host {
            font-style: italic;
          }
  :host-context 有时候,需要以某些来自宿主的祖先元素为条件来决定是否要应用某些样式。比如,在文档的 <body> 元素上可能有一个用于表示样式主题 (theme) 的 CSS 类,你应当基于它来决定组件的样式。
        这时可以使用 :host-context() 伪类选择器。它也以类似 :host() 形式使用。它在当前组件宿主元素的祖先节点中查找 CSS 类,直到文档的根节点为止。它只能与其它选择器组合使用。
        在下面的例子中,只有当该组件的某个祖先元素有 CSS 类 active 时,才会把该组件内部的所有文本置为斜体。
          :host-context(.active) {
            font-style: italic;
          }
        注意:只有宿主元素及其各级子节点会受到影响,不包括加上 active 类的这个节点的祖先。

  ::ng-deep 把伪类 ::ng-deep 应用到任何一条 CSS 规则上就会完全禁止对那条规则的视图封装。任何带有 ::ng-deep 的样式都会变成全局样式。
        为了把指定的样式限定在当前组件及其下级组件中,请确保在 ::ng-deep 之前带上 :host 选择器。使用场景:使用了第三方组件,又想修改第三方组件里元素的样式时, 或者是项目里的通用组件,想在某个使用它的组件里单独修改它的样式,而不影响别的组件时
        如果 ::ng-deep 组合器在 :host 伪类之外使用,该样式就会污染其它组件。
 

内容投影

  内容投影是一种模式,你可以在其中插入或投影要在另一个组件中使用的内容。
  父组件引用子组件时在标签内部放置内容,子组件把对应的内容显示出来


  单插槽内容投影

    父:<app-child>Content To Child:{{contentToChild}}<app-another></app-another></app-child>
    子:使用<ng-content></ng-content>显示Content To Child:{{contentToChild}}<app-another></app-another>的内容
 

  多插槽内容投影

    一个组件可以具有多个插槽。每个插槽可以指定一个 CSS 选择器,该选择器会决定将哪些内容放入该插槽。该模式称为多插槽内容投影。
    使用此模式,你必须指定希望投影内容出现在的位置。
    将 select 属性添加到 <ng-content> 元素上。Angular 使用的选择器支持标签名、属性、CSS 类和 :not 伪类的任意组合。
    父:

        <app-zippy-multislot>
          <p question>
            Is content projection cool?
          </p>
          <p>Let's learn about content projection!</p>
        </app-zippy-multislot>

    子:

        Default:<ng-content></ng-content>
        Question:<ng-content select="[question]"></ng-content>

    使用 question 属性的内容将投影到带有 select=[question] 属性的 <ng-content> 元素。
    如果你的组件包含不带 select 属性的 <ng-content> 元素,则该实例将接收所有与其他 <ng-content> 元素都不匹配的投影组件。
 

  有条件的内容投影

    如果你的组件需要有条件地渲染内容或多次渲染内容,则应配置该组件以接受一个 <ng-template> 元素,其中包含要有条件渲染的内容。
    在这种情况下,不建议使用 <ng-content> 元素,因为只要组件的使用者提供了内容,即使该组件从未定义 <ng-content> 元素或该 <ng-content> 元素位于 ngIf 语句的内部,该内容也总会被初始化。
    使用 <ng-template> 元素,你可以让组件根据你想要的任何条件显式渲染内容,并可以进行多次渲染。在显式渲染 <ng-template> 元素之前,Angular 不会初始化该元素的内容。
    组件可以使用 @ContentChild 或 @ContentChildren 装饰器获得对此模板内容的引用(即 TemplateRef)。
    <ng-container> 元素是一个逻辑结构,可用于对其他 DOM 元素进行分组;但是,ng-container 本身不会在 DOM 树中渲染。
    父:

        <app-example-zippy>
          <button type="button" appExampleZippyToggle>Is content project cool?</button>
          <ng-template appExampleZippyContent>
            It depends on what you do with it.
          </ng-template>
        </app-example-zippy>

    子:

        <ng-content></ng-content>
        <div *ngIf="expanded" [id]="contentId">
          <ng-container [ngTemplateOutlet]="content.templateRef"></ng-container>
        </div>
        @Directive({
          selector: 'button[appExampleZippyToggle]',
        })
        export class ZippyToggleDirective {
          @HostBinding('attr.aria-expanded') ariaExpanded = this.zippy.expanded;
          @HostBinding('attr.aria-controls') ariaControls = this.zippy.contentId;
          @HostListener('click') toggleZippy() {
            this.zippy.expanded = !this.zippy.expanded;
          }
          constructor(public zippy: ZippyComponent) {}
        }

        let nextId = 0;
        @Directive({
          selector: '[appExampleZippyContent]'
        })
        export class ZippyContentDirective {
          constructor(public templateRef: TemplateRef<unknown>) {}
        }

        @Component({
          selector: 'app-example-zippy',
          templateUrl: 'example-zippy.template.html',
        })
        export class ZippyComponent {
          contentId = `zippy-${nextId++}`;
          @Input() expanded = false;
          @ContentChild(ZippyContentDirective) content!: ZippyContentDirective;
        }

动态组件

  指令
    在添加组件之前,先要定义一个锚点来告诉 Angular 要把组件插入到什么地方。
    广告条使用一个名叫 AdDirective 的辅助指令来在模板中标记出有效的插入点。
    AdDirective 注入了 ViewContainerRef 来获取对容器视图的访问权,这个容器就是那些动态加入的组件的宿主。
  加载组件

    <ng-template adHost></ng-template>

  解析组件

    @ViewChild(AdDirective, {static: true}) adHost!: AdDirective;
    const viewContainerRef = this.adHost.viewContainerRef;
    viewContainerRef.clear();
    const componentRef = viewContainerRef.createComponent<AdComponent>(adItem.component);
	componentRef.instance.data = adItem.data;

Angular 元素

  Angular 提供了 createCustomElement() 函数,以支持把 Angular 组件及其依赖转换成自定义元素。
  该函数会收集该组件的 Observable 型属性,提供浏览器创建和销毁实例时所需的 Angular 功能,还会对变更进行检测并做出响应。
  这个转换过程实现了 NgElementConstructor 接口,并创建了一个构造器类,用于生成该组件的一个自举型实例。
  使用内置的 customElements.define()函数把这个配置好的构造器和相关的自定义元素标签注册到浏览器的CustomElementRegistry中。 当浏览器遇到这个已注册元素的标签时,就会使用该构造器来创建一个自定义元素的实例。

  使用动态加载组件的方式

    // Previous dynamic-loading method required you to set up infrastructure
    // before adding the popup to the DOM.
    showAsComponent(message: string) {
      // Create element
      const popup = document.createElement('popup-component');

      // Create the component and wire it up with the element
      const factory = this.componentFactoryResolver.resolveComponentFactory(PopupComponent);
      const popupComponentRef = factory.create(this.injector, [], popup);

      // Attach to the view so that the change detector knows to run
      this.applicationRef.attachView(popupComponentRef.hostView);

      // Listen to the close event
      popupComponentRef.instance.closed.subscribe(() => {
        document.body.removeChild(popup);
        this.applicationRef.detachView(popupComponentRef.hostView);
      });

      // Set the message
      popupComponentRef.instance.message = message;

      // Add to the DOM
      document.body.appendChild(popup);
    }

  使用自定义元素的方式

    constructor(injector: Injector, public popup: PopupService) {
      // Convert `PopupComponent` to a custom element.
      const PopupElement = createCustomElement(PopupComponent, {injector});
      // Register the custom element with the browser.
      customElements.define('popup-element', PopupElement);
    }
    // This uses the new custom-element method to add the popup to the DOM.
    showAsElement(message: string) {
      // Create element
      const popupEl: NgElement & WithProperties<PopupComponent> = document.createElement('popup-element') as any;

      // Listen to the close event
      popupEl.addEventListener('closed', () => document.body.removeChild(popupEl));

      // Set the message
      popupEl.message = message;

      // Add to the DOM
      document.body.appendChild(popupEl);
    }

指令

  内置指令

    内置属性型指令

      NgClass 添加和删除一组 CSS 类
      NgStyle 添加和删除一组 HTML 样式
      NgModel 将双向数据绑定添加到 HTML 表单元素

	    <input [(ngModel)]="currentItem.name" id="example-ngModel">
	    <input [ngModel]="currentItem.name" (ngModelChange)="setUppercaseName($event)" id="example-uppercase">

      NgNonBindable 停用 Angular 处理过程

        要防止在浏览器中进行表达式求值,请将 ngNonBindable 添加到宿主元素。ngNonBindable 会停用模板中的插值、指令和绑定。
        将 ngNonBindable 应用于元素将停止对该元素的子元素的绑定。但是,ngNonBindable 仍然允许指令在应用 ngNonBindable 的元素上工作。

		<div ngNonBindable [appHighlight]="'yellow'">This should not evaluate: {{ 1 +1 }}, but will highlight yellow.</div>

    内置结构型指令

      NgIf 有条件地从模板创建或销毁子视图
      NgFor 为列表中的每个条目重复渲染一个节点

	    <div *ngFor="let item of items; let i=index">{{i + 1}} - {{item.name}}</div>

        通过跟踪对条目列表的更改,可以减少应用程序对服务器的调用次数。使用 *ngFor 的 trackBy 属性,Angular 能只更改和重新渲染已更改的条目,而不必重新加载整个条目列表。

		  trackByItems(index: number, item: Item): number { return item.id; }
		  <div *ngFor="let item of items; trackBy: trackByItems">
            ({{item.id}}) {{item.name}}
		  </div>

        Angular 的 <ng-container> 是一个分组元素,它不会干扰样式或布局,因为 Angular 不会将其放置在 DOM 中。当没有单个元素承载指令时,可以使用 <ng-container>。

		  <select [(ngModel)]="hero">
            <ng-container *ngFor="let h of heroes">
              <ng-container *ngIf="showSad || h.emotion !== 'sad'">
                <option [ngValue]="h">{{h.name}} ({{h.emotion}})</option>
              </ng-container>
            </ng-container>
          </select>

      NgSwitch 一组在备用视图之间切换的指令

        <div [ngSwitch]="currentItem.feature">
          <app-stout-item    *ngSwitchCase="'stout'"    [item]="currentItem"></app-stout-item>
          <app-device-item   *ngSwitchCase="'slim'"     [item]="currentItem"></app-device-item>
          <app-lost-item     *ngSwitchCase="'vintage'"  [item]="currentItem"></app-lost-item>
          <app-best-item     *ngSwitchCase="'bright'"   [item]="currentItem"></app-best-item>
          <!-- . . . -->
          <app-unknown-item  *ngSwitchDefault           [item]="currentItem"></app-unknown-item>
        </div>

  自定义属性型指令

    从 @angular/core 导入 ElementRef。ElementRef 的 nativeElement 属性会提供对宿主 DOM 元素的直接访问权限。
    添加两个事件处理程序,它们会在鼠标进入或离开时做出响应,每个事件处理程序都带有 @HostListener() 装饰器

    @Directive({
      selector: '[appHighlight]'
    })
    export class HighlightDirective {

      constructor(private el: ElementRef) { }

      @Input() defaultColor = '';

      @Input() appHighlight = '';

      @HostListener('mouseenter') onMouseEnter() {
        this.highlight(this.appHighlight || this.defaultColor || 'red');
      }

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

      private highlight(color: string) {
        this.el.nativeElement.style.backgroundColor = color;
      }
    }
    <p [appHighlight]="color" defaultColor="violet">Highlight me too!</p>

  自定义结构性指令

    @Directive({ selector: '[appUnless]'})
    export class UnlessDirective {
      private hasView = false;

      constructor(
        private templateRef: TemplateRef<any>,
        private viewContainer: ViewContainerRef) { }

      @Input() set appUnless(condition: boolean) {
        if (!condition && !this.hasView) {
          this.viewContainer.createEmbeddedView(this.templateRef);
          this.hasView = true;
        } else if (condition && this.hasView) {
          this.viewContainer.clear();
          this.hasView = false;
        }
      }
    }
    <p *appUnless="condition" class="unless a">
      (A) This paragraph is displayed because the condition is false.
    </p>

  指令组合 API

    向组件/指令添加指令

      你可以通过将 hostDirectives 属性添加到组件的装饰器来将指令应用于组件。我们称这样的指令为宿主指令。
      当框架渲染组件时,Angular 还会创建每个宿主指令的实例。指令的宿主绑定被应用于组件的宿主元素。默认情况下,宿主指令的输入和输出不会作为组件公共 API 的一部分公开。
      Angular 会在编译时静态应用宿主指令。你不能在运行时动态添加指令。
      hostDirectives 中使用的指令必须是 standalone: true 的。
      Angular 会忽略 hostDirectives 属性中所应用的那些指令的 selector 。
      你可以为 hostDirective 的输入和输出起别名来自定义组件的 API

      @Component({
        selector: 'admin-menu',
        template: 'admin-menu.html',
        hostDirectives: [{
          directive: MenuBehavior,
          inputs: ['menuId: id'],
          outputs: ['menuClosed: closed'],
        }],
      })
      export class AdminMenu { }

      <admin-menu id="top-menu" (closed)="logMenuClosed()">

    指令的执行顺序

       宿主指令和直接在模板中使用的组件和指令会经历相同的生命周期。但是,宿主指令总是会在应用它们的组件或指令之前执行它们的构造函数、生命周期钩子和绑定。
         MenuBehavior 实例化
         AdminMenu 实例化
         MenuBehavior 接收输入( ngOnInit )
         AdminMenu 接收输入 ( ngOnInit )
         MenuBehavior 应用宿主绑定
         AdminMenu 应用宿主绑定

表单

  响应式表单

    提供对底层表单对象模型直接、显式的访问。它们与模板驱动表单相比,更加健壮:它们的可扩展性、可复用性和可测试性都更高。
    如果表单是你的应用程序的关键部分,或者你已经在使用响应式表单来构建应用,那就使用响应式表单。
    数据模型的可变性
      通过以不可变的数据结构提供数据模型,来保持数据模型的纯粹性。
      每当在数据模型上触发更改时,FormControl 实例都会返回一个新的数据模型,而不会更新现有的数据模型。
      这使你能够通过该控件的可观察对象跟踪对数据模型的唯一更改。这让变更检测更有效率,因为它只需在唯一性更改(译注:也就是对象引用发生变化)时进行更新。
      由于数据更新遵循响应式模式,因此你可以把它和可观察对象的各种运算符集成起来以转换数据。
    使用[formControl][formControlName]访问输出输入

  模板驱动表单

    依赖模板中的指令来创建和操作底层的对象模型。
    它们对于向应用添加一个简单的表单非常有用,比如电子邮件列表注册表单。它们很容易添加到应用中,但在扩展性方面不如响应式表单。
    如果你有可以只在模板中管理的非常基本的表单需求和逻辑,那么模板驱动表单就很合适。
    数据模型的可变性
      依赖于可变性和双向数据绑定,可以在模板中做出更改时更新组件中的数据模型。
      由于使用双向数据绑定时没有用来对数据模型进行跟踪的唯一性更改,因此变更检测在需要确定何时更新时效率较低。
    使用[(ngModel)]访问输出输入

  创建响应式表单

    // 使用new:
      profileForm = new FormGroup({
        firstName: new FormControl(''),
        lastName: new FormControl(''),
        address: new FormGroup({
          street: new FormControl(''),
          city: new FormControl(''),
          state: new FormControl(''),
          zip: new FormControl('')
        })
      });
	// 使用FormBuilder
	  constructor(private fb: FormBuilder) {}
      profileForm = this.fb.group({
        firstName: ['', Validators.required],
        lastName: [''],
        address: this.fb.group({
          street: [''],
          city: [''],
          state: [''],
          zip: ['']
        }),
        aliases: this.fb.array([
          this.fb.control('')
        ])
      });

    显示

      <form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
        <label for="first-name">First Name: </label>
        <input id="first-name" type="text" formControlName="firstName" required>

        <label for="last-name">Last Name: </label>
        <input id="last-name" type="text" formControlName="lastName">

        <div formGroupName="address">
          <h2>Address</h2>

          <label for="street">Street: </label>
          <input id="street" type="text" formControlName="street">

          <label for="city">City: </label>
          <input id="city" type="text" formControlName="city">

          <label for="state">State: </label>
          <input id="state" type="text" formControlName="state">

          <label for="zip">Zip Code: </label>
          <input id="zip"type="text" formControlName="zip">
        </div>

        <div formArrayName="aliases">
          <h2>Aliases</h2>
          <button type="button" (click)="addAlias()">+ Add another alias</button>

          <div *ngFor="let alias of aliases.controls; let i=index">
            <!-- The repeated alias template -->
            <label for="alias-{{ i }}">Alias:</label>
            <input id="alias-{{ i }}" type="text" [formControlName]="i">
          </div>
        </div>


        <p>Complete the form to enable button.</p>
        <button type="submit" [disabled]="!profileForm.valid">Submit</button>
      </form>

    动态添加FormArray子控件

	  this.aliases.push(this.fb.control(''));

    取值赋值

	  this.profileForm.value
      this.profileForm.value.firstName
      this.profileForm.setValue(obj)
	    // 使用 setValue() 方法来为单个控件设置新值。setValue() 方法会严格遵循表单组的结构,并整体性替换控件的值。
		// 严格检查可以帮助你捕获复杂表单嵌套中的错误
      this.profileForm.patchValue(obj)
        // 用此对象中定义的任意属性对表单模型进行替换。只会更新表单模型中所定义的那些属性。

  验证表单输入

    在模板驱动表单中验证输入

      <input type="text" id="name" name="name" class="form-control"
            required minlength="4" appForbiddenName="bob"
            [(ngModel)]="hero.name" #name="ngModel">
      <div *ngIf="name.invalid && (name.dirty || name.touched)" class="alert">
        <div *ngIf="name.errors?.['required']">Name is required.</div>
        <div *ngIf="name.errors?.['minlength']">Name must be at least 4 characters long.</div>
        <div *ngIf="name.errors?.['forbiddenName']">Name cannot be Bob.</div>
      </div>

      <input> 元素带有一些 HTML 验证属性:required 和 minlength。它还带有一个自定义的验证器指令 forbiddenName
      #name="ngModel" 把 NgModel 导出成了一个名叫 name 的局部变量。NgModel 把自己控制的 FormControl 实例的属性映射出去,让你能在模板中检查控件的状态,比如 valid 和 dirty

    在响应式表单中验证输入

      验证器(Validator)函数
        同步验证器
          这些同步函数接受一个控件实例,然后返回一组验证错误或 null。可以在实例化一个 FormControl 时把它作为构造函数的第二个参数传进去。
        异步验证器
          这些异步函数接受一个控件实例并返回一个 Promise 或 Observable,它稍后会发出一组验证错误或 null。在实例化 FormControl 时,可以把它们作为第三个参数传入。
        出于性能方面的考虑,只有在所有同步验证器都通过之后,Angular 才会运行异步验证器。当每一个异步验证器都执行完之后,才会设置这些验证错误。
      内置验证器函数
        Validators.min(min: number): ValidatorFn
          此验证器要求控件的值大于或等于指定的数字。 它只有函数形式,没有指令形式。
          如果验证失败,则此验证器函数返回一个带有 min 属性的映射表(map),否则为 null。
        Validators.max(max: number): ValidatorFn
          此验证器要求控件的值小于等于指定的数字。 它只有函数形式,没有指令形式。
          如果验证失败,则此验证器函数返回一个带有 max 属性的映射表(map),否则为 null。
        Validators.required(control: AbstractControl<any, any>): ValidationErrors | null
          此验证器要求控件具有非空值。
          如果验证失败,则此验证器函数返回一个带有 required 属性的映射表(map),否则为 null。
        Validators.requiredTrue(control: AbstractControl<any, any>): ValidationErrors | null
          此验证器要求控件的值为真。它通常用来验证检查框。
          如果验证失败,则此验证器函数返回一个带有 required 属性、值为 true 的映射表(map),否则为 null。
        Validators.email(control: AbstractControl<any, any>): ValidationErrors | null
          此验证器要求控件的值能通过 email 格式验证。
          如果验证失败,则此验证器函数返回一个带有 `email` 属性的映射表(map),否则为 `null`。
        Validators.minLength(minLength: number): ValidatorFn
          此验证器要求控件值的长度大于等于所指定的最小长度。当使用 HTML5 的 minlength 属性时,此验证器也会生效。
          如果验证失败,则此验证器函数返回一个带有 minlength 属性的映射表(map),否则为 null。
        Validators.maxLength(maxLength: number): ValidatorFn
          此验证器要求控件值的长度小于等于所指定的最大长度。当使用 HTML5 的 maxlength 属性时,此验证器也会生效。
          如果验证失败,则此验证器函数返回一个带有 maxlength 属性的映射表(map),否则为 null。
        Validators.pattern(pattern: string | RegExp): ValidatorFn
          此验证器要求控件的值匹配某个正则表达式。当使用 HTML5 的 pattern 属性时,它也会生效。
          如果验证失败,则此验证器函数返回一个带有 pattern 属性的映射表(map),否则为 null。
        Validators.nullValidator(control: AbstractControl<any, any>): ValidationErrors | null
          此验证器什么也不做。
        Validators.compose(validators: null): null
          把多个验证器合并成一个函数,它会返回指定控件的各个错误映射表的并集。
          如果验证失败,则此验证器函数返回各个验证器所返回错误对象的一个并集,否则为 null。
        Validators.composeAsync(validators: AsyncValidatorFn[]): AsyncValidatorFn | null
          把多个异步验证器合并成一个函数,它会返回指定控件的各个错误映射表的并集。
          如果验证失败,则此验证器函数返回各异步验证器所返回错误对象的一个并集,否则为 null。

      示例

        ngOnInit(): void {
          this.heroForm = new FormGroup({
            name: new FormControl(this.hero.name, [
              Validators.required,
              Validators.minLength(4),
              forbiddenNameValidator(/bob/i) // <-- Here's how you pass in the custom validator.
            ]),
            alterEgo: new FormControl(this.hero.alterEgo),
            power: new FormControl(this.hero.power, Validators.required)
          });

        }
        get name() { return this.heroForm.get('name'); }
        get power() { return this.heroForm.get('power'); }

		/** 返回自定义验证函数 */
        export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
          return (control: AbstractControl): ValidationErrors | null => {
            const forbidden = nameRe.test(control.value);
            return forbidden ? {forbiddenName: {value: control.value}} : null;
          };
        }
        <input type="text" id="name" class="form-control"
              formControlName="name" required>
        <div *ngIf="name.invalid && (name.dirty || name.touched)" class="alert alert-danger">
		  <!-- 这里的name是组件类中定义的name的getter -->
          <div *ngIf="name.errors?.['required']">Name is required.</div>
          <div *ngIf="name.errors?.['minlength']">Name must be at least 4 characters long.</div>
          <div *ngIf="name.errors?.['forbiddenName']">Name cannot be Bob.</div>
        </div>

    为模板驱动表单中添加自定义验证器

      @Directive({
        selector: '[appForbiddenName]',
        providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenValidatorDirective, multi: true}]
      })
      export class ForbiddenValidatorDirective implements Validator {
        @Input('appForbiddenName') forbiddenName = '';

        validate(control: AbstractControl): ValidationErrors | null {
          return this.forbiddenName ? forbiddenNameValidator(new RegExp(this.forbiddenName, 'i'))(control)
                                    : null;
        }
      }
      <input type="text" id="name" name="name" class="form-control"
        required minlength="4" appForbiddenName="bob"
        [(ngModel)]="hero.name" #name="ngModel">

      注意,自定义验证指令是用 useExisting 而不是 useClass 来实例化的。注册的验证程序必须是 ForbiddenValidatorDirective 实例本身 - 表单中的实例,也就是表单中 forbiddenName 属性被绑定到了"bob"的那个。
      如果用 useClass 来代替 useExisting,就会注册一个新的类实例,而它是没有 forbiddenName 的。

    表示控件状态的 CSS 类

      Angular 会自动把很多控件属性作为 CSS 类映射到控件所在的元素上。你可以使用这些类来根据表单状态给表单控件元素添加样式。目前支持下列类:
        .ng-valid           有效
        .ng-invalid        无效
        .ng-pending      异步验证中
        .ng-pristine       初始状态
        .ng-dirty            已修改
        .ng-untouched  未接触
        .ng-touched      已接触
        .ng-submitted (只对 form 元素添加)

    跨字段交叉验证

      跨字段交叉验证器是一种自定义验证器,可以对表单中不同字段的值进行比较,并针对它们的组合进行接受或拒绝。
      在它们共同的祖先控件中执行验证

      /** 返回自定义验证函数 */
      export const identityRevealedValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
        const name = control.get('name');
        const alterEgo = control.get('alterEgo');
        return name && alterEgo && name.value === alterEgo.value ? { identityRevealed: true } : null;
      };
      const heroForm = new FormGroup({
        'name': new FormControl(),
        'alterEgo': new FormControl(),
        'power': new FormControl()
      }, { validators: identityRevealedValidator });

      <div *ngIf="heroForm.errors?.['identityRevealed'] && (heroForm.touched || heroForm.dirty)" class="cross-validation-error-message alert alert-danger">
        Name cannot match alter ego.
      </div>

      为模板驱动表单添加交叉验证

      @Directive({
        selector: '[appIdentityRevealed]',
        providers: [{ provide: NG_VALIDATORS, useExisting: IdentityRevealedValidatorDirective, multi: true }]
      })
      export class IdentityRevealedValidatorDirective implements Validator {
        validate(control: AbstractControl): ValidationErrors | null {
          return identityRevealedValidator(control);
        }
      }

	  <form #heroForm="ngForm" appIdentityRevealed>
      <div *ngIf="heroForm.errors?.['identityRevealed'] && (heroForm.touched || heroForm.dirty)" class="cross-validation-error-message alert">
          Name cannot match alter ego.
      </div>

    创建异步验证器

      异步验证器实现了 AsyncValidatorFn 和 AsyncValidator 接口。它们与其同步版本非常相似,但有以下不同之处。
        validate() 函数必须返回一个 Promise 或Observable对象,
        返回的Observable对象必须是有尽的,这意味着它必须在某个时刻完成(complete)。要把无尽的Observable对象转换成有尽的,可以在管道中加入过滤操作符,比如 first、last、take 或 takeUntil。
 

      异步验证在同步验证完成后才会发生,并且只有在同步验证成功时才会执行。
      如果更基本的验证方法已经发现了无效输入,那么这种检查顺序就可以让表单避免使用昂贵的异步验证流程(比如 HTTP 请求)

      异步验证开始之后,表单控件就会进入 pending 状态。可以检查控件的 pending 属性,并用它来给出对验证中的视觉反馈。

	    <input [(ngModel)]="name" #model="ngModel" appSomeAsyncValidator>
        <app-spinner *ngIf="model.pending"></app-spinner>
      @Injectable({ providedIn: 'root' })
      export class UniqueAlterEgoValidator implements AsyncValidator {
        constructor(private heroesService: HeroesService) {}

        validate(
          control: AbstractControl
        ): Observable<ValidationErrors | null> {
          return this.heroesService.isAlterEgoTaken(control.value).pipe(
            map(isTaken => (isTaken ? { uniqueAlterEgo: true } : null)),
            catchError(() => of(null))
          );
        }
      }

      interface HeroesService {
        isAlterEgoTaken: (alterEgo: string) => Observable<boolean>;
      }

      constructor(private alterEgoValidator: UniqueAlterEgoValidator) {}
      const alterEgoControl = new FormControl('', {
        asyncValidators: [this.alterEgoValidator.validate.bind(this.alterEgoValidator)],
        updateOn: 'blur'
      });

      将异步验证器添加到模板驱动表单

	  @Directive({
        selector: '[appUniqueAlterEgo]',
        providers: [
          {
            provide: NG_ASYNC_VALIDATORS,
            useExisting: forwardRef(() => UniqueAlterEgoValidatorDirective),
            multi: true
          }
        ]
      })
      export class UniqueAlterEgoValidatorDirective implements AsyncValidator {
        constructor(private validator: UniqueAlterEgoValidator) {}

        validate(
          control: AbstractControl
        ): Observable<ValidationErrors | null> {
          return this.validator.validate(control);
        }
      }
      <input type="text"
               id="alterEgo"
               name="alterEgo"
               #alterEgo="ngModel"
               [(ngModel)]="hero.alterEgo"
               [ngModelOptions]="{ updateOn: 'blur' }"
               appUniqueAlterEgo>

    与原生 HTML 表单验证器交互
      默认情况下,Angular 通过在 <form> 元素上添加 novalidate 属性来禁用原生 HTML 表单验证,并使用指令将这些属性与框架中的验证器函数相匹配。
      如果你想将原生验证与基于 Angular 的验证结合使用,你可以使用 ngNativeValidate 指令来重新启用它。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值