Construindo componentes mais flexíveis com ngTemplateOutlet - Angular 2+
Reaproveitamento de código com certeza é uma das preocupações de todo desenvolvedor. Quando criamos um componente, a ideia de compartilhá-lo em outros projetos pode trazer economia de tempo e dinheiro.
Eventualmente temos problemas quando tentamos utilizar um componente que quase "encaixa" na nossa necessidade. Por exemplo, suponha que você tenha um componente que renderize um card conforme a imagem abaixo, porém sua necessidade é um pouco diferente:
Como este cenário é muito comum, podemos recorrer ao <ng-template>
e <ng-container>
para deixar nossos componentes mais customizáveis.
Componente Card
Vamos direto ao código para mostrar como o componente card foi estruturado:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CardComponent } from './card.component';
@NgModule({
declarations: [
CardComponent,
],
imports: [
CommonModule,
],
exports: [
CardComponent,
]
})
export class CardModule {
}
import { Component, ContentChild, Input, TemplateRef } from '@angular/core';
@Component({
selector: 'app-card',
styleUrls: ['./card.component.css'],
templateUrl: './card.component.html',
})
export class CardComponent {
@Input() conteudo: string;
@Input() data: Date;
@Input() titulo: string;
}
<div class="card">
<div class="titulo">
<div>{{titulo}}</div>
<div>
{{data | date}}
</div>
</div>
<div class="conteudo">{{conteudo}}</div>
</div>
.card {
background: #fff;
border: 1px solid #ccc;
border-radius: 8px;
box-shadow: 0 1px 8px #ccc;
margin: 16px auto;
}
.titulo {
align-items: center;
border-bottom: 1px solid #ddd;
color: #444;
display: flex;
font-size: 1.2em;
font-weight: bold;
text-transform: uppercase;
}
.titulo div:nth-child(1) {
flex-grow: 1;
}
.titulo div:nth-child(2) {
color: #aaa;
font-size: 0.6em;
}
.conteudo {
color: #777;
font-size: 1em;
line-height: 26px;
}
.card, .titulo, .conteudo {
padding: 8px;
}
Utilizando o componente:
<app-card
[data]="dataAtual"
titulo="Card Exemplo 1"
conteudo="Comportamento padrão do card. É
exibido um título, data e um texto">
</app-card>
Customização com ngTemplateOutlet
Veja que o componente acima não permite que o desenvolvedor customize a parte onde a data é renderizada (canto superior direito). Vamos tomar como objetivo a alteração do componente de modo que seja possível customizar esta região.
<div class="card">
<div class="titulo">
<div>{{titulo}}</div>
<div>
<!-- ********************************** -->
<!-- POSSIBILITAR UMA CUSTOMIZAÇÃO AQUI -->
{{data | date}}
<!-- ********************************** -->
</div>
</div>
<div class="conteudo">{{conteudo}}</div>
</div>
Fazendo uma analogia, nosso componente é como se fosse a moldura de um quadro, você pode simplesmente troca a tela para obter um outro resultado mantendo os aspectos originais da moldura, ou seja, o "componente" moldura permite a customização do seu "template" (tela).
Como primeiro passo vamos isolar em um template (<ng-template>
) a responsabilidade de renderizar uma data já formatada:
<ng-template #header let-data="dataFormatada">
{{data}}
</ng-template>
Veja que este template é identificado através da marcação #header
e recebe um parâmetro chamado dataFormatada
. O valor do dataFormatada
é atribuído à variável data
.
No lugar do trecho {{data | date}}
vamos adicionar um <ng-container>
:
<div class="card">
<div class="titulo">
<div>{{titulo}}</div>
<div>
<!--
Como "jogar" o template aqui
no container?
-->
┌→ <ng-container>
| </ng-container>
| </div>
| </div>
| <div class="conteudo">{{conteudo}}</div>
| </div>
|
└→ <ng-template #header let-data="dataFormatada">
{{data}}
</ng-template>
Agora vamos utilizar algumas propriedades do <ng-container>
para "dizer" ao <ng-container>
que renderize o <ng-template #header>
:
<div class="card">
<div class="titulo">
<div>{{titulo}}</div>
<div>
<ng-container
[ngTemplateOutlet]="header"
[ngTemplateOutletContext]="{ dataFormatada: (data | date) }">
</ng-container>
</div>
</div>
<div class="conteudo">{{conteudo}}</div>
</div>
<ng-template #header let-data="dataFormatada">
{{data}}
</ng-template>
[ngTemplateOutlet]
recebe o template que será renderizado. No nosso caso será o templateheader
[ngTemplateOutletContext]
envia um objeto (contexto) para o template. Neste caso vamos passar a data já formatada na variáveldataFormatada
. O templateheader
já está preparado para receber e tratar esta variável.
Pronto! com esta pequena refatoração isolamos a área responsável por renderizar a data no canto superior direito. Porém, nada mudou no resultado final, ainda! veja na imagem abaixo o resultado até este momento:
Importante: perceba que o <ng-container>
pode receber qualquer template. Esta refatoração foi importante justamente por este motivo. Agora podemos passar um template que assumirá a responsabilidade de renderizar a data no canto superior direito.
Trazendo isto para o código, teríamos a seguinte situação:
<app-card
[data]="dataAtual"
titulo="Card Exemplo 4"
conteudo="Card Teste">
<!--
Template para customizar a renderização
do canto superior direito
-->
<ng-template let-data="dataFormatada">
Meu template: {{data}}
</ng-template>
</app-card>
No código acima estamos passando um <ng-template>
para dentro do app-card
, porém o componente ainda não está preparado para receber um <ng-template>
externo. Então para identificar a presença de um <ng-template>
, podemos utilizar o @ContentChild
e convencionar um identificador (como um id
) padrão para este <ng-template>
externo:
import {
Component,
ContentChild,
Input,
TemplateRef
} from '@angular/core';
@Component({
selector: 'app-card',
styleUrls: ['./card.component.css'],
templateUrl: './card.component.html',
})
export class CardComponent {
@Input() conteudo: string;
@Input() data: Date;
@Input() titulo: string;
// Aqui verificamos se existe um template chamado tmpHeader
// O Angular irá procurar por um <ng-template #tmpHeader>
@ContentChild('tmpHeader') headerCustomizado: TemplateRef<any>;
}
Agora nosso componente consegue identificar a presença de um <ng-template>
identificado com o valor #tmpHeader
. Então podemos passar um template customizado da seguinte forma:
<app-card
[data]="dataAtual"
titulo="Card Exemplo 4"
conteudo="Card Teste">
<!--
Template para customizar a renderização
do canto superior direito
-->
<ng-template #tmpHeader let-data="dataFormatada">
Meu template: {{data}}
</ng-template>
</app-card>
Para finalizar, precisamos "dizer" ao <ng-container>
(dentro do template do componente) que se a variável headerCustomizado
tiver um valor, ou seja, houver um <ng-template #tmpHeader>
, ela (template externo) deverá ser utilizada para renderizar o canto superior direito do card. Caso contrário, o <ng-template #header>
(dentro do template do próprio componente) deverá ser utilizado.
O código ficará da seguinte forma:
<div class="card">
<div class="titulo">
<div>{{titulo}}</div>
<div>
<!-- quando headerCustomizado é null,
utilizamos o template padrão header -->
<ng-container
[ngTemplateOutlet]="(headerCustomizado || header)"
[ngTemplateOutletContext]="{ dataFormatada: (data | date) }">
</ng-container>
</div>
</div>
<div class="conteudo">{{conteudo}}</div>
</div>
<ng-template #header let-data="dataFormatada">
{{data}}
</ng-template>
Resultado:
Veja que gora podemos passar qualquer template para o componente de modo a customizar o trecho onde a data é renderizada. Abaixo há alguns exemplos:
<app-card
[data]="dataAtual"
titulo="Card Exemplo 1"
conteudo="Comportamento padrão do card. É
exibido um título, data e um texto">
</app-card>
<app-card
[data]="dataAtual"
titulo="Card Exemplo 2"
conteudo="Customizando parte do template
através da injeção de um template
customizado para incluir um botão
ao lado da data">
<ng-template #tmpHeader let-data="dataFormatada">
<div style="display: flex; align-items: center">
<div>{{data}}</div>
<div>
<input
type="button"
value="?"
title="Ajuda">
</div>
</div>
</ng-template>
</app-card>
<app-card
[data]="dataAtual"
titulo="Card Exemplo 3"
conteudo="Customizando parte do template
através da injeção de um template
customizado para criar botão
com o texto da data.">
<ng-template #tmpHeader let-data="dataFormatada">
<input type="button" [value]="data">
</ng-template>
</app-card>
<app-card
[data]="dataAtual"
titulo="Card Exemplo 4"
conteudo="Card Teste">
<ng-template #tmpHeader let-data="dataFormatada">
Meu template: {{data}}
</ng-template>
</app-card>
Considerações
Com o entendimento do <ng-container>
e ngTemplateOutlet
podemos construir componentes mais flexíveis e aumentar o reaproveitamento de código. Inclusive, em um post um pouco mais antigo utilizamos este recurso para criar um componente no estilo tabela.
Código fonte:
Links interessantes: