ngTemplateOutlet: O segredo da personalização

Andrew Rosário
10 min readApr 7, 2021

--

Atenção: Este artigo foi originalmente escrito por Stephen Cooper do site InDepthDev. Veja o link original aqui.

O ngTemplateOutlet é uma ferramenta poderosa para criar componentes personalizáveis. É usado por muitas bibliotecas Angular para permitir que os usuários forneçam modelos personalizados. Mas como podemos fazer isso com nossos próprios componentes?

Neste artigo, demonstramos como usar o ngTemplateOutlet junto com o ngTemplateOutletContext para tornar um componente totalmente personalizável.

Personalizando um Dropdown

Estaremos trabalhando com um Dropdown Selector, pois ele serve como um ótimo caso de uso para personalizar um componente com ngTemplateOutlet. Nosso Dropdown é usado por vários clientes, cada um com uma série de features pendentes. Vamos começar apresentando nosso código do componente e, em seguida, começar a adicionar novos recursos.

(Se você quiser pular para o final, o Dropdown personalizável final está disponível aqui).

Selector Component

Nosso componente começa com uma API limpa. Ele pega uma lista de strings e as exibe via ngFor em um dropdown.

export class SelectorComponent {
selected: string;
@Input() options: string[];
@Output()selectionChanged = new EventEmitter<string>();
selectOption(option: string) {
this.selected = option;
this.selectionChanged.emit(option);
}
}
// selector.component.ts

Tudo parece bem até agora com uma interface de componente limpa.

<div dropdown>
<button dropdownToggle>{{selected || ‘Select’}}</button>
<ul dropdownMenu>
<li *ngFor=”let option of options” (click)=”selectOption(option)”>
{{option}}
</li>
</ul>
</div>
<! — selector.component.html →

Usando este componente, nosso primeiro cliente pode selecionar seu tubarão favorito.

<app-selector [options]=”sharks”></app-selector><! — client-one.component.html →
Dropdown inicial de strings

Feature: Personalizar o texto da opção

Nosso segundo cliente, que também gosta de tubarões, deseja incluir o nome latino no dropdown. Poderíamos fazer uma pequena alteração adicionando uma função de callback como um Input para atualizar o texto exibido. Isso não é necessariamente recomendado.

@Input() displayFunc: (string) => string = x => x; 
// selector.component.ts

O HTML do componente ficaria assim:

<li *ngFor=”let option of options”>
<! — Passando a opção através da função de callback →
{{displayFunc(option)}}
</li>
<! — selector.component.html →

E a chamada do componente no cliente dois:

<app-selector [options]=”sharks” [displayFunc]=”appendLatin”>
</app-selector>
<! — client-two.component.html →

Lembrete: esta não é uma abordagem recomendada!

Usando uma função de callback para atualizar o conteúdo do texto

Feature: Ícone de "Seguro para nadar"

O cliente um agora deseja incluir um ícone mostrando se é seguro para nadar perto de um determinado tubarão. Eles nos fornecem seu pacote de ícones robusto que temos que usar. Como vamos fazer isso?

Ao contrário da feature anterior, que apenas alterou o conteúdo do texto, adicionar um ícone exigirá alterações estruturais em nosso template HTML.

Abordagem errada usando *ngIf

Sem saber do ngTemplateOutlet, poderíamos decidir usar o *ngIf e outro callback que fornece o nome do ícone com base no tubarão atual.

<li *ngFor=”let option of options”>
<! — Introduzindo o ícone dentro do nosso dropdown →
<c1-icon *ngIf=”getIconFunc(option)” [name]=”getIconFunc(option)” />
{{displayFunc(option)}}
</li>
<! — selector.component.html →

Se nenhum ícone for fornecido, o padrão retorna undefined para ocultar o ícone por meio de nosso *ngIf. Isso garante que nossos outros clientes não vejam esses ícones.

@Input()
getIconFunc: (string) => string = x => undefined;
// selector.component.ts

O código do cliente um:

