【Angular 2+】使用<ng-content>实现嵌入包含(transclusion)

1 为什么要用transclusion(嵌入包含)

不要被术语嵌入包含所迷惑,下面用一个简单的例子将它说明清楚。

现有一个卡片组件(card component),它由headerbodyfooter三个部分组成。

  • 该卡片的布局(3个部分)和颜色(header和footer的背景色为灰色)是固定的
  • header和footer只允许文本内容;
  • body中可以是任何形式的内容。

clipboard.png

比如,我们可以这样使用该组件:

clipboard.png

或者这样:

clipboard.png

亦或者这样

clipboard.png

总之非常方便。

问题来了,我们该如何实现**头部和尾部格式固定,而body中的内容可以动态显示呢?
--答案是使用嵌入包含**。

2 什么是transclusion(嵌入包含)

transclusion是一个方法,允许你定义个固定视图模板的同时,还可以通过 <ng-content>定义一个插槽,以显示动态的内容。

很有意思吧,下面我们就来实现一下。

3 实现嵌入包含transclusion

3.1 App结构

|- app/
    |- app.component.html
    |- app.component.ts
    |- app.module.ts
    |- card.component.ts
    |- card.component.html
    |- main.ts
|- index.html
|- systemjs.config.js
|- tsconfig.json

3.2 单插槽的嵌入包含

定义组件
// card.component.ts

import { Component, Input, Output } from '@angular/core';
@Component({
  selector: 'card',
  templateUrl: 'card.component.html',
})
export class CardComponent {
    @Input() header: string = 'this is header';   
    @Input() footer: string = 'this is footer';
}

组件模板template:

<!-- card.component.html -->

<div class="card">
    <div class="card-header">
        {{ header }}
    </div>

    <!-- single slot transclusion here -->
    <ng-content></ng-content>

    <div class="card-footer">
        {{ footer }}
    </div>
</div>
使用组件

现在我们已经定义好了一个组件,接下来我们将使用它。

<!-- app.component.html -->

<h1>Single slot transclusion</h1>
<card header="my header" footer="my footer">
    <!-- put your dynamic content here -->
    <div class="card-block">
        <h4 class="card-title">You can put any content here</h4>
        <p class="card-text">For example this line of text and</p>
        <a href="#" class="btn btn-primary">This button</a>
      </div>
      <!-- end dynamic content -->
<card>

最后在根模块的declarations中声明添加即可。

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

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

@NgModule({
  imports:      [ BrowserModule ],
  declarations: [ AppComponent, CardComponent ], // add in declaration
  bootstrap:    [ AppComponent ],
})

export class AppModule { }

好了,大功告成,保存并运行吧。div.card-block中的内容将会代替<ng-content></ng-content>

3.3 插槽的选择器

<ng-content>接受一个 select属性,让插槽具有选择性。
<!-- card.component.html -->

<div class="card">
    <div class="card-header">
        {{ header }}
    </div>

    <!-- add the select attribute to ng-content -->
    <ng-content select="[card-body]"></ng-content>

    <div class="card-footer">
        {{ footer }}
    </div>
</div>

注意,我们添加了select=[card-body],这里意思是“让包含card-body属性的元素取代我”。
接着,在html中添加card-body属性。

<!-- app.component.html -->

<h1>Single slot transclusion</h1>
<card header="my header" footer="my footer">

    <div class="card-block" card-body><!--  We add the card-body attribute here -->
        <h4 class="card-title">You can put any content here</h4>
        <p class="card-text">For example this line of text and</p>
        <a href="#" class="btn btn-primary">This button</a>
      </div>

<card>

保存并运行,一切照常运行。
现在,如果移除card-body,卡片body什么也显示不出来,那是因为我们定义的<ng-content>具有选择性--只有带有card-body属性的元素才可以代替插槽。

3.4 强大的选择器

<ng-content>select属性非常强大。举几个例子,

3.4.1 带值的属性
<!-- card.component.html -->
...
<ng-content select="[card-type=body]"></ng-content>
...
3.4.2 使用CSS选择器

card.component.html

...
<ng-content select=".card-body"></ng-content>
...

app.component.html

...
<div class="card-block card-body">...</div>
...

除此之外,例如select=[card][body],selector=".card.body"

3.4.3 使用HTML标签

card.component.html

...
<ng-content select="card-body"></ng-content>
...

app.component.html

...
<card-body class="card-block">...<card-body>
...

但是,你会遇到一个错误:Unhandled Promise rejection: Template parse errors: 'card-body' is not a known element
Angular 2不认识card-body标签,它既不是指令,也不是组件。一个快速回避该错误的方法是:在模块元数据中添加属性schemas,如下:

// app.module.ts

import { NgModule, NO_ERRORS_SCHEMA }      from '@angular/core'; //
import { BrowserModule } from '@angular/platform-browser';

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

@NgModule({
  imports:      [ BrowserModule ],
  declarations: [ AppComponent, CardComponent ],
  bootstrap:    [ AppComponent ],
  schemas:      [ NO_ERRORS_SCHEMA ] // add this line
})

export class AppModule { }

3.5 多插槽的嵌入包含

<!-- card.component.html -->

<div class="card">
    <div class="card-header">
    <!-- header slot here -->
        <ng-content select="card-header"></ng-content>
    </div>
    <!-- body slot here -->
    <ng-content select="card-body"></ng-content>
    <div class="card-footer">
    <!-- footer -->
        <ng-content select="card-footer"></ng-content>
    </div>
</div>

app

<!-- app.component.html -->

<h1>Multi slot transclusion</h1>
<card>
    <!-- header -->
    <card-header>
        New <strong>header</strong>
    </card-header>

    <!-- body -->
    <card-body>
        <div class="card-block">
            <h4 class="card-title">You can put any content here</h4>
            <p class="card-text">For example this line of text and</p>
            <a href="#" class="btn btn-primary">This button</a>
          </div>
    </card-body>

    <!-- footer -->
    <card-footer>
        New <strong>footer</strong>
    </card-footer>
<card>

4 总结

我们应该使用哪个选择器呢?属性选择器?html标签?类选择器?

视情况而定。我更偏向于使用属性选择器,因为它更易读。HTML标签易读但需要在元数据中添加schema属性。

应该尽可能避免使用类选择器,因为不直观。你第一眼看到它时,第一反应不是“嵌入包含”,除非你阅读了组件的源码。当然了,这都取决于你!

好了,就到这里!祝coding愉快!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值