Explorar técnicas de manipulação de DOM Angular usando ViewContainerRef

Quando eu leio sobre trabalhar com DOM em Angular eu sempre vejo uma ou poucas destas classes mencionadas: ElementRef, TemplateRef, ViewContainerRef e outras. Infelizmente, embora algumas delas sejam abordadas em documentos Angulares ou artigos relacionados, ainda não encontrei a descrição do modelo mental geral e exemplos de como estas funcionam em conjunto. Este artigo pretende descrever esse modelo.

Se você está procurando por informações mais profundas sobre manipulação DOM em Angular usando Renderer e View Containers confira minha palestra na NgVikings. Ou leia um artigo aprofundado sobre manipulação dinâmica de DOM trabalhando com DOM em Angular: consequências inesperadas e técnicas de otimização

Se você vem de angular.js mundo, você sabe que foi muito fácil manipular o DOM. DOM Angular injetado element na função link e você poderia consultar qualquer nó dentro do modelo do componente, adicionar ou remover nós filhos, modificar estilos, etc. No entanto, esta abordagem tinha uma grande falha – estava firmemente ligada a uma plataforma de navegador.

A nova versão Angular roda em plataformas diferentes – em um navegador, em uma plataforma móvel ou dentro de um web worker. Portanto, é necessário um nível de abstração para ficar entre a API específica da plataforma e as interfaces do framework. Em angular estas abstrações vêm em uma forma dos seguintes tipos de referência: ElementRef, TemplateRef, ViewRef, ComponentRef e ViewContainerRef. Neste artigo vamos dar uma olhada detalhada em cada tipo de referência e mostrar como elas podem ser usadas para manipular DOM.

@ViewChildLink para esta seção

Antes de explorarmos as abstrações de DOM, vamos entender como acessamos essas abstrações dentro de uma classe componente/diretivo. Angular fornece um mecanismo chamado DOM queries. Ele vem na forma de @ViewChild e @ViewChildren decoradores. Eles se comportam da mesma forma, apenas o primeiro retorna uma referência, enquanto o segundo retorna múltiplas referências como um objeto QueryList. Nos exemplos deste artigo estarei usando principalmente o decorador ViewChild e não estarei usando o símbolo @ antes dele.

Usualmente, estes decoradores são emparelhados com variáveis de referência de template. Uma variável de referência de template é simplesmente uma referência nomeada a um elemento DOM dentro de um template. Você pode vê-la como algo semelhante ao atributo id de um elemento html. Você marca um elemento DOM com uma referência de modelo e depois consulta-o dentro de uma classe usando o decorador ViewChild. Aqui está o exemplo básico:

