# 在 Angular 中實現無渲染的功能性元件 ###### 2020.08.16 ###### tags: `自主學習` `翻譯` `Angular` --- 原始文章 [Going Renderless in Angular: All of the Functionality, None of the Render](https://netbasal.com/going-renderless-in-angular-all-of-the-functionality-none-of-the-render-1b105e001c8a) --- 無渲染元件 (Renderless Components) 指的是不渲染任何畫面的元件。 這種元件的職責在於提供可重用的功能性。 在 [Vue.js](https://css-tricks.com/building-renderless-vue-components/) 中有 <code>Renderless Components</code>。 在 [React](https://reactjs.org/docs/render-props.html) 中有 <code>Render Props</code> 作者用兩個例子,對照上述兩個前端框架,實作 Angular 中無渲染的功能性元件 <br> ## 使用結構型指令 對照 Vue.js 的 [toggle](https://css-tricks.com/building-renderless-vue-components/) 例子: ``` html=1 <toggle> <div slot-scope="{ on, setOn, setOff }"> <button @click="click(setOn)">Blue pill</button> <button @click="click(setOff)">Red pill</button> <div> <span v-if="on">It's all a dream, go back to sleep.</span> <span v-else> I don't know how far the rabbit hole goes, I'm not a rabbit, neither do I measure holes. </span> </div> </div> </toggle> ``` <code>toggle</code> 元件負責提供 API 用來作為畫面渲染的開關。元件本身並不關注畫面的渲染或樣式。 Angular 中的結構型指令,藉由新增和移除 DOM 元素來改變 DOM 版面。並且能夠提供 <code>context</code> 物件讓其他元件使用。 下面是一個藉由 <code>context</code>,暴露 API 給外部的 <code>toggle</code> 元件: ``` javascript=1 type Toggle = { on: boolean; setOn: Function; setOff: Function; toggle: Function; } @Directive({ selector: '[toggle]' }) export class ToggleDirective implements OnInit { on = true; @Input('toggleOn') initialState = true; constructor(private tpl: TemplateRef<{ $implicit: Toggle }>, private vcr: ViewContainerRef) { } ngOnInit() { this.on = this.initialState; this.vcr.createEmbeddedView(this.tpl, { $implicit: { on: this.on, setOn: this.setOn, setOff: this.setOff, toggle: this.toggle, } }); } setOn() { this.on = true } setOff() { this.on = false } toggle() { this.on = !this.on } } ``` 藉由 <code>this.vcr.createEmbeddedView()</code>,我們創造畫面(第一個參數) 並且將 API 藉由 <code>context</code> (第二個參數) 暴露給外部。其中 <code>TemplateRef</code> 的泛型會做為暴露給外部的 <code>context</code> 的類型,IDE 會做型別檢查。 使用 <code>toggle</code> 元件: ``` html=1 <div *toggle="let controller; on: false"> <button (click)="controller.setOn()">Blue pill</button> <button (click)="controller.setOff()">Red pill</button> <div> <span *ngIf="controller.on">...</span> <span *ngIf="!controller.on">...</span> </div> </div> ``` ![IDE 檢查](https://miro.medium.com/max/875/1*guK7ENQ91LzKcQs40Ai3QQ.gif) <br> ## 使用 ExportAs 對照 React 的 [Render Props](https://reactjs.org/docs/render-props.html) 例子: ``` javascript=1 class Mouse extends React.Component { state = { x: 0, y: 0 }; handleMouseMove = (event) { this.setState({ x: event.clientX, y: event.clientY }); } render() { return ( <div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}> {this.props.children(this.state)} </div> ); } } ``` React 的 <code>Render Props</code> 提供了一種方式,讓封裝在元件內的狀態和行為能夠分享給外部元件。上述例子中,<code>Mouse</code> 元件用來追蹤並保存滑鼠的位置。 因此可以像這樣使用 <code>Mouse</code> 元件: ``` javascript=1 class MouseTracker extends React.Component { render() { return ( <div> <h1>Move the mouse around!</h1> <Mouse> {mouse => ( <p>The mouse position is {mouse.x}, {mouse.y}</p> )} </Mouse> </div> ); } } ``` 在 Angular 中可以用 <code>exportAs</code> 來實現這樣的功能性元件: ``` javascript=1 @Component({ selector: 'mouse', exportAs: 'mouse', template: ` <div (mousemove)="handleMouseMove($event)"> <ng-content></ng-content> </div> ` }) export class MouseComponent { private _state = { x: 0, y: 0 }; get state() { return this._state; }; handleMouseMove(event) { this._state = { x: event.clientX, y: event.clientY }; } } ``` <code>MouseComponent</code> 並不關注畫面內容,而是藉由 <code>@Component()</code> 裝飾器中 <code>exportAs</code> 屬性,暴露出自己的 API,讓外部元件可以在範本中使用他的 API: ``` javascript=1 @Component({ selector: 'mouse-tracker', template: ` <mouse #mouse="mouse"> <p>The mouse position is {{ mouse.state.x }}, {{ mouse.state.y }}</p> </mouse> ` }) export class MouseTrackerComponent {} ``` 上面的範本中的 <code>#mouse="mouse"</code> 即是接取到 <code>MouseComponent</code> 的實例。 也可以用這種方式實作第一個例子中的 <code>toggle</code> 功能。 <br> ## 總結 使用結構型指令的好處,在於可以藉由定義 <code>context</code> 的型別,而明確定義哪些是要暴露給外部的 API,並且可以簡單地控制是否要根據狀態渲染畫面。 如果像 <code>MouseComponent</code> 的例子,還是必須具有部分的畫面(用來定義追蹤滑鼠移動的區塊)時,就可以使用 <code>@Component()</code> 的 <code>exportAs</code> 功能。