Если вы начинаете работать с angularjs, вам было довольно просто получить доступ к DOM и управлять им там. У вас был доступ к узлу DOM через element
, введенный в link
функцию директивы.
function link(scope, element, attrs) { }
Или через angular.element
, который был встроенным подмножеством jQuery в AngularJS. Но у этого подхода были свои недостатки. Это сделало ваш код тесно связанным с API браузера.
Новый Angular (начиная со 2) работает на нескольких платформах: мобильных, веб-воркерах и т. Д. Таким образом, они представили ряд API-интерфейсов для работы в качестве уровня абстракции между вашим кодом и API-интерфейсами платформы. Эти API-интерфейсы представлены в виде различных ссылочных типов, таких как ElementRef
, TemplateRef
, ViewRef
, ComponentRef
и ViewContainerRef
.
В этом блоге мы увидим несколько примеров того, как эти ссылочные типы могут использоваться для управления DOM в angular. Но перед этим давайте посмотрим, как получить доступ к этим ссылочным типам в компоненте / директиве.
DOM запросы
Angular предоставляет два способа запроса / доступа к различным ссылочным типам внутри компонента / директивы. Эти
- ViewChild / ViewChildren
- ContentChild / ContentChildren
ViewChild / ViewChildren
Это декораторы, которые могут использоваться в компоненте / директиве как @ViewChild
(возвращает одну ссылку) или @ViewChildren
(возвращает список ссылок в форме QueryList
). Они будут назначать значения ссылочных типов из шаблона полям компонентов, к которым они применяются. Основное использование следующее:
@ViewChild(selector, {read: ReferenceType}) fieldName;
Селектор может быть строкой, представляющей ссылочную переменную шаблона, или класс компонента / директивы, или TemplateRef, или поставщик, определенный в дереве дочерних компонентов.
@ViewChild("myElem") template: ElementRef;
Второй параметр является необязательным и требуется только для запроса некоторых ссылочных типов, которые не могут быть легко выведены с помощью Angular, например ViewContainerRef
.
@ViewChild("myContainer", {read: ViewContainerRef}) container: ViewContainerRef;
ContentChild / ContentChildren
Использование очень похоже на использование ViewChild / ViewChildren. Единственное отличие состоит в том, что он запрашивает <ng-content>
спроецированных элементов компонента, а @ViewChild
запрашивает внутри шаблона компонента. Это будет лучше объяснено на примерах в следующих разделах.
Доступ к DOM через ElementRef
ElementRef
- это очень простой уровень абстракции для элемента DOM в Angular. Это угловая оболочка вокруг собственного элемента.
Вы можете получить ElementRef в компоненте или директиве следующими способами:
Внедрение зависимости
К основному элементу компонента или директивы можно получить доступ через прямой DI в конструкторе.
@Component({ selector: 'app-test', template: '<div>I am a test component</div>' }) export class TestComponent implements OnInit { constructor(private element: ElementRef) { } ngOnInit() { console.log(this.element.nativeElement); } } /* * Output: * <app-test> * <div>I am a test component</div> * </app-test> * */
Использование переменных ViewChild и шаблонов
@Component({ selector: 'app-test', template: ` <div #child1>First Child</div> <div>Second Child</div> ` }) export class TestComponent implements OnInit { @ViewChild("child1") firstChild: ElementRef; constructor() { } ngOnInit() { console.log(this.firstChild.nativeElement); } } /* * Output: <div>First Child</div> * */
Использование ContentChild
Работает аналогично @ViewChild
, но для <ng-content>
проецируемых элементов.
// Child Component @Component({ selector: "component-a", template: `<ng-content></ng-content>` }) export class ComponentA { @ContentChild("contentChild") contentChild: ElementRef; ngOnInit() { console.log(this.contentChild.nativeElement); } } // Parent Component @Component({ selector: 'app-test', template: ` <component-a> <div #contentChild>Content Child 1</div> <div>Content Child 2</div> </component-a> ` }) export class TestComponent implements OnInit {} /* * Output: <div>Content Child 1</div> * */
Выглядит довольно просто, что вы можете легко получить доступ к элементу DOM через ElementRef
, а затем манипулировать DOM, обратившись к nativeElement
. Что-то вроде этого:
@Component({ selector: 'app-test-component', template: ` <div class="header">I am a header</div> <div class="body" #body> </div> <div class="footer">I am a footer</div> ` }) export class TestComponent implements AfterContentInit { @ViewChild("body") bodyElem: ElementRef; ngAfterContentInit(): void { this.bodyElem.nativeElement.innerHTML = `<div>Hi, I am child added by directly calling the native APIs.</div>`; } }
Однако прямое использование ElementRef не одобряется Angular Team, потому что он напрямую предоставляет доступ к DOM, что может сделать ваше приложение уязвимым для XSS-атак. Это также создает тесную связь между вашим приложением и уровнями рендеринга, что затрудняет запуск приложения на нескольких платформах.
Итак, как нам с этим бороться? Ответ: Просмотры.
В Angular все есть "представление"
Представление - это самый маленький строительный блок пользовательского интерфейса углового приложения. У каждого компонента свое представление. Вы можете рассматривать его как группу элементов, которые можно создавать и уничтожать вместе.
Представления можно разделить на два типа:
- Встроенные представления - созданы из шаблонов
- Представления хоста - созданы из компонентов
Отображение представления в пользовательском интерфейсе можно разбить на два этапа:
- Создание представления из шаблона или компонента
- Визуализация представления в контейнер представления
Встроенные представления
Встроенные представления создаются из шаблонов, определенных с помощью элемента <ng-template>
.
Создание встроенного представления
Сначала необходимо получить доступ к шаблону внутри компонента как TemplateRef
с использованием @ViewChild
и ссылочной переменной шаблона. Затем встроенное представление может быть создано из TemplateRef
путем передачи контекста привязки данных.
const viewRef = this.template.createEmbeddedView({ name: "View 1" });
Этот контекст используется шаблоном in<ng-template>
.
<ng-template #template let-viewName="name"> <div>Hi, My name is {{viewName}}. I am a view created from a template</div> </ng-template>
Вы также можете использовать свойство $implicit
в контексте, если у вас есть только одно свойство для привязки.
const viewRef = this.template.createEmbeddedView({ $implicit: "View 1" });
В этом случае вы можете пропустить присвоение значений шаблонным переменным.
<ng-template #template let-viewName> <div>Hi, My name is {{viewName}}. I am a view created from a template</div> </ng-template>
Визуализация встроенного представления
До сих пор мы создали только экземпляр ViewRef
. Это представление все еще не отображается в пользовательском интерфейсе. Чтобы увидеть его в пользовательском интерфейсе, нам нужен заполнитель (контейнер представления) для его рендеринга. Этот заполнитель предоставляется ViewContainerRef
.
Любой элемент может служить контейнером представления, однако <ng-container>
- лучший кандидат, поскольку он отображается как комментарий и не оставляет никаких избыточных элементов в html DOM.
@Component({ selector: 'app-test-component', template: ` <div class="header">I am a header</div> <div class="body"> <ng-container #container></ng-container> </div> <div class="footer">I am a footer</div> <ng-template #template let-viewName="name"> <div>Hi, My name is {{viewName}}. I am a view created from a template</div> </ng-template> `, }) export class TestComponent implements AfterContentInit { @ViewChild("template") template: TemplateRef; @ViewChild("container", {read: ViewContainerRef}) container: ViewContainerRef; constructor() { } ngAfterContentInit(): void { const viewRef = this.template.createEmbeddedView({ name: "View 1" }); this.container.insert(viewRef); } }
Оба элемента <ng-container>
и <ng-template>
будут отображаться как комментарии, оставляя HTML DOM аккуратным и чистым.
Вышеупомянутый двухэтапный процесс создания представления и добавления его в контейнер можно дополнительно сократить, используя метод createEmbeddedView
, доступный в самом ViewContainerRef
.
this.container.createEmbeddedView(this.template, { name: "View 1" });
Это можно еще больше упростить, переместив всю логику создания представления из класса компонента в шаблон с помощью ngTemplateOutlet
и ngTemplateOutletContext
.
@Component({ selector: 'app-test-component', template: ` <div class="header">I am a header</div> <div class="body"> <ng-container [ngTemplateOutlet]="template" [ngTemplateOutletContext]="{name: 'View 1'}"></ng-container> </div> <div class="footer">I am a footer</div> <ng-template #template let-viewName="name"> <div>Hi, My name is {{viewName}}. I am a view created from a template</div> </ng-template> ` }) export class TestComponent {}
Просмотры хоста
Host Views очень похожи на Embedded View. Единственное отличие состоит в том, что Host Views создаются из компонентов, а не из шаблонов.
Создание основного представления
Чтобы создать основной вид, сначала вам нужно создать ComponentFactory
компонента, который вы хотите визуализировать, используя ComponentFactoryResolver
.
constructor( private componentFactoryResolver: ComponentFactoryResolver ) { this.someComponentFactory = this.componentFactoryResolver.resolveComponentFactory(SomeComponent); }
Затем создается динамический экземпляр компонента путем передачи Injector
экземпляра в фабрику. Каждый компонент должен быть привязан к экземпляру Injector
. Вы можете использовать инжектор родительского компонента для динамически создаваемых компонентов.
const componentRef = this.someComponentFactory.create(this.injector); const viewRef = componentRef.hostView;
Визуализация вида хоста
Визуализация основного представления почти аналогична визуализации встроенного представления. Вы можете напрямую вставить его в контейнер представления.
@Component({ selector: 'app-test-component', template: ` <div class="header">I am a header</div> <div class="body"> <ng-container #container></ng-container> </div> <div class="footer">I am a footer</div> `, }) export class TestComponentComponent implements AfterContentInit { @ViewChild("container", {read: ViewContainerRef}) container: ViewContainerRef; private someComponentFactory: ComponentFactory<SomeComponent>; constructor( private componentFactoryResolver: ComponentFactoryResolver, private injector: Injector ) { this.someComponentFactory = this.componentFactoryResolver.resolveComponentFactory(SomeComponent); } ngAfterContentInit(): void { const componentRef = this.someComponentFactory.create(this.injector); const viewRef = componentRef.hostView; this.container.insert(viewRef); } }
Или путем прямого вызова createComponent
метода ViewContainerRef
и передачи экземпляра фабрики компонентов.
this.container.createComponent(this.someComponentFactory);
Теперь, как и во встроенном представлении, мы также можем перенести всю логику создания основного представления в самом шаблоне с помощью ngComponentOutlet
.
@Component({ selector: 'app-test-component', template: ` <div class="header">I am a header</div> <div class="body"> <ng-container [ngComponentOutlet]="comp"></ng-container> </div> <div class="footer">I am a footer</div> ` }) export class TestComponent { comp = SomeComponent; }
Не забудьте сохранить ссылку на класс компонента в поле родительского компонента. В шаблоне есть доступ только к полям компонентов.
Резюме
Здесь мы подошли к концу. Подведем итог тому, что мы поняли до сих пор.
- Мы можем получить доступ к DOM в Angular, используя различные ссылочные типы, такие как
ElementRef
,TemplateRef
,ViewRef
,ComponentRef
иViewContainerRef
. - Эти ссылочные типы можно запросить из шаблонов, используя
@ViewChild
и@ContentChild
. - Доступ к собственному элементу DOM браузера можно получить через
ElementRef
. Однако не рекомендуется напрямую манипулировать этим элементом из соображений безопасности. - Понятие просмотров.
- Как создать и отобразить встроенный вид.
- Как создать и отобразить компонентный вид.
Итак, на сегодня все, что нужно для понимания манипуляций с DOM в Angular. Это был мой первый блог. Пожалуйста, оставьте мне отзыв в комментариях.