<app-selector [options]=”sharks” [getIconFunc]=”getIconFunc”> </app-selector> <! — client-one.component.html — >

Isso funciona e permite que eles tenham o seguinte dropdown, mas não vamos ficar muito animados porque essa não é uma ótima solução.

Solução questionável utilizando ngIf

Cliente insatisfeito devido à dependência do ícone

Na feature anterior, introduzimos uma dependência do pacote de ícones no cliente. Isso é muito ruim! Considere forçar outros clientes a instalar uma dependência extra para compilar seus aplicativos, mesmo que eles nunca precisem realmente do pacote.

Você pode considerar que sua melhor opção é duplicar o componente e ter uma instância separada para cada cliente. Embora isso possa ser uma solução rápida para o cliente dois, agora significa que você tem vários dropdowns para dar manutenção. Não é uma posição feliz como desenvolvedor!

Que tal usar o ng-content?

No Angular, podemos usar o <ng-content> para realizar a projeção de conteúdo. Talvez possamos substituir o ícone no modelo por um <ng-content> e fazer com que o cliente projete seu ícone em nosso dropdown. Desta forma, podemos remover a dependência do ícone de nosso componente.

<li *ngFor=”let option of options”>
<! — Removido: <c1-icon [name]=”swimIcon(option)” /> →
<ng-content></ng-content>
{{displayFunc(option)}}
</li>
<! — selector.component.html →

No HTML colocamos o nosso ícone:

<app-selector [options]=”sharks”>
<c1-icon [name]=”swimIcon(????)” />
</app-selector>
<! — client-one.component.html →

Embora pareça promissor, não funcionará. O ícone será exibido apenas para o último item da lista. Você só pode projetar o conteúdo em um único local, a menos que use slots nomeados. Não há uma maneira fácil de nomear slots dinamicamente como na lista acima.

ng-content não funciona para este caso de uso

O principal problema é que <ng-content> não está ciente do contexto em que está sendo processado. Ele não conhece a opção de tubarão para a qual está sendo usado. Isso significa que não podemos personalizar seu conteúdo com base no valor do dropdown.

Se ao menos houvesse uma maneira de projetarmos um modelo em nosso componente que também estivesse ciente de seu contexto local. É aqui que o ngTemplateOutlet entra!

NgTemplateOutlet

O ngTemplateOutlet atua como um placeholder para renderizar um template depois de fornecer um contexto ao mesmo. Em nosso caso, queremos um placeholder de template para cada opção do dropdown e o contexto seria o tubarão.

A documentação do Angular para o ngTemplateOutlet é um pouco ausente. Esse problema foi levantado e as ideias sobre como demonstrar o recurso começaram a ser compartilhadas.

Definindo um Template

Antes de podermos usar o ngTemplateOutlet, devemos primeiro definir um template usando o <ng-template>. O template é o corpo do elemento do <ng-template>.

<ng-template #myTemplate>
<div>Hello template</div>
</ng-template>

Para referenciar o template, nós o nomeamos por meio da sintaxe #. Adicionando #myTemplate ao elemento, podemos obter uma referência ao template usando o nome myTemplate. O tipo de myTemplate tipo TemplateRef.

Renderizando um Template

O conteúdo de um elemento <ng-template> não é renderizado no navegador. Para ter o corpo do template renderizado, devemos agora passar a referência do template para o ngTemplateOutlet.

<! — Define nosso template →
<ng-template #myTemplate> World! </ng-template>
Hello
<! — Renderiza o template neste outlet →
<ng-container [ngTemplateOutlet]=”myTemplate”></ng-container>
Output renderizado do nosso template e o outlet

O ng-template e ngTemplateOutletnos permite definir templates reutilizáveis, o que por si só é um recurso poderoso, mas estamos apenas começando!

Fornecendo o contexto do Template

Podemos levar os templates para o próximo nível, fornecendo um contexto. Isso nos permite passar dados para o template. Em nosso caso, o dado é o tubarão da opção atual. Para passar contexto para um template você usa o [ngTemplateOutletContext].