<>Copy
@Component({ selector: 'sample', template: ` <span #tref>I am span</span> `})export class SampleComponent implements AfterViewInit { @ViewChild("tref", {read: ElementRef}) tref: ElementRef; ngAfterViewInit(): void { // outputs `I am span` console.log(this.tref.nativeElement.textContent); }}

A sintaxe básica do elemento ViewChild decorator é:

<>Copiar
@ViewChild(, {read: });

Neste exemplo você pode ver que eu especifiquei tref como um nome de referência modelo no html e receber o ElementRef associado a este elemento. O segundo parâmetro read nem sempre é necessário, pois Angular pode inferir o tipo de referência pelo tipo do elemento DOM. Por exemplo, se for um simples elemento html como span, Angular retorna ElementRef. Se for um elemento template, retorna TemplateRef. Algumas referências, como ViewContainerRef não podem ser inferidas e têm de ser pedidas especificamente no parâmetro read. Outras, como ViewRef não podem ser retornadas do DOM e têm que ser construídas manualmente.

Okay, agora que sabemos como consultar as referências, vamos começar a explorá-las.

ElementRefLink para esta seção

Esta é a abstração mais básica. Se você observar sua estrutura de classes, você verá que ela contém apenas o elemento nativo ao qual está associada. É útil para acessar o elemento nativo DOM como podemos ver aqui:

<>Copy
// outputs `I am span`console.log(this.tref.nativeElement.textContent);

No entanto, tal uso é desencorajado pela equipe Angular. Não só representa um risco para a segurança, mas também une estreitamente a sua aplicação e camadas de renderização, o que torna difícil executar uma aplicação em múltiplas plataformas. Eu acredito que não é o acesso a nativeElement que quebra a abstração, mas sim o uso de uma API DOM específica como textContent. Mas como você verá mais tarde, o modelo mental de manipulação de DOM implementado em Angular quase nunca requer esse acesso de nível inferior.

ElementRef pode ser retornado para qualquer elemento DOM usando o decorador ViewChild. Mas como todos os componentes são hospedados dentro de um elemento DOM personalizado e todas as diretivas são aplicadas aos elementos DOM, as classes de componentes e diretivas podem obter uma instância de ElementRef associada ao seu elemento hospedeiro através da Injeção de Dependência (DI):

<>Copy
@Component({ selector: 'sample', ...export class SampleComponent{ constructor(private hostElement: ElementRef) { //outputs <sample>...</sample> console.log(this.hostElement.nativeElement.outerHTML); }

Então, enquanto um componente pode obter acesso ao seu elemento hospedeiro através de DI, o decorador ViewChild é usado com mais frequência para obter uma referência a um elemento DOM na sua visualização (template). Mas, é invertido para as directivas – elas não têm vistas e normalmente trabalham directamente com o elemento a que estão ligadas.

TemplateRefLink para esta secção

A noção de um template deve ser familiar para a maioria dos programadores web. É um grupo de elementos DOM que são reutilizados em views em toda a aplicação. Antes do padrão HTML5 introduzir a tag do template, a maioria dos templates chegou ao browser envolto numa tag script com alguma variação do atributo type:

<>Copy
<script type="text/template"> <span>I am span in template</span></script>

Esta abordagem certamente teve muitos inconvenientes como a semântica e a necessidade de criar manualmente modelos DOM. Com a tag template, um browser parses html e cria uma árvore DOM mas não a renderiza. Pode então ser acedido através da propriedade content:

<>Copy
<script> let tpl = document.querySelector('#tpl'); let container = document.querySelector('.insert-after-me'); insertAfter(container, tpl.content);</script><div class="insert-after-me"></div><ng-template> <span>I am span in template</span></ng-template>

Angular abraça esta abordagem e implementa TemplateRef classe para trabalhar com um template. Aqui está como pode ser usado:

<>Copy
@Component({ selector: 'sample', template: ` <ng-template #tpl> <span>I am span in template</span> </ng-template> `})export class SampleComponent implements AfterViewInit { @ViewChild("tpl") tpl: TemplateRef<any>; ngAfterViewInit() { let elementRef = this.tpl.elementRef; // outputs `template bindings={}` console.log(elementRef.nativeElement.textContent); }}

O framework remove o elemento template do DOM e insere um comentário no seu lugar. Assim fica quando renderizado:

<>Copiar
<sample> <!--template bindings={}--></sample>

Por si só a classe TemplateRef é uma classe simples. Ela contém uma referência ao seu elemento hospedeiro na propriedade elementRef e tem um método: createEmbeddedView. Entretanto, este método é muito útil já que nos permite criar uma view e retornar uma referência a ela como ViewRef.

ViewRefLink para esta seção

Este tipo de abstração representa uma Angular View. No mundo Angular uma View é um bloco fundamental de construção da IU da aplicação. É o menor agrupamento de elementos que são criados e destruídos juntos. A filosofia Angular encoraja os desenvolvedores a ver a IU como uma composição de Views, não como uma árvore de tags HTML autônoma.

Angular suporta dois tipos de Views:

  • Vistas embutidas que são ligadas a um Template
  • Vistas Host que são ligadas a um Componente

Criar um viewLink embutido para esta seção

Um template simplesmente contém um plano para uma view. Uma vista pode ser instanciada a partir do template usando o método createEmbeddedView acima mencionado:

<>Cópia
ngAfterViewInit() { let view = this.tpl.createEmbeddedView(null);}

Criar um host viewLink para esta seção

Vistas de host são criadas quando um componente é instanciado dinamicamente. Um componente pode ser criado dinamicamente usando ComponentFactoryResolver:

<>Copy
constructor(private injector: Injector, private r: ComponentFactoryResolver) { let factory = this.r.resolveComponentFactory(ColorComponent); let componentRef = factory.create(injector); let view = componentRef.hostView;}

Em Angular, cada componente é ligado a uma instância particular de um injetor, então estamos passando a instância do injetor atual ao criar o componente. Além disso, não se esqueça que componentes que são instanciados dinamicamente devem ser adicionados aos Componentes de Entrada de um módulo ou componente de hospedagem.

Então, vimos como ambas as vistas embedded e host podem ser criadas. Uma vez criada uma vista, ela pode ser inserida no DOM usando ViewContainer. A próxima seção explora sua funcionalidade.

ViewContainerRefLink para esta seção

ViewContainerRef representa um container onde uma ou mais vistas podem ser anexadas.

A primeira coisa a mencionar aqui é que qualquer elemento DOM pode ser usado como um container de vista. O interessante é que Angular não insere vistas dentro do elemento, mas anexa-as após o elemento ligado a ViewContainer. Isto é similar a como o elemento router-outlet insere componentes.

Usualmente, um bom candidato para marcar um lugar onde um ViewContainer deve ser criado é o elemento ng-container. Ele é renderizado como um comentário e por isso não introduz elementos HTML redundantes no DOM. Aqui está o exemplo de criar um ViewContainer num lugar específico num template de componente:

<>Copiar
@Component({ selector: 'sample', template: ` <span>I am first span</span> <ng-container #vc></ng-container> <span>I am last span</span> `})export class SampleComponent implements AfterViewInit { @ViewChild("vc", {read: ViewContainerRef}) vc: ViewContainerRef; ngAfterViewInit(): void { // outputs `template bindings={}` console.log(this.vc.element.nativeElement.textContent); }}

Apenas como com outras abstracções do DOM, ViewContainer está ligado a um elemento particular do DOM acedido através da propriedade element. No exemplo acima está vinculado ao elemento ng-container renderizado como um comentário, e assim a saída é template bindings={}.

Manipular vistasLink para esta seção

ViewContainer fornece uma API conveniente para manipulação de vistas:

<>Copiar
class ViewContainerRef { ... clear() : void insert(viewRef: ViewRef, index?: number) : ViewRef get(index: number) : ViewRef indexOf(viewRef: ViewRef) : number detach(index?: number) : ViewRef move(viewRef: ViewRef, currentIndex: number) : ViewRef}

Vimos anteriormente como dois tipos de vistas podem ser criados manualmente a partir de um template e de um componente. Uma vez que temos uma vista, podemos inseri-la num DOM usando o método insert. Aqui está o exemplo de criar uma vista embutida a partir de um template e inseri-la num local específico marcado por um elemento ng-container:

<>Copiar
@Component({ selector: 'sample', template: ` <span>I am first span</span> <ng-container #vc></ng-container> <span>I am last span</span> <ng-template #tpl> <span>I am span in template</span> </ng-template> `})export class SampleComponent implements AfterViewInit { @ViewChild("vc", {read: ViewContainerRef}) vc: ViewContainerRef; @ViewChild("tpl") tpl: TemplateRef<any>; ngAfterViewInit() { let view = this.tpl.createEmbeddedView(null); this.vc.insert(view); }}

Com esta implementação, o resultado html parece assim:

<>Copiar
<sample> <span>I am first span</span> <!--template bindings={}--> <span>I am span in template</span> <span>I am last span</span> <!--template bindings={}--></sample>

Para remover uma vista do DOM, podemos usar o método detach. Todos os outros métodos são auto-explicativos e podem ser usados para obter uma referência a uma vista pelo índice, mover a vista para outro local, ou remover todas as vistas do recipiente.

Creating ViewsLink to this section

ViewContainer também fornece uma API para criar uma vista automaticamente:

<>Copy
class ViewContainerRef { element: ElementRef length: number createComponent(componentFactory...): ComponentRef<C> createEmbeddedView(templateRef...): EmbeddedViewRef<C> ...}

Estes são simplesmente invólucros convenientes para o que fizemos manualmente acima. Eles criam uma vista a partir de um template ou componente e inserem-no no local especificado.

ngTemplateOutlet e ngComponentOutletLink para esta secção

Embora seja sempre bom saber como funcionam os mecanismos subjacentes, normalmente é desejável ter algum tipo de atalho. Este atalho vem na forma de duas directivas: ngTemplateOutlet e ngComponentOutlet. No momento desta escrita ambas são experimentais e ngComponentOutlet estarão disponíveis a partir da versão 4. Mas se você leu tudo acima, será muito fácil de entender o que eles fazem.

ngTemplateOutletLink para esta seção

Esta marca um elemento DOM como um ViewContainer e insere uma visualização embutida criada por um template sem a necessidade de fazer isso explicitamente em uma classe de componentes. Isto significa que o exemplo acima onde criamos uma view e a inserimos em um elemento #vc DOM pode ser reescrito assim:

<>Copy
@Component({ selector: 'sample', template: ` <span>I am first span</span> <ng-container ="tpl"></ng-container> <span>I am last span</span> <ng-template #tpl> <span>I am span in template</span> </ng-template> `})export class SampleComponent {}

Como você pode ver não usamos nenhum código instanciador de view na classe de componentes. Muito útil!

ngComponentOutletLink para esta seção

Esta diretiva é análoga a ngTemplateOutlet com a diferença de que ela cria uma vista do host (instancia um componente), e não uma vista embebida. Você pode usá-la assim:

<>Copiar
<ng-container *ngComponentOutlet="ColorComponent"></ng-container>

Brapping upLink para esta seção

Eu percebo que toda esta informação pode ser muito para digerir. Mas na verdade é bastante coerente e estabelece um modelo mental claro para manipular o DOM através de vistas. Você obtém referências às abstrações do DOM angular usando uma consulta ViewChild junto com as referências das variáveis do modelo. O invólucro mais simples em torno de um elemento DOM é ElementRef. Para templates você tem TemplateRef que lhe permite criar uma view embutida. As vistas do host podem ser acessadas em componentRef criado usando ComponentFactoryResolver. As vistas podem ser manipuladas com ViewContainerRef. Existem duas directivas que tornam o processo manual automático: ngTemplateOutlet – para vistas embebidas e ngComponentOutlet para vistas host (componentes dinâmicos).

Discutir com a comunidade

Leave a Reply