Какое представление?
- Вид — это наименьшая группа элементов отображения, которую можно создавать и уничтожать одновременно.
- Каждый компонент имеет связанное представление.
- По сути, это структуры данных, образованные узлами (узлами просмотра), которые имеют ссылки на соответствующие узлы в DOM.
В классе TypeScript компонента мы определяем бизнес-логику (как данные извлекаются и изменяются), но в HTML-шаблоне компонента мы определяем, какие собственные узлы JS DOM (и, следовательно, какие элементы HTML) будут созданы, то есть мы определите представление, независимый от платформы объект, который определяет репрезентативные узлы, которые будут созданы (если мы создаем обычное веб-приложение, то узлы DOM, которые будут созданы).
Во время выполнения Angular выполняет бизнес-логику, которая находится внутри класса компонента, результирующие данные затем привязываются к представлению, которое затем преобразуется в элементы DOM/HTML, которые мы видим в браузере:
Соответствующие последствия о представлениях
- Структура (количество и порядок) узлов представления (основные компоненты представлений Angular) и, следовательно, структура DOM могут быть изменены только путем создания и уничтожения целых представлений внутри ViewContainer (позже мы увидим ViewContainer). . Это напрямую вытекает из определения представления (представление — это наименьшая группа элементов отображения, которые можно создавать и уничтожать вместе) и того, как работает Angular. Таким образом, мы не должны создавать элементы с операторами, как в примере ниже. Вместо этого мы должны использовать ViewContainer, как мы объясним:
document.createElement("div");
- Напротив, свойства определенного узла представления могут быть изменены напрямую, например, в ответ на взаимодействие с пользователем, но, опять же, мы должны сделать это, изменив представление, в данном случае с Renderer2, а не напрямую изменяя DOM. Не беспокойтесь о Renderer2, мы объясним это позже.
- Представления (ViewRef) наследуются от класса ChangeDetectorRef, поэтому Angular выполняет обнаружение изменений, обходя дерево представлений, а не «настоящую» DOM. Из-за этого мы всегда должны видоизменять наш «DOM», манипулируя представлением, как уже упоминалось.
Имея это в виду, что произойдет, если мы изменим DOM напрямую?
- Мы будем использовать API браузера, поэтому наше приложение не будет зависеть от платформы, и мы не сможем легко скомпилировать его в собственные мобильные приложения, используя, например, ionic:
- Например, если мы удалим элементы непосредственно из DOM, это приведет к тому, что представление Angular по-прежнему будет иметь ссылку на эти удаленные узлы DOM. Обнаружение изменений будет продолжать выполняться без необходимости на тех узлах просмотра, которые больше не имеют связанных узлов DOM.
- Наконец, это угроза безопасности:
Как же нам тогда манипулировать «ДОМом»?
Angular предоставляет нам Renderer2 для изменения определенного узла (например, добавления атрибута/свойства) и ViewContainer для создания или удаления узлов, то есть для внесения изменений в структуру документа.
Рендерер2
Renderer2 действительно прост в использовании, вам просто нужно получить элемент DOM с помощью ViewChild, затем внедрить renderer2 в конструктор и, наконец, использовать один из методов рендерера для свойства, определенного с помощью ViewChild. Например, если мы хотим добавить класс к узлу View/DOM, чтобы сделать его текст полужирным:
<!-- element.component.html --> <div #myDiv>Hello world!</div> // element.component.ts @ViewChild('myDiv') myDiv!: ElementRef; constructor(private renderer: Renderer2) { } ngAfterViewInit() { this.renderer.addClass(this.myDiv.nativeElement, 'bold'); }
Ниже вы можете увидеть все методы Renderer2, но не используйте те, которые создают или удаляют узлы DOM, такие как removeChild, так как это может привести к неожиданному поведению стратегии Angular Change Detector. Вместо этого используйте ViewContainer, как мы объясним в следующем разделе:
abstract class Renderer2 { abstract data: {...} destroyNode: ((node: any) => void) | null abstract destroy(): void abstract createElement(name: string, namespace?: string): any abstract createComment(value: string): any abstract createText(value: string): any abstract appendChild(parent: any, newChild: any): void abstract insertBefore(parent: any, newChild: any, refChild: any, isMove?: boolean): void abstract removeChild(parent: any, oldChild: any, isHostElement?: boolean): void abstract selectRootElement(selectorOrNode: any, preserveContent?: boolean): any abstract parentNode(node: any): any abstract nextSibling(node: any): any abstract setAttribute(el: any, name: string, value: string, namespace?: string): void abstract removeAttribute(el: any, name: string, namespace?: string): void abstract addClass(el: any, name: string): void abstract removeClass(el: any, name: string): void abstract setStyle(el: any, style: string, value: any, flags?: RendererStyleFlags2): void abstract removeStyle(el: any, style: string, flags?: RendererStyleFlags2): void abstract setProperty(el: any, name: string, value: any): void abstract setValue(node: any, value: string): void abstract listen(target: any, eventName: string, callback: (event: any) => boolean | void): () => void }
ViewContainer
В случае с ViewContainer вы, вероятно, видели некоторые приложения, в которых они используют этот элемент в шаблоне компонента для создания ViewContainer (поскольку это самый простой способ сделать это):
<ng-container></ng-container>
Мы можем видеть этот элемент просто как крючок в определенном месте DOM, который мы можем использовать для создания и уничтожения узлов View/DOM, которые будут размещены там, где был расположен ‹ng-container›. .
Из класса компонента мы можем получить доступ к этому элементу следующим образом:
<!-- element.component.html --> <ng-container #myContainer></ng-container> // element.component.ts @ViewChild('myContainer', {read: ViewContainerRef}) myContainer!: ViewContainerRef;
Это создаст свойство myContainer в классе нашего компонента типа ViewContainerRef, у которого есть 2 важных метода, которые позволят нам создавать представления (опять же, вы можете думать о них как о группе элементов DOM) и вставлять их в контейнер:
Почему у нас есть 2 разных метода для создания представлений? Просто потому, что в Angular есть два типа представлений:
Когда мы создаем обычный компонент в Angular, его шаблон просто определяет инструкции для создания экземпляров Host Views. Напротив, если мы используем элемент ‹ng-template› в наших шаблонах, все, что мы поместим в его открывающий и закрывающий тег, будет тем, что будет помещено во встроенное представление.
Например, если бы мы хотели создать встроенное представление, сначала мы использовали бы элемент ‹ng-template› и разместили внутри него узлы View/DOM, которые мы хотим, чтобы встроенное представление имело:
<!-- element.html --> <ng-template let-name='fromContext' #myTemplate> <span>{{name}}</span> </ng-template> <span>Hello</span> <ng-container #myContainer></ng-container> <span>!</span>
Мы получаем доступ к этому шаблону из класса компонента и делаем с ним то, что хотим:
// element.component.ts @ViewChild('myTemplate') myTemplate!: TemplateRef<any>; @ViewChild('myContainer', {read: ViewContainerRef}) myContainer!: ViewContainerRef; ngAfterViewChecked() { this.myContainer.createEmbeddedView(this.myTemplate, {fromContext: 'world'}); } // Expected HTML output: // <span>Hello</span><span>world</span><span>!</span> --> "Hello world!"
[ Обратите внимание, как мы передаем второй аргумент в createEmbeddedView для определения контекста embeddedView, к свойствам которого можно получить доступ из ‹ng-template› с HTML-атрибутами let-{property} . ]
Если ViewContainer и Renderer2 являются единственными правильными способами манипулирования DOM, почему так мало разработчиков используют их (особенно ViewContainer)?
Это верно, но также и неверно. Да, очень немногие разработчики используют ViewContainer напрямую, но также верно и то, что все мы используем встроенные директивы angular, и они используют ViewContainer и renderer2 внутри.
В случае со структурными директивами они очень полезны, потому что, когда мы создаем структурную директиву и используем ее, используя символ * + имя директивы, компилятор Angular делает для нас несколько вещей автоматически.
<div *ngIf="condition">Content to render when condition is true.</div>
- Angular поместит ‹ng-container› под элементом, в котором мы использовали директиву.
- Затем он создаст ‹ng-template› и поместит все, что у нас есть, внутрь элемента, внутри этого ‹ng-template›.
- Наконец, из конструктора класса директив мы можем легко получить доступ как к ViewContainer, так и к Template, чтобы выполнять манипуляции с DOM со встроенными представлениями.
Пример того, как angular преобразует элемент с помощью структурной директивы:
<div *ngFor="let hero of heroes; let i=index; let odd=odd; trackBy: trackById" [class.odd]="odd"> ({{i}}) {{hero.name}} </div> <ng-template ngFor let-hero [ngForOf]="heroes" let-i="index" let-odd="odd" [ngForTrackBy]="trackById"> <div [class.odd]="odd"> ({{i}}) {{hero.name}} </div> </ng-template> <ng-container></ng-container>
Затем мы можем получить к ним доступ из конструктора:
constructor( private templateRef: TemplateRef<any>, private viewContainer: ViewContainerRef) { }
Подводить итоги
- Если вы хотите изменить атрибуты определенного узла DOM, внедрите Renderer2 и используйте его методы или создайте директиву атрибута.
- Если вы хотите манипулировать структурой DOM, создавая или удаляя элементы DOM, создайте свою собственную структурную директиву, как описано в документации Angular (Angular — структурные директивы), и используйте шаблоны и ViewContainers, как мы объяснили в этой статье. .