Aqui, estamos passando cada opção do dropdown para o optionTemplate. Isso permitirá que o template de opção exiba um valor diferente para cada item da lista. Também estamos definindo o índice atual para a propriedade idx do nosso contexto, pois isso pode ser útil para estilização.

<li *ngFor=”let item of items; index as i”>
<! — Definindo a opção como a propriedade $implicit do nosso contexto junto com o índice da linha →
<ng-container
[ngTemplateOutlet]=”optionTemplate”
[ngTemplateOutletContext]=”{ $implicit: option, idx: i }”
></ng-container>
</li>
<! — selector.component.html →

Você também pode usar a sintaxe abreviada abaixo.

<! — Sintaxe alternativa →
<ng-container
*ngTemplateOutlet=”optionTemplate; context:{ $implicit: option, idx: i }”
></ng-container>

Usando o contexto em seu Template

Para acessar o contexto em nosso template, usamos a sintaxe let-* para definir as variáveis ​​de input de template. Para vincular a propriedade $implicit a uma variável de template chamada option, adicionamos let-option ao nosso template. Podemos usar qualquer nome para a nossa variável de template, portanto, se colocássemos let-item ou let-shark também vincularíamos à propriedade $implicit no contexto.

Isso nos permite definir um template fora do componente de dropdown, mas com acesso à opção atual, como se nosso template fosse definido no próprio dropdown!

<ng-template #optionTemplate let-option let-position=”idx”>
{{ position }} : {{option}}
</ng-template>
<! — client-one.component.html →

Para acessar as outras propriedades em nosso contexto, temos que ser mais explícitos. Para vincular o valor idx a uma variável de template chamada position, adicionamos let-position=idx. Alternativamente, poderíamos nomeá-la como id adicionando let-id=idx.

Observe que devemos saber o nome exato da propriedade ao extrair valores do contexto que não são a propriedade $implicit. A propriedade $implicit é uma ferramenta útil, o que significa que os usuários não precisam estar cientes desse nome e também podem escrever menos código.

Autores de bibliotecas, por favor, adicionem a estrutura de tipo do seu contexto à sua documentação! Atualmente, não há nenhum auto-complete / type checking disponível para variáveis ​​de input de template.

Ao usar variáveis ​​de input de template, somos capazes de combinar o estado de onde definimos o template, com o contexto fornecido para nós onde o template é instanciado . Isso nos fornece alguns recursos incríveis!

Resolvendo nossas features pendentes

Agora estamos em posição de resolver as features conflitantes de nossos clientes com um único selector. Como um lembrete, nosso primeiro cliente queria seu ícone personalizado no dropdown, enquanto nosso segundo cliente, justificadamente, não queria essa dependência.

Configurando o Template Outlet no nosso componente

Para usar um modelo em nosso app-selector, substituímos a função de exibição e o elemento de ícone por um ng-container contendo um ngTemplateOutlet. Este outlet usará o optionTemplate do usuário ou o nosso defaultTemplate se nenhum modelo for fornecido pelo usuário.

<li *ngFor=”let option of options; index as i”>
<! — Define um template padrão →
<ng-template #defaultTemplate let-option>{{ option }}</ng-template>
<ng-container
[ngTemplateOutlet]=”optionTemplate || defaultTemplate”
[ngTemplateOutletContext]=”{ $implicit: option, index: i}”
>
</ng-container>
</li>
<! — selector.component.html →

Os templates padrão são uma ótima maneira de adaptar o ngTemplateOutlet a um componente existente.

Para garantir que o template será capaz de exibir a opção atual, devemos nos lembrar de configurar o contexto. Definimos a opção como a propriedade $implicit e também fornecemos o índice da linha atual.

O componente aceitará o optionTemplate por meio de um Input.

@Input()
optionTemplate: TemplateRef<any>;
// selector.component.ts

