ngTemplateOutlet: O segredo da personalização
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 →
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!
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.
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.
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>
O ng-template
e ngTemplateOutlet
nos 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 @Input
optionTemplate.
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.
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 →
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.