【angular】@Component

Angular元素就是打包成自定义元素的 Angular 组件。所谓自定义元素就是一套与具体框架无关的用于定义新 HTML 元素的 Web 标准。@angular/elements 包导出了一个 createCustomElement() API,它在 Angular 组件接口与变更检测功能和内置 DOM API 之间建立了一个桥梁。

Angular 组件:ComponentDirective的子类:它是一个装饰器,用于把某个类标记为 Angular 组件,并为它配置一些元数据,以决定该组件在运行期间该如何处理、实例化和使用。

选项
moduleId :包含该组件的那个模块的 ID。该组件必须能解析模板和样式表中使用的相对 URLSystemJS 在每个模块中都导出了 __moduleName 变量。在 CommonJS 中,它可以设置为 module.id
template :Angular 组件的内联模板 templateUrlAngular 组件模板文件的 URL。两者二选一即可。
styleUrls :一个或多个 URL,指向包含本组件 CSS 样式表的文件。
styles :本组件用到的一个或多个内联 CSS 样式。
animations :一个或多个动画 trigger()调用,包含一些 state()transition()定义。 参见动画和相关的 API 文档。
interpolation :改写默认的插值表达式起止分界符({{}}
entryComponents :这个NgModule中也有,在Component中用的应该不多
preserveWhitespaces :为 true 则保留,为 false 则从编译后的模板中移除可能多余的空白字符。 空白字符就是指那些能在 JavaScript 正则表达式中匹配 \s 的字符。默认为 false

changeDetection:用于当前组件的变更检测策略。该策略是下列值之一:


encapsulation :供模板和 CSS 样式使用的样式封装策略。取值为:


viewProviders :定义一组可注入对象,它们在视图的各个子节点中可用。参见例子。重点看下这里:

template: `<needs-greeter></needs-greeter>`
class Greeter {
   greet(name:string) {
     return 'Hello ' + name + '!';
   }
}

@Directive({
  selector: 'needs-greeter'
})
class NeedsGreeter {
  greeter:Greeter;

  constructor(greeter:Greeter) {
    this.greeter = greeter;
  }
}

@Component({
  selector: 'greet',
  viewProviders: [
    Greeter
  ],
  template: `<needs-greeter></needs-greeter>`
})
class HelloWorld {
}

继承自 Directive 装饰器

选项
selector :这个 CSS选择器用于在模板中标记出该指令,并触发该指令的实例化。
inputs :列举某个指令的一组可供数据绑定的输入属性; outputs :列举一组可供事件绑定的输出属性。(这俩属性一般不用了,用对应的装饰器替代
providers :一组依赖注入令牌,它允许 DI 系统为这个指令或组件提供依赖。这个在NgModule一节已有较详细介绍
exportAs :定义一个名字,用于在模板中把该指令赋值给一个变量。
queries已有对应的属性装饰器,后面具体介绍
host :使用一组键-值对,把类的属性映射到宿主元素的绑定(PropertyAttribute事件)。也已有对应的属性装饰器,后面具体介绍
jit :如果为 true,则该指令/组件将会被 AOT 编译器忽略,始终使用 JIT 编译。

几种元数据--属性装饰器:组件之间的交互 详解

A. 组件的输入 inputs & @Input

angular允许使用两种形式来定义组件的输入,一种是在装饰器@Component中使用inputs来定义,另一种是使用@Input来定义。

首先先介绍在装饰器中使用的输入。inputs接收的是一个字符串数组,用来指定我们输入的键名。

@Component({
    selector: 'my-component',
    inputs: ['name']
})
class MyComponent {
    name: string;
}

name就会对应我们组件中的name变量。

然后我们定义一个组件,当然不可避免有的时候会在其他的组件的模板中使用,所以就可以这样写。

上级组件:

export class AppComponent {
    myName = 'zhangsan';
    ...
}

上级组件的模板:

<app-messages [name]="myName"></app-messages>

方括号[]:数据绑定,也叫输入绑定。将等号右边的变量绑定在左边[]中的变量上。

我们的组件:

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

export class MessagesComponent implements OnInit {
    name: string;
}

这里我们就用name接受了上级组件的myName

通过上面的图,就很容易看输入数据的对应关系。

然后我们打印一下看看变量是否成功输入了。

export class MessagesComponent implements OnInit {
    name: string;
    ngOnInit() {
      console.log(this.name);
    }
}

成功输入!

@Input:上面我们实现了组件的数据输入,但是angular并没有满足现状,还提供另外一种输入的方法,就是@Input

@Component({
    selector: 'my-component'
})

class MyComponent {
    @Input() name: string;
}

只要在我们的组件中定义变量的时候使用@Input装饰器就行了。对比上面我们使用inputs时,少了一个二次声明。这种方法感觉数据的传递少了一层关系,更加易于理解,而且代码也更加的工整。

通过 setter 截听输入属性值的变化

子组件NameChildComponent 的输入属性 name 上的这个 setter,会 trim 掉名字里的空格,并把空值替换成默认字符串。

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

@Component({
  selector: 'app-name-child',
  template: '<h3>"{{name}}"</h3>'
})
export class NameChildComponent {
  private _name = '';

  @Input()
  set name(name: string) {
    this._name = (name && name.trim()) || '<no name set>';
  }

  get name(): string { return this._name; }
}

NameParentComponent

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

@Component({
  selector: 'app-name-parent',
  template: `
  <h2>Master controls {{names.length}} names</h2>
  <app-name-child *ngFor="let name of names" [name]="name">
</app-name-child>
  `
})
export class NameParentComponent {
  // Displays 'Dr IQ', '<no name set>', 'Bombasto'
  names = ['Dr IQ', '   ', '  Bombasto  '];
}
效果图

通过ngOnChanges()来截听输入属性值的变化(类似vuewatch?)

当需要监视多个、交互式输入属性的时候,本方法比用属性的 setter 更合适,看下面案例:

VersionChildComponent组件 会监测输入属性 [major][minor] 的变化,并把这些变化编写成日志以报告这些变化。

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

@Component({
  selector: 'app-version-child',
  template: `
    <h3>Version {{major}}.{{minor}}</h3>
    <h4>Change log:</h4>
    <ul>
      <li *ngFor="let change of changeLog">{{change}}</li>
    </ul>
  `
})
export class VersionChildComponent implements OnChanges {
  @Input() major: number;
  @Input() minor: number;
  changeLog: string[] = [];

  ngOnChanges(changes: {[propKey: string]: SimpleChange}) {
    let log: string[] = [];
    for (let propName in changes) {
      let changedProp = changes[propName];
      let to = JSON.stringify(changedProp.currentValue);
      if (changedProp.isFirstChange()) {
        log.push(`Initial value of ${propName} set to ${to}`);
      } else {
        let from = JSON.stringify(changedProp.previousValue);
        log.push(`${propName} changed from ${from} to ${to}`);
      }
    }
    this.changeLog.push(log.join(', '));
  }
}

VersionParentComponent 提供 [minor][major] 值,把修改它们值的方法绑定到按钮上。

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

@Component({
  selector: 'app-version-parent',
  template: `
    <h2>Source code version</h2>
    <button (click)="newMinor()">New minor version</button>
    <button (click)="newMajor()">New major version</button>
    <app-version-child [major]="major" [minor]="minor"></app-version-child>
  `
})
export class VersionParentComponent {
  major = 1;
  minor = 23;

  newMinor() {
    this.minor++;
  }

  newMajor() {
    this.major++;
    this.minor = 0;
  }
}
image

B. 组件输出outputs & @Output

说完了组件的输入,下面我们就该聊聊组件的输出了。要将数据从组件中传递出去,就要使用输出绑定

<button (click)="display()"></button>

圆括号(): 事件绑定,又叫输出绑定。这里我们监听click事件,然后触发display方法。

除了clickangular还有很多内置的事件,当然,我们在编写自己的组件的时候,也可以自定义一个事件,来与外部通信。

自定义事件,需要做三件事情:

  • 1.在@Component配置中,制定outputs配置项
  • 2.在配置的属性中,设置一个EventEmitter(事件触发器)
  • 3.在适当的时候,也就是要触发的方法中,通过EventEmitter触发事件

下面看一下示例:

@Component({
    selector: 'my-component',
    outputs: ['newEvent']
})

export class MyComponent {
    newEvent: EventEmitter<string>;
    constructor() {
        this.newEvent = new EventEmitter();
    }
    display(): void {
        this.newEvent.emit("test event");
    }
}

然后我们就可以通过上面模板中的代码实现输出了。

如果想在一个父级的组件中使用这个输出,就要使用我们自己的事件了。下面看一个示例:

父级组件:

export class AppComponent {
    ...
    showEvent(message: string) {
        console.log(hello: ${message});
    }
}

父级模板:

<app-messages (newEvent)="showEvent($event)">
</app-messages>

我们的组件:

@Component({
    selector: 'app-messages',
    outputs: ['newEvent'],
    templateUrl: './messages.component.html'
})
export class MessagesComponent {
    newEvent: EventEmitter<string>;
    constructor(private messageService: MessageService) {
        this.newEvent = new EventEmitter();
    }
    display(): void {
        this.newEvent.emit('test event');
    }
}

我们的组件模板:

<button (click)="display()">触发</button>

然后点击触发,可以看到输出hello:test event。数据输出成功!

好了我们再来梳理整个输出过程:

1.我们自定以一个组件,通过内置的click事件触发display方法,这时就会触发我们自定义的事件:newEvent

2.当事件触发的时候,他会执行上一级的方法:showEvent

3.我们的事件输出了一个字符串test event,然后通过$event获取这个输出结果,并当做参数传给上一级的方法showEvent

@Output:同输入相同,angular也为我们提供了输出的第二种方式:@Output。用法与@input类似

export class MessagesComponent {
    @Output() newEvent: EventEmitter<string>;
}

C. host

@HostBinding()@HostListener()在自定义指令时非常有用。@HostBinding()可以为指令的宿主元素添加类、样式、属性等,而@HostListener()可以监听宿主元素上的事件。

@Component({
  selector: 'demo-component',
  host: {
    '(click)': 'onClick($event.target)', // 事件
    'role': 'nav', // 属性
    '[class.pressed]': 'isPressed', // 类
  }
})
export class DemoComponent {
  isPressed: boolean = true;
 
  onClick(elem: HTMLElement) {
    console.log(elem);
  }
}

等价于@HostBinding@HostListener

@Component({
  selector: 'demo-component'
})
export class DemoComponent {
  @HostBinding('attr.role') role = 'nav';
  @HostBinding('class.pressed') isPressed: boolean = true;
 
  @HostListener('click', ['$event.target'])
  onClick(elem: HTMLElement) {
    console.log(elem);
  }
}

举例说明:实现一个在输入时实时改变字体和边框颜色

import { Directive, HostBinding, HostListener } from '@angular/core'; 
@Directive({  
    selector:'[appRainbow]'      
})

export class RainbowDirective{ 
possibleColors = [
  'darksalmon', 'hotpink', 'lightskyblue', 'goldenrod',   
  'peachpuff', 'mediumspringgreen', 'cornflowerblue', 
  'blanchedalmond', 'lightslategrey'  
];                       
@HostBinding('style.color') color: string; 
@HostBinding('style.borderColor') borderColor: string;    
@HostListener('keydown') onKeydown() {    
    const colorPick =
       Math.floor(Math.random()*this.possibleColors.length);      
    this.color = this.borderColor 
               = this.possibleColors[colorPick];  
}}

说一下上面代码的主要部分:

  • ① 为我们的指令取名为appRainbow
  • ② 定义我们需要展示的所有可能的颜色
  • ③ 定义并用@HostBinding()装饰colorborderColor,用于设置样式
  • ④ 用@HostListener()监听宿主元素的keydown事件,为colorborderColor随机分配颜色

在页面上使用这个指令:

<input appRainbow>

效果如下:

image

D. queries :类似于Vue中的slot相关的知识点

ContentChild

contentChild

等价于@ContentChild

@Directive({
    selector: 'li'
})
export class ListItem{ }

@Component({
  selector: 'my-list',
  template: `
    <ul>
      <ng-content></ng-content>
    </ul>
  `
})
export class MyListComponent {
  @ContentChild(ListItem) items: QueryList<ListItem>;
}

占位符ng-content支持select属性,即类似vue中的slot的name属性,可以占多个位置。

多个坑

ContentChildren:通过 Content Projection 方式设置的视图中获取匹配的多个元素,返回的结果是一个 QueryList 集合。

parent.component.ts

import { 
  Component, ContentChildren, QueryList, AfterContentInit
} from '@angular/core';
import { ChildComponent } from './child.component';

@Component({
    selector: 'exe-parent',
    template: `
      <p>Parent Component</p>  
      <ng-content></ng-content>
    `
})
export class ParentComponent implements AfterContentInit {
    
    @ContentChildren(ChildComponent)
    childCmps: QueryList<ChildComponent>;

    ngAfterContentInit() {
        console.dir(this.childCmps);
    }
}

app.component.ts

import { Component } from '@angular/core';
@Component({
  selector: 'my-app',
  template: `
    <h4>Welcome to Angular World</h4>
    <exe-parent>
      <exe-child></exe-child>
      <exe-child></exe-child>
    </exe-parent>
  `,
})
export class AppComponent { }

ViewChildContentChild的区别是啥呢?ContentChild是通过占位标签<ng-content>来将子组件嵌入父组件;而ViewChild是直接写在父组件的template中(viewChildren就不多加说明喽!)

viewChild

动态组件ViewContainerRef&ng-template

下面的例子展示了如何构建动态广告条:要在广告条中显示一系列不同的广告,几个不同的小组可能会频繁加入新的广告组件,你需要一种新的组件加载方式,它不需要在广告条组件的模板中引用固定的组件。

AdDirective:注入了 ViewContainerRef 来获取对容器视图的访问权,这个容器就是那些动态加入的组件的宿主。

import { Directive, ViewContainerRef } from '@angular/core';
@Directive({
  selector: '[ad-host]',
})
export class AdDirective {
  constructor(public viewContainerRef: ViewContainerRef) { }
}

<ng-template> 元素是动态加载组件的最佳选择,因为它不会渲染任何额外的输出

@Component({
  template:`
    <div class="ad-banner-example">
        <h3>Advertisements</h3>
        <ng-template ad-host></ng-template>
    </div>`
})
export class AdBannerComponent implements OnInit, OnDestroy {
  @Input() ads: AdItem[];
  currentAdIndex = -1;
  @ViewChild(AdDirective, {static: true}) adHost: AdDirective;
  interval: any;

  constructor(private componentFactoryResolver: ComponentFactoryResolver) { }

  ngOnInit() {
    this.loadComponent();
    this.getAds();
  }

  ngOnDestroy() {
    clearInterval(this.interval);
  }

  loadComponent() {
    ... 具体方法实现及说明见官方文档...
  }

  getAds() {
    this.interval = setInterval(() => {
      this.loadComponent();
    }, 3000);
  }
}

生命周期钩子

点击上述链接,查看原文...

生命周期的顺序如下图:红色部分钩子angular只会触发一次,而绿色钩子会触发多次。(我不懂:一般情况下,如果要实现check钩子,代码一定要非常简洁&轻量级,不然,分分钟内存泄露。)

image
import { 
Component, OnInit, Input, DoCheck, AfterContentInit, OnChanges, 
AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy 
} from '@angular/core';
import { SimpleChanges } from '@angular/core/src/metadata/lifecycle_hooks';

let nextId: number = 1;

@Component({
  selector: 'app-test-demo',
  templateUrl: './test-demo.component.html',
  styleUrls: ['./test-demo.component.css']
})
export class TestDemoComponent implements 
    OnChanges, OnInit, DoCheck, 
    AfterContentInit, AfterContentChecked, AfterViewInit, 
    AfterViewChecked, OnDestroy  {

  @Input()
  public stock: string = "";
  logIt(msg: string) {
    console.log(`${nextId++} ${msg}`);
  }

  constructor() {
    this.logIt('-- constructor方法' + this.stock);
  }

  /**当被绑定的输入属性的值发生变化时调用,
    首次调用一定会发生在ngOnInit()之前。*/
   ngOnChanges(changes: SimpleChanges) {
     let currentVal = changes['stock'].currentValue;
     this.logIt('-- ngOnChanges方法' + this.stock);
   }

  /**当Angular完成组件的创建和引入时,将调用此回调。
它也会在Angular显示数据绑定属性时初始化*/
   ngOnInit() {
    this.logIt('-- ngOnInit方法');
   }

 //需要检查组件或指令的输入属性时
   ngDoCheck() {
    this.logIt('-- ngDoCheck');
   }

  //当把内容投影进组件之后调用
   ngAfterContentInit() {
    this.logIt('-- ngAfterContentInit');
   }

   //每次完成被投影组件内容的变更检测之后调用
   ngAfterContentChecked() {
    this.logIt('-- ngAfterContentChecked');
   }
   
   //初始化完组件视图及其子视图之后调用
   ngAfterViewInit() {
    this.logIt('-- ngAfterViewInit');
   }

  //每次做完组件视图和子视图的变更检测之后调用
   ngAfterViewChecked() {
    this.logIt('-- ngAfterViewChecked');
   }

  /**当Angular每次销毁指令/组件之前调用并清扫。
一般切换路由的时候,就会调用该组件的ngOnDestroy接口*/
   ngOnDestroy() {
    this.logIt('-- ngOnDestroy');
   }

}

//调用:
<app-test-demo [stock]="title" ></app-test-demo>

运行效果

image

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,601评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,367评论 1 305
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,249评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,539评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,967评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,929评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,098评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,825评论 0 207
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,609评论 1 249
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,796评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,282评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,603评论 3 261
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,277评论 3 242
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,159评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,959评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,079评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,874评论 2 277

推荐阅读更多精彩内容

  • 大家好,我是IT修真院北京分院第23期的学员郭婷婷,一枚正直纯洁善良的WEB前端程序员。 今天给大家分享一下,修真...
    茶纸团阅读 1,621评论 0 0
  • 学习资料来自 Angular.cn 与 Angular.io。 模板语法 在线例子 在 Angular 中,组件扮...
    小镭Ra阅读 3,617评论 0 3
  • Angular 2架构总览 - 简书http://www.jianshu.com/p/aeb11061b82c A...
    葡萄喃喃呓语阅读 1,437评论 2 13
  • AngularJS是什么?AngularJs(后面就简称ng了)是一个用于设计动态web应用的结构框架。首先,它是...
    200813阅读 1,528评论 0 3
  • 蓝色的窗帘……雪白的墙壁……天花板上的灯……伤感的音乐……睁着的迷离的泪眼……记忆在那里旋转,怎么也没法用意念抹去...
    梦里飘雪阅读 262评论 0 0