Você também pode usar o @ContentChild para passar o template para o seu componente. Isso força o template a ser definido dentro do <app-selector> no qual pode ser preferido se você tiver muitos Inputs. No entanto, isso torna mais difícil de compartilhar templates em várias instâncias de componentes.

Definindo o Template do cliente

Agora podemos definir nosso template personalizado com base no código do cliente. Aqui, usamos a variável de template de input para garantir a exibição do ícone correto para o tubarão fornecido.

<ng-template #sharkTemplate let-shark>
<c1-icon name=”{{ getIconFunc(shark) }}” />
{{ shark }}
</ng-template>
<! — Passando o sharkTemplate para o nosso selector via Input →
<app-selector
[options]=”sharks”
[optionTemplate]=”sharkTemplate”
></app-selector>
<! — client-one.component.html →

Em seguida, passamos nosso template por referência para o componente por meio do @InputoptionTemplate.

Tudo isso resulta em nosso dropdown de tubarões final atendendo às solicitações do cliente e, ao mesmo tempo, garantindo que nenhum outro cliente exija mais a dependência do ícone.

Dropdown personalizado usando template outlet

Tratores em vez de tubarões

Quando pensamos que tínhamos acabado, o cliente dois voltou para nós com a empolgante notícia de que eles não gostam mais de tubarões e, em vez disso, adoram tratores! Eles agora querem um dropdown para escolher tratores com imagens e botões.

A boa notícia é que podemos dar a eles o que quiserem, sem alterar nada em nosso código do dropdown. Essa é a beleza e o poder do ngTemplateOutlet.

Acabamos de atualizar o uso do template na base de código do cliente dois para tratores e passamos adiante.

<ng-template #tractorTemplate let-tractor>
<label>{{ tractor.name }}</label>
<img src=”{{ tractor.img }}” />
<button>Buy Now!</button>
</ng-template>
<! — Nenhuma mudança no seletor para o novo estilo de dropdown →
<app-selector
[options]=”tractors”
[optionTemplate]=”tractorTemplate”
></app-selector>
<! — client-two.component.html →
Mesmo dropdown, mas template de cliente totalmente diferente

Código Final do Dropdown

Ao usar o ngTemplateOutlet, podemos separar a responsabilidade do dropdown das personalizações do usuário. Isso nos permite manter uma API de componente mínima sem restringir a criatividade de nossos clientes.

Este é o código TypeScript:

export class SelectorComponent<T> {
@Input()
options: T[];
@ContentChild(“optionTemplate”)
optionTemplateRef?: TemplateRef<any>;
@Output()
selectionChanged = new EventEmitter<T>();
}
// selector.component.ts

E este o HTML:

<li *ngFor=”let option of options; index as i”>
<ng-template #defaultTemplate let-option>{{ option }}</ng-template>
<ng-container
[ngTemplateOutlet]=”optionTemplate || defaultTemplate”
[ngTemplateOutletContext]=”{ $implicit: option, index: i}”
>
</ng-container>
</li>
<! — selector.component.html →

Conclusão

Espero que depois de ler este artigo você seja capaz de usar o ngTemplateOutlet para oferecer suporte a personalizações de templates em seus próprios componentes! Também espero que você tenha um entendimento mais profundo de como suas bibliotecas de componentes favoritas estão usando ngTemplateOutlet para permitir que você as personalize.

Leitura Adicional

Aqui, cobri um único caso de uso para ngTemplateOutlet. Se você gostou deste artigo, eu recomendo fortemente a leitura do artigo Agnostic components in Angular de Alex Inkin, que leva as coisas ainda mais longe.

Exemplo ao Vivo

Experimente você mesmo com este exemplo ao vivo no Stackblitz ou clone o repositório StephenCooper / ngTemplateOutlets do GitHub.

--

--

Andrew Rosário
Andrew Rosário

Written by Andrew Rosário

Desenvolvedor Front-end, mentor e palestrante. Apaixonado por tecnologia e por compartilhar conhecimento.

No responses yet