Я ежедневно работаю как с Angular, так и с React, а также большой поклонник Vue и Svelte и слежу за их развитием. В этой статье я покажу, как с помощью Angular получить компоненты без рендеринга, как в Vue, и рендер-пропсы, как в React.
Как следует из самого названия, компоненты без рендеринга ничего не рендерят; их единственное назначение — предоставлять функциональность для повторного использования. Давайте возьмем в качестве примера переключатель toggle
, написанный на Vue, и преобразуем его на Angular. Вот Vue-версия:
<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>
Безрендерный компонент toggle
отвечает за API для переключателя представления. Ему все равно, как это представление структурировано или стилизовано. Той же функциональности можно достичь в Angular с помощью структурных директив.
Структурные директивы
Структурная директива изменяет макет DOM, добавляя и удаляя элементы DOM (например, представление view
). Дополнением к этому служит такая мощная функция, как предоставление объекта context
, который доступен любому адресату структурной директивы.
Создадим структурную директиву toggle
, которая предоставляет доступ к API через свойство context
:
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 }
}
Мы создаем view
и передаем публичный API, который хотим сделать доступным через context
, роль которого исполняет второй параметр в createEmbeddedView
. Самое интересное здесь то, что TemplateRef
принимает универсальный тип, который служит типом для context
. Современные IDE, такие как Webstorm, автоматически выводят его в шаблоне. Давайте им воспользуемся:
<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>
Спасибо, Webstorm!
ExportAs и его применение
Второй случай разберем на официальном примере рендер-пропсов React. Рендер-пропсы предоставляют возможность одним компонентам совместно использовать состояние или поведение, инкапсулированное в другом компоненте, если им требуется то же самое состояние. Например, следующий компонент React отслеживает положение мыши в веб-приложении:
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>
);
}
}
Мы используем дочерний (children
) рендер-проп в качестве функции, которая предоставляет состояние компонента любому потребителю.
Поведение, которым мы хотим поделиться, благодаря этой технике становится максимально портируемым. Чтобы реализовать это поведение, отрисуйте <Mouse>
с помощью рендер-пропа, который говорит ей, что нужно визуализировать с текущими координатами курсора (x, y)
:
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:
@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
};
}
}
Компонент MouseComponent
не связан с его содержимым. Он предоставляет свой API с помощью свойства exportAs
, которое сообщает Angular, что мы можем использовать этот API компонента в шаблоне:
@Component({
selector: 'mouse-tracker',
template: `
<mouse #mouse="mouse">
<p>The mouse position is {{ mouse.state.x }}, {{ mouse.state.y }}</p>
</mouse>
`
})
export class MouseTrackerComponent {}
И всё, ничего больше не нужно. Метод exportAs
также можно применить вместо структурной директивы для компонента toggle
, который мы написали ранее.
Итоги
Не думаю, что есть правильные или неправильные варианты использования, когда речь заходит о структурной директиве или функции exportAs
. Преимущества структурных директив заключаются в том, что мы можем явно определить API, который хотим предоставлять в представлении, и проконтролировать, следует ли визуализировать представление или нет. Когда нам все еще нужна какая-то часть представления, как в примере с MouseComponent
, можно воспользоваться компонентом и функцией exportAs
.
Читайте также:
- Четыре сигнала нехватки концептуальных знаний в Angular
- Плохие практики Angular
- 3 способа визуализации больших списков в Angular
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Netanel Basal: Going Renderless in Angular: All of the Functionality, None of the Render