Esse desafio foi bem diferente do modal, porque aqui eu precisei entender melhor como um componente pai consegue “enxergar” os componentes filhos que foram passados dentro dele.
Publicado em 03 de maio de 2026
Esse desafio foi bem diferente do modal, porque aqui eu precisei entender melhor como um componente pai consegue “enxergar” os componentes filhos que foram passados dentro dele.
A ideia era criar um componente de abas customizado, parecido com isso:
<app-tabs>
<app-tab title="Perfil">Conteúdo do perfil</app-tab>
<app-tab title="Config">Conteúdo das configurações</app-tab>
<app-tab title="Segurança">Conteúdo de segurança</app-tab>
</app-tabs>
No começo parece simples, mas aí vem a pergunta:
Como o app-tabs sabe quais app-tab existem dentro dele?
Foi aí que entrou o @ContentChildren.
A primeira coisa que fiz foi criar o componente de cada aba, sendo:
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-tab',
standalone: true,
templateUrl: './tab.html',
})
export class Tab {
@Input() title = '';
active = false;
}
E no HTML do app-tab, eu deixei ele renderizar o conteúdo apenas quando a aba estiver ativa:
@if (active) {
<div class="tab-content">
<ng-content></ng-content>
</div>
}
Aqui tem um detalhe legal: o conteúdo da aba só aparece quando active for true.
Ou seja, se a aba não estiver selecionada, o conteúdo dela nem é renderizado na tela. Isso já ajuda no bônus de renderizar apenas a aba ativa.
Depois criei o componente pai, o app-tabs.
import {
AfterContentInit,
Component,
ContentChildren,
QueryList,
} from '@angular/core';
import { Tab } from './tab/tab';
@Component({
selector: 'app-tabs',
standalone: true,
imports: [],
templateUrl: './tabs.html',
styleUrl: './tabs.scss',
})
export class Tabs implements AfterContentInit {
@ContentChildren(Tab) tabs!: QueryList<Tab>;
ngAfterContentInit() {
const firstTab = this.tabs.first;
if (firstTab) {
this.selectTab(firstTab);
}
}
selectTab(selectedTab: Tab) {
this.tabs.forEach((tab) => {
tab.active = false;
});
selectedTab.active = true;
}
}
Essa linha aqui foi a parte mais importante do desafio:
@ContentChildren(Tab) tabs!: QueryList<Tab>;
Basicamente ela diz:
Angular, pega todos os componentes Tab que foram colocados dentro do meu app-tabs.
Então quando eu escrevo isso:
<app-tabs>
<app-tab title="Perfil">Conteúdo do perfil</app-tab>
<app-tab title="Config">Conteúdo das configurações</app-tab>
<app-tab title="Segurança">Conteúdo de segurança</app-tab>
</app-tabs>
O Angular entende mais ou menos assim:
tabs = [
Tab Perfil,
Tab Config,
Tab Segurança
]
O QueryList<Tab> é basicamente uma lista especial do Angular com todos esses componentes encontrados.
Dai no HTML do app-tabs, eu criei os botões de navegação com base nessa lista de abas:
<section class="tabs">
<div class="tabs-header">
@for (tab of tabs; track tab.title) {
<button
type="button"
class="tab-button"
[class.active]="tab.active"
(click)="selectTab(tab)"
>
{{ tab.title }}
</button>
}
</div>
<div class="tabs-body">
<ng-content></ng-content>
</div>
</section>
Aqui acontece uma coisa bem legal.
O @for percorre todas as abas encontradas pelo @ContentChildren e cria um botão para cada uma.
Então se eu tiver três abas:
<app-tab title="Perfil"></app-tab>
<app-tab title="Config"></app-tab>
<app-tab title="Segurança"></app-tab>
automaticamente vou ter três botões:
Perfil | Config | Segurança
Outro ponto importante é o ng-content.
<ng-content></ng-content>
Ele é responsável por renderizar os app-tab que foram passados dentro do app-tabs.
Mas como cada app-tab só mostra o conteúdo quando active for true, apenas a aba selecionada aparece.
A função que troca a aba ativa ficou assim:
selectTab(selectedTab: Tab) {
this.tabs.forEach((tab) => {
tab.active = false;
});
selectedTab.active = true;
}
A lógica é bem simples:
1. Desativa todas as abas
2. Ativa apenas a aba clicada
Então quando clico em “Config”, por exemplo, todas as outras viram false e só a aba Config fica com active = true.
Também usei o ngAfterContentInit:
ngAfterContentInit() {
const firstTab = this.tabs.first;
if (firstTab) {
this.selectTab(firstTab);
}
}
Esse método foi necessário porque o Angular só consegue preencher o @ContentChildren depois que o conteúdo dentro do componente foi carregado.
Ou seja, no começo o componente Tabs ainda não sabe quais app-tab existem dentro dele.
A ordem é mais ou menos assim:
1. Angular cria o app-tabs
2. Angular lê o conteúdo dentro dele
3. Angular encontra os app-tab
4. Angular preenche o this.tabs
5. Angular chama o ngAfterContentInit
Por isso eu não posso simplesmente pegar this.tabs.first no início da classe ou no constructor.
O lugar certo para mexer com conteúdo projetado é o ngAfterContentInit.
Para usar o componente, ficou assim:
<app-tabs>
<app-tab title="Perfil">
<h2>Perfil</h2>
<p>Aqui ficam as informações do usuário.</p>
</app-tab>
<app-tab title="Config">
<h2>Configurações</h2>
<p>Aqui ficam as configurações da conta.</p>
</app-tab>
<app-tab title="Segurança">
<h2>Segurança</h2>
<p>Aqui ficam opções de senha e privacidade.</p>
</app-tab>
</app-tabs>
E como estou usando componentes standalone, precisei importar tanto o Tabs quanto o Tab no componente onde estou usando:
import { Component } from '@angular/core';
import { Tabs } from './components/tabs/tabs';
import { Tab } from './components/tabs/tab/tab';
@Component({
selector: 'app-root',
standalone: true,
imports: [Tabs, Tab],
templateUrl: './app.html',
styleUrl: './app.scss',
})
export class App {}
No começo eu achei estranho precisar importar os dois, porque na minha cabeça o Tab estava “dentro” do Tabs.
Mas depois fez sentido.
Eu preciso importar Tabs porque uso:
<app-tabs>
E preciso importar Tab porque também uso diretamente no HTML:
<app-tab>
Então o Angular precisa conhecer os dois seletores.
Esse desafio me ajudou bastante a entender a diferença entre ViewChild e ContentChildren.
ViewChild ou ViewChildren serve para pegar elementos que estão dentro do template do próprio componente.
Exemplo:
<!-- tabs.html -->
<button #meuBotao>Teste</button>
Nesse caso o botão está dentro da view do próprio componente.
Já o ContentChildren serve para pegar elementos que foram passados de fora, entre a abertura e fechamento do componente.
Exemplo:
<app-tabs>
<app-tab title="Perfil">Perfil</app-tab>
<app-tab title="Config">Config</app-tab>
</app-tabs>
Esses app-tab não nasceram dentro do tabs.html.
Eles foram passados pelo componente pai.
Então o correto é usar:
@ContentChildren(Tab)
E não:
@ViewChildren(Tab)
A imagem mental que ficou para mim foi essa:
app-tabs é uma caixa.
Eu coloco várias app-tab dentro dessa caixa.
O @ContentChildren é o jeito da caixa olhar para dentro dela e descobrir:
“quais abas colocaram aqui dentro?”
Esse componente foi muito bacana porque parece simples visualmente, mas por baixo ele ensina bastante coisa importante do Angular:
ng-content
ContentChildren
QueryList
AfterContentInit
componente pai controlando filhos
compound components
renderização condicional
E o mais legal é perceber que esse padrão é muito parecido com componentes famosos que a gente usa no dia a dia, tipo tabs de bibliotecas de UI.
A diferença é que aqui eu consegui montar do zero e entender melhor como esse tipo de componente funciona por baixo dos panos.
Carregando comentários...