Nesse desafio foi construído uma galeria de filmes com busca e ordenação, utilizamos varios conceitos e uso dos signals.
Publicado em 02 de maio de 2026
Nesse desafio eu construí uma galeria de filmes com busca por input em tempo real e ordenação. Embora seja um projeto simples, foi desafiador, pois me deparei com alguns problemas de imutabilidade, que foram corrigidos posteriormente usando Signals.
A estrutura do projeto ficou:
app/
features/gallery/
gallery.component.ts # container
components/
card.component.ts # apresentacional
search-bar.component.ts
sort-select.component.ts
Sendo o gallery o componente pai e os demais componentes filhos.
Comecei pela criação do componente gallery (pai) e do componente card (filho), pois já queria ver os dados refletidos na tela. Sendo assim, eu mockei um array de filmes no TS do gallery:
movies: Movie[] = [
{
id: 1,
title: 'Inception',
duration: 148,
year: 2010,
description: 'Um ladrão que invade sonhos...',
image: 'https://image.tmdb.org/t/p/w500/edv5CZvWj09upOsy2Y6IwDhK8bt.jpg',
isFavorite: false
},
{
id: 2,
title: 'Interstellar',
duration: 169,
year: 2014,
description: 'Exploradores viajam...',
image: 'https://image.tmdb.org/t/p/w500/rAiYTfKGqDCRIIqo664sY9XZIvQ.jpg',
isFavorite: false
}
];
E usei o @for para iterar sobre os elementos:
<section>
<h1>Movies</h1>
@for (movie of movies; track movie.title; let i = $index) {
<app-card [movie]="movie"></app-card>
}
</section>
Daí, no componente card, eu simplesmente renderizei os dados que vêm do @Input:
<div>
<h2>{{ movie.title }}</h2>
<p>Duração: {{ movie.duration }}</p>
<p>Ano: {{ movie.year }}</p>
<p>{{ movie.description }}</p>
<img [src]="movie.image" [alt]="movie.title" style="max-width: 200px;">
<button (click)="onFavorite()">
{{ movie.isFavorite ? 'Favoritado' : 'Adicionar aos Favoritos' }}
</button>
</div>
export class Card {
@Input({ required: true }) movie!: Movie;
@Output() favorite = new EventEmitter<void>();
onFavorite() {
this.favorite.emit();
}
}
Além disso, emiti um evento para favoritar e deixei o input obrigatório.
Até aí tudo tranquilo. O problema veio quando precisei filtrar os filmes.
Ao criar o componente app-search, eu estava filtrando diretamente o array movies e atribuindo o resultado nele. Na prática, eu perdia a lista original.
Estava fazendo a seguinte cagada:
handleTerm(term: string) {
const searchTerm = term.toLowerCase().trim();
this.movies = this.movies.filter((movie) => {
const title = movie.title.toLowerCase().trim();
return title.includes(searchTerm);
});
}
O correto seria:
handleTerm(term: string) {
const searchTerm = term.toLowerCase().trim();
this.filteredMovies = this.movies.filter((movie) => {
const title = movie.title.toLowerCase().trim();
return title.includes(searchTerm);
});
}
Ou seja, precisamos de outro array (filteredMovies) para guardar o resultado da busca, mantendo a fonte original intacta.
Claro que, para isso funcionar, precisei emitir eventos do app-search para o componente pai:
<form [formGroup]="search">
<div>
<label for="search">Busca por nome</label>
<input id="search" type="text" formControlName="searchTerm">
</div>
<button type="button" (click)="handleSearch()">Buscar</button>
<button type="button" (click)="clearSearch()">Limpar</button>
</form>
export class Search implements OnInit {
search: FormGroup;
@Output() onSearch = new EventEmitter<string>();
@Output() textSearch = new EventEmitter<string>();
constructor(private fb: FormBuilder) {
this.search = this.fb.group({
searchTerm: ['']
});
}
ngOnInit(): void {
this.search.get('searchTerm')?.valueChanges.subscribe((value) => {
this.textSearch.emit(value);
});
}
handleSearch() {
const term = this.search.get('searchTerm')?.value;
this.onSearch.emit(term);
}
clearSearch() {
..()?.();
..();
}
}
Depois disso, usei o componente no gallery:
<app-search
(onSearch)="handleTextSearch($event)"
(textSearch)="handleTextSearch($event)">
</app-search>
E no TS:
handleSort(sort: string) {
this.sortBy.set(sort);
}
handleTextSearch(text: string) {
this.searchTerm.set(text);
}
Depois decidi que, para esse aplicativo, o melhor seria o uso dos Signals. Então usei signals no termo de busca, na ordenação e na própria fonte de dados movies.
Agora com o array completo:
movies = signal<Movie[]>([
{
id: 1,
title: 'Inception',
duration: 148,
year: 2010,
description: 'Um ladrão que invade sonhos...',
image: 'https://image.tmdb.org/t/p/w500/edv5CZvWj09upOsy2Y6IwDhK8bt.jpg',
isFavorite: false
},
{
id: 2,
title: 'Interstellar',
duration: 169,
year: 2014,
description: 'Exploradores viajam...',
image: 'https://image.tmdb.org/t/p/w500/rAiYTfKGqDCRIIqo664sY9XZIvQ.jpg',
isFavorite: false
},
{
id: 3,
title: 'The Dark Knight',
duration: 152,
year: 2008,
description: 'Batman enfrenta o Coringa em Gotham.',
image: 'https://image.tmdb.org/t/p/w500/qJ2tW6WMUDux911r6m7haRef0WH.jpg',
isFavorite: false
},
{
id: 4,
title: 'The Matrix',
duration: 136,
year: 1999,
: ,
: ,
:
},
{
: ,
: ,
: ,
: ,
: ,
: ,
:
},
{
: ,
: ,
: ,
: ,
: ,
: ,
:
},
{
: ,
: ,
: ,
: ,
: ,
: ,
:
},
{
: ,
: ,
: ,
: ,
: ,
: ,
:
},
{
: ,
: ,
: ,
: ,
: ,
: ,
:
},
{
: ,
: ,
: ,
: ,
: ,
: ,
:
}
]);
A ideia ficou mais clara: movies guarda a fonte de dados, searchTerm e sortBy guardam o estado da tela, e filteredMovies usa computed() para recalcular automaticamente a lista sempre que algo muda.
filteredMovies = computed(() => {
const term = this.searchTerm().toLowerCase().trim();
const sort = this.sortBy();
let result = this.movies().filter(movie => {
const title = movie.title.toLowerCase().trim();
return title.includes(term);
});
if (sort === 'title') {
result = [...result].sort((a, b) => a.title.localeCompare(b.title));
}
if (sort === 'year') {
result = [...result].sort((a, b) => a.year - b.year);
}
if (sort === 'duration') {
result = [...result].sort((a, b) => a.duration - b.duration);
}
return result;
});
Repare que com os signals usamos set e update, que são métodos principais para manipular estado.
Bom, o post ficou grande, mas achei que o desafio foi legal e quis compartilhar com vocês.
Um grande abraço!
Carregando comentários...