[Angular 基础] - 视图封装 & 局部引用 & 父子组件中内容传递
之前的笔记:
-
[Angular 基础] - Angular 渲染过程 & 组件的创建
-
[Angular 基础] - 数据绑定(databinding)
-
[Angular 基础] - 指令(directives)
以上为静态页面,即不涉及到跨组件交流的内容
以下涉及到组件内的沟通,从这开始数据就“活”了
-
[Angular 基础] - 自定义事件 & 自定义属性
下面的例子依旧会沿用 [Angular 基础] - 自定义事件 & 自定义属性 这里创建的项目
视图封装(view encapsulation)
在 [Angular 基础] - Angular 渲染过程 & 组件的创建 中曾经提到过 CSS 的作用域为当前组件,这是因为 Angular 实现的 view encapsulation。
这个部分可以在 @Component
中修改,如:
@Component({
selector: 'my-component',
template: `
<p>My Component</p>
`,
encapsulation: ViewEncapsulation.Emulated // default
})
Angular 的 view encapsulation 有 3 个值:Emulated
, None
和 ShadowDom
Emulated
这也是 Angular 默认的实现,在这个实现里,Angular 会为当前组件增添独特的属性,这样当前组件的 CSS 只能绑定于当前的组件上,是一个对 shadow dom 的拟态实现,如下:
注意这里的 _ngcontent-hash-value
,这就是 Angular 随机生成的属性名称,有且只会作用于当前组件上。我这里搜索的是对应的属性名称,可以看到整个 app-server-element
下的 HTML 标签都共享同一个属性名称,无论是 server 还是 blueprint,只要是被 app-server-element
渲染的,都是如此:
None
None
代表着 Angular 将不会提供任何的 view encapsulation。
如修改一下 app.component.ts
,将其 view encapsulation 修改为 None
,同时为 p
标签增加颜色:
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
encapsulation: ViewEncapsulation.None,
})
p {
color: blue;
}
其效果如下:
鉴于该样式写在 app.component.css
中,会被所有的组件访问,因此它会成为所有 p
标签的默认样式,一直到被覆盖为止
ShadowDom
这个使用方法就是仰仗原生浏览器去实现 Shadow DOM,这里将其添加到 app-server-elemen
中:
@Component({
selector: 'app-server-element',
templateUrl: './server-element.component.html',
styleUrl: './server-element.component.css',
encapsulation: ViewEncapsulation.ShadowDom,
})
实现效果如下:
可以看到,不仅 Angular 没有新增对应的属性,并且其他的样式也消失不见了。这是因为对于浏览器来说,CSS 库不会自动被应用的,因此如果要使用 CSS 库的话,要么手动导入,要么重新实现一下,或者使用 JS 动态绑定
前者依旧会引入大量的重复,因此在常见的 2C 项目中是一个比较少见的实现,比较常规的使用是在自己写库的时候会用到
局部引用(local reference)
local reference 在 [Angular 基础] - 指令(directives) 中的 ngIf
中出现过:
<p *ngIf="serverCreated; else noServer">
Sever was created, server name is {{ serverName }}
</p>
<ng-template #noServer>
<p>No server was created!</p>
</ng-template>
其中 #noServer
就是对 ng-template
的 local reference,这里起到的作用就是可以让 Angular 直接在 else
这个条件中获取 <ng-template #noServer> <p>No server was created!</p> </ng-template>
这个元素。
这个做法其实和 React 中的 ref
的作用有些相似,比如说以当前的代码为例,在 cockpit 中获取 server name 和 server content 用的方法是双向绑定,也就是这样的语法 [(ngModel)]="newServerName"
,但是如果不需要追踪这个值的变化,只需要在点击提交的时候获取元素中的值,则是可以通过 local reference 去实现:
-
V 层修改
<!-- <input type="text" class="form-control" [(ngModel)]="newServerName" /> --> <input type="text" class="form-control" #serverNameInput /> <!-- 省略若干实现,注意这里传的值 --> <button class="btn btn-primary" (click)="onAddServer(serverNameInput)"> Add Server </button>
其中需要注意的一点就是,local reference 只能在 View 层中传递,它无法 直接 在 VM 层中被访问
-
VM 层修改
export class CockpitComponent { onAddServer(nameInput: HTMLInputElement) { console.log(nameInput); console.dir(nameInput); } }
输出结果为:
Local Reference 的主要用法如下:
-
直接访问 DOM 元素
-
直接访问子元素
这点下面会提到怎么实现
-
搭配 structural directives 进行条件渲染
-
获取第三方库中的值
父子组件间内容的访问与投射
[Angular 基础] - 自定义事件 & 自定义属性 是父子组件中的属性与事件的交流,这里是内容 (content, DOM) 之间的投射与访问
@ViewChild
虽然说是父组件访问子组件的方法,不过也可以用在同组件的 VM 层和 V 层
获取 Element Ref
具体的方法也是通过绑定 local reference 和 @ViewChild
decorator 去实现,代码如下:
-
V 层
这里的修改和 local reference 中对 V 层的修改类似
<input type="text" class="form-control" #serverContentInput />
-
VM 层
export class CockpitComponent { @ViewChild('serverContentInput', { static: true }) serverContentInput: ElementRef; onAddServer(nameInput: HTMLInputElement) { console.log(this.serverContentInput); } }
输出结果如下:
⚠️:和直接使用 local reference 不同,这里创造的是一个 ElementRef
父组件获取子组件
这里只有 VM 层的修改,在 app.component.ts
中添加一下代码:
export class AppComponent {
@ViewChild(CockpitComponent, { static: true })
cockpitComponent: CockpitComponent;
onServerAdded(serverData: Omit<ServerElement, 'type'>) {
console.log(this.cockpitComponent);
}
}
输出结果如下:
⚠️:这个方法只能获取第一个 instance,如果有多个子组件,可以用 @ViewChildren
进行实现,这个用法暂时不会涉及,等到用到时再补充
❗: 一般不推荐用这种方法去访问/获取 V 层的数据
投射内容
这里是在父元素中渲染一个 placeholder,随后等数据接收完毕后,让子元素重写这个 placeholder,以当前项目为例,目前 server-element
渲染的内容为:
<p>
<strong *ngIf="aliasElement.type === 'server'" style="color: red"
>{{ aliasElement.content }}</strong
>
<em *ngIf="aliasElement.type === 'blueprint'">{{ aliasElement.content }}</em>
</p>
这样的数据是在 child component 中处理的,不过在有些情况下,对应的数据处理可能需要在父组件完成,而不是通过传递 props 在子组件中进行二次检查——尤其很多时候需要传递 onclick
, onsubmit
这种点击事件到子组件中,但是逻辑处理依旧存在于父组件里就会显得比较麻烦——也是可以实现的,如这里将 p
标签的渲染改放到父组件中:
<app-server-element
*ngFor="let serverElement of serverElements"
[element]="serverElement"
>
<p>
<strong *ngIf="serverElement.type === 'server'" style="color: red"
>{{ serverElement.content }}</strong
>
<em *ngIf="serverElement.type === 'blueprint'"
>{{ serverElement.content }}</em
>
</p>
</app-server-element>
不过直接这么修改会导致数据丢失:
这时候,需要在 server-element
中放置一个 ng-content
的指令(directive),这样当前组件就会接受从父组件传来的 内容(content),并且将其投射出来,现在的 server-element
代码如下:
<div class="panel panel-default">
<div class="panel-heading">{{ aliasElement.name }}</div>
<div class="panel-body">
<ng-content></ng-content>
</div>
</div>
这一部分相对于 React 来说确实更加的动态,子组件不需要从父组件接受数据——当然,也可以动态绑定属性和事件进行实现
@ContentChild
使用 @ContentChild
可以让子组件访问 父组件投射到子组件中的 内容(content),也就是上面使用 ng-content
进行投射的渲染内容
具体方法如下:
-
父组件中声明一个 local reference
这里的实现在 V 层:
<p #contentParagraph> <strong *ngIf="serverElement.type === 'server'" style="color: red" >{{ serverElement.content }}</strong > <em *ngIf="serverElement.type === 'blueprint'" >{{ serverElement.content }}</em > </p>
-
子组件中从 VM 层访问 local reference
export class ServerElementComponent { @ContentChild('contentParagraph', { static: true }) paragraph: ElementRef; // 这个在 lifecycle 会重新提一下 ngAfterContentInit() { console.log('ngAfterContentInit in ServerElementComponent'); console.log(this.paragraph, this.paragraph.nativeElement.textContent); } }
渲染结果如下:
⚠️:这个 directive 其实和 @ViewChild
/@ViewChildren
一样,也可以用在父组件中获取子组件的投射,并且在 ngAfterContentInit
确认投射完成后做一些对应操作