Componentes angulares 101 - uma visão geral

Publicados: 2022-03-11

Os componentes estão disponíveis em Angular desde o início; no entanto, muitas pessoas ainda se encontram usando componentes incorretamente. No meu trabalho, vi pessoas não usando nada, criando componentes em vez de diretivas de atributo e muito mais. Essas são questões que os desenvolvedores Angular juniores e seniores tendem a fazer, inclusive eu. E assim, este post é para pessoas como eu quando eu estava aprendendo Angular, já que nem a documentação oficial nem a documentação não oficial sobre componentes explicam com casos de uso como e quando usar os componentes.

Neste artigo, vou identificar as formas corretas e incorretas de usar componentes Angular, com exemplos. Este post deve lhe dar uma ideia clara sobre:

  • A definição de componentes angulares;
  • Quando você deve criar componentes Angular separados; e
  • Quando você não deve criar componentes Angular separados.

Imagem de capa para componentes Angular

Antes de podermos entrar no uso correto dos componentes Angular, quero tocar brevemente no tópico do componente em geral. De um modo geral, todo e qualquer aplicativo Angular tem pelo menos um componente por padrão - o root component . A partir daí, cabe a nós, como projetar nosso aplicativo. Normalmente, você cria um componente para páginas separadas e, em seguida, cada página contém uma lista de componentes separados. Como regra geral, um componente deve atender aos seguintes critérios:

  • Deve ter uma classe definida, que contém dados e lógica; e
  • Deve ser associado a um modelo HTML que exiba informações para o usuário final.

Vamos imaginar um cenário onde temos um aplicativo que possui duas páginas: Tarefas futuras e Tarefas concluídas . Na página Próximas tarefas , podemos visualizar as próximas tarefas, marcá-las como "concluídas" e, finalmente, adicionar novas tarefas. Da mesma forma, na página Tarefas concluídas , podemos visualizar as tarefas concluídas e marcá-las como “desfeitas”. Por fim, temos links de navegação, que nos permitem navegar entre as páginas. Dito isso, podemos dividir a página a seguir em três seções: componente raiz, páginas, componentes reutilizáveis.

Diagrama de tarefas futuras e tarefas concluídas

Exemplo de wireframe de aplicativo com seções de componentes separadas coloridas

Com base na captura de tela acima, podemos ver claramente que a estrutura do aplicativo seria algo assim:

 ── myTasksApplication ├── components │ ├── header-menu.component.ts │ ├── header-menu.component.html │ ├── task-list.component.ts │ └── task-list.component.html ├── pages │ ├── upcoming-tasks.component.ts │ ├── upcoming-tasks.component.html │ ├── completed-tasks.component.ts │ └── completed-tasks.component.html ├── app.component.ts └── app.component.html

Então, vamos vincular os arquivos do componente com os elementos reais no wireframe acima:

  • header-menu.component e task-list.component são componentes reutilizáveis, que são exibidos com uma borda verde na captura de tela do wireframe;
  • upcoming-tasks.component e completed-tasks.component são páginas que são exibidas com uma borda amarela na captura de tela do wireframe acima; e
  • Por fim, app.component é o componente raiz, que é exibido com uma borda vermelha na captura de tela do wireframe.

Dito isso, podemos especificar lógica e design separados para cada componente. Com base nos wireframes acima, temos duas páginas que reutilizam um componente— task-list.component . A questão surgiria - como especificamos que tipo de dados devemos mostrar em uma página específica? Felizmente, não precisamos nos preocupar com isso, porque ao criar um componente, você também pode especificar variáveis ​​de entrada e saída .

Variáveis ​​de entrada

As variáveis ​​de entrada são usadas dentro dos componentes para passar alguns dados do componente pai. No exemplo acima, poderíamos ter dois parâmetros de entrada para o task-list.componenttasks e listType . Da mesma forma, tasks seria uma lista de strings que exibiria cada string em uma linha separada, enquanto listType seria next ou completed , o que indicaria se a caixa de seleção está marcada. Abaixo, você pode encontrar um pequeno trecho de código de como o componente real pode ficar.

 // task-list.component.ts import { Component, Input } from '@angular/core'; @Component({ selector: 'app-task-list', templateUrl: 'task-list.component.html' }) export class TaskListComponent { @Input() tasks: string[] = []; // List of tasks which should be displayed. @Input() listType: 'upcoming' | 'completed' = 'upcoming'; // Type of the task list. constructor() { } }

Variáveis ​​de saída

Semelhante às variáveis ​​de entrada, as variáveis ​​de saída também podem ser usadas para passar algumas informações entre os componentes, mas desta vez para o componente pai. Por exemplo, para o task-list.component , poderíamos ter a variável de saída itemChecked . Ele informaria ao componente pai se um item foi marcado ou desmarcado. As variáveis ​​de saída devem ser emissores de eventos. Abaixo, você pode encontrar um pequeno trecho de código de como o componente pode ficar com uma variável de saída.

 // task-list.component.ts import { Component, Input, Output } from '@angular/core'; @Component({ selector: 'app-task-list', templateUrl: 'task-list.component.html' }) export class TaskListComponent { @Input() tasks: string[] = []; // List of tasks which should be displayed. @Input() listType: 'upcoming' | 'completed' = 'upcoming'; // Type of the task list. @Output() itemChecked: EventEmitter<boolean> = new EventEmitter(); @Output() tasksChange: EventEmitter<string[]> = new EventEmitter(); constructor() { } /** * Is called when an item from the list is checked. * @param selected---Value which indicates if the item is selected or deselected. */ onItemCheck(selected: boolean) { this.itemChecked.emit(selected); } /** * Is called when task list is changed. * @param changedTasks---Changed task list value, which should be sent to the parent component. */ onTasksChanged(changedTasks: string[]) { this.taskChange.emit(changedTasks); } }
Possível conteúdo task-list.component.ts após adicionar variáveis ​​de saída

Uso de componente filho e vinculação de variável

Vamos dar uma olhada em como usar este componente dentro do componente pai e como fazer diferentes tipos de associações de variáveis. Em Angular, existem duas maneiras de vincular as variáveis ​​de entrada - vinculação unidirecional, o que significa que a propriedade deve estar entre colchetes [] e vinculação bidirecional, o que significa que a propriedade deve ser encapsulada entre colchetes e colchetes [()] . Dê uma olhada no exemplo abaixo e veja as diferentes maneiras pelas quais os dados podem ser passados ​​entre os componentes.

 <h1>Upcoming Tasks</h1> <app-task-list [(tasks)]="upcomingTasks" [listType]="'upcoming'" (itemChecked)="onItemChecked($event)"></app-task-list>
Possível conteúdo upcoming-tasks.component.html

Vamos passar por cada parâmetro:

  • O parâmetro tasks é passado usando ligação bidirecional. Isso significa que, caso o parâmetro tasks seja alterado no componente filho, o componente pai refletirá essas alterações na variável upcomingTasks . Para permitir a vinculação de dados bidirecional, você deve criar um parâmetro de saída, que segue o modelo “[inputParameterName]Change”, neste caso tasksChange .
  • O parâmetro listType é passado usando uma ligação unidirecional. Isso significa que ele pode ser alterado no componente filho, mas não será refletido no componente pai. Lembre-se de que eu poderia ter atribuído o valor 'upcoming' a um parâmetro dentro do componente e passado isso - não faria diferença.
  • Finalmente, o parâmetro itemChecked é uma função de escuta e será chamada sempre que onItemCheck for executado no task-list.component . Se um item estiver marcado, $event manterá o valor true , mas, se estiver desmarcado, manterá o valor false .

Como você pode ver, em geral, o Angular fornece uma ótima maneira de passar e compartilhar informações entre vários componentes, portanto, você não deve ter medo de usá-los. Apenas certifique-se de usá-los com sabedoria e não exagerar.

Quando criar um componente angular separado

Como mencionado anteriormente, você não deve ter medo de usar componentes Angular, mas definitivamente deve usá-los com sabedoria.

Diagrama de componentes angulares em ação

Faça uso inteligente de componentes Angular

Então, quando você deve criar componentes Angular?

  • Você deve sempre criar um componente separado se o componente puder ser reutilizado em vários lugares, como em nosso task-list.component . Nós os chamamos de componentes reutilizáveis .
  • Você deve considerar a criação de um componente separado se o componente tornar o componente pai mais legível e permitir que eles adicionem cobertura de teste adicional. Podemos chamá-los de componentes de organização de código .
  • Você deve sempre criar um componente separado se tiver uma parte de uma página que não precisa ser atualizada com frequência e deseja aumentar o desempenho. Isso está relacionado à estratégia de detecção de alterações. Podemos chamá-los de componentes de otimização .

Essas três regras me ajudam a identificar se preciso criar um novo componente e automaticamente me dão uma visão clara da função do componente. Idealmente, ao criar um componente, você já deve saber qual será sua função dentro do aplicativo.

Como já vimos o uso de componentes reutilizáveis , vamos dar uma olhada no uso de componentes de organização de código . Vamos imaginar que temos um formulário de cadastro e, na parte inferior do formulário, temos uma caixa com Termos e Condições . Normalmente, o juridiquês tende a ser muito grande, ocupando muito espaço – neste caso, em um modelo HTML. Então, vamos dar uma olhada neste exemplo e então ver como podemos mudá-lo.

No início, temos um componente— registration.component —que contém tudo, incluindo o formulário de registro, bem como os próprios termos e condições.

 <h2>Registration</h2> <label for="username">Username</label><br /> <input type="text" name="username" [(ngModel)]="username" /><br /> <label for="password">Password</label><br /> <input type="password" name="password" [(ngModel)]="password" /><br /> <div class="terms-and-conditions-box"> Text with very long terms and conditions. </div> <button (click)="onRegistrate()">Registrate</button>
Estado inicial antes de separar </code>registration.component</code> em vários componentes

O modelo agora parece pequeno, mas imagine se substituíssemos “Texto com termos e condições muito longos” por um pedaço de texto real com mais de 1.000 palavras – isso tornaria a edição do arquivo difícil e desconfortável. Temos uma solução rápida para isso - poderíamos inventar um novo componente terms-and-conditions.component , que conteria tudo relacionado a termos e condições. Então, vamos dar uma olhada no arquivo HTML para os terms-and-conditions.component .

 <div class="terms-and-conditions-box"> Text with very long terms and conditions. </div>
Modelo HTML </code>terms-and-conditions.component</code> recém-criado

E agora podemos ajustar o componente de registration.component e usar o terms-and-conditions.component dentro dele.

 <h2>Registration</h2> <label for="username">Username</label><br /> <input type="text" name="username" [(ngModel)]="username" /><br /> <label for="password">Password</label><br /> <input type="password" name="password" [(ngModel)]="password" /><br /> <app-terms-and-conditions></app-terms-and-conditions> <button (click)="onRegistrate()">Registrate</button>
Modelo </code>registration.component.ts</code> atualizado com componente de organização de código

Parabéns! Acabamos de diminuir o tamanho do registration.component em centenas de linhas e facilitamos a leitura do código. No exemplo acima, fizemos alterações no template do componente, mas o mesmo princípio pode ser aplicado à lógica do componente.

Por fim, para os componentes de otimização , sugiro fortemente que você leia este post, pois ele fornecerá todas as informações necessárias para entender a detecção de alterações e em quais casos específicos você pode aplicá-la. Você não o usará com frequência, mas pode haver alguns casos, e se você puder pular verificações regulares em vários componentes quando não for necessário, é uma vantagem para o desempenho.

Dito isto, nem sempre devemos criar componentes separados, então vamos dar uma olhada quando você deve evitar criar um componente separado.

Quando evitar a criação de um componente angular separado

Existem três pontos principais com base nos quais sugiro criar um componente Angular separado, mas há casos em que eu evitaria criar um componente separado também.

Ilustração de muitos componentes

Muitos componentes irão atrasá-lo.

Novamente, vamos dar uma olhada nos marcadores que permitirão que você entenda facilmente quando não deve criar um componente separado.

  • Você não deve criar componentes para manipulações do DOM. Para esses, você deve usar diretivas de atributo.
  • Você não deve criar componentes se isso tornar seu código mais caótico. Isso é o oposto dos componentes de organização de código .

Agora, vamos dar uma olhada mais de perto e verificar os dois casos em exemplos. Vamos imaginar que queremos ter um botão que registre a mensagem quando ela for clicada. Isso poderia ser um erro e um componente separado poderia ser criado para essa funcionalidade específica, que conteria um botão e ações específicos. Vamos primeiro verificar a abordagem incorreta:

 // log-button.component.ts import { Component, Input, Output } from '@angular/core'; @Component({ selector: 'app-log-button', templateUrl: 'log-button.component.html' }) export class LogButtonComponent { @Input() name: string; // Name of the button. @Output() buttonClicked: EventEmitter<boolean> = new EventEmitter(); constructor() { } /** * Is called when button is clicked. * @param clicked - Value which indicates if the button was clicked. */ onButtonClick(clicked: boolean) { console.log('I just clicked a button on this website'); this.buttonClicked.emit(clicked); } }
Lógica do componente incorreto log-button.component.ts

E, portanto, este componente é acompanhado com a seguinte visualização html.

 <button (click)="onButtonClick(true)">{{ name }}</button>
Modelo de componente incorreto log-button.component.html

Como você pode ver, o exemplo acima funcionaria e você poderia usar o componente acima em todas as suas visualizações. Faria o que você quer, tecnicamente. Mas a solução correta aqui seria usar diretivas. Isso permitiria não apenas reduzir a quantidade de código que você precisa escrever, mas também adicionar a possibilidade de aplicar essa funcionalidade a qualquer elemento que você deseja, não apenas botões.

 import { Directive } from '@angular/core'; @Directive({ selector: "[logButton]", hostListeners: { 'click': 'onButtonClick()', }, }) class LogButton { constructor() {} /** * Fired when element is clicked. */ onButtonClick() { console.log('I just clicked a button on this website'); } }
diretiva logButton , que pode ser atribuída a qualquer elemento

Agora, quando criamos nossa diretiva, podemos simplesmente usá-la em nosso aplicativo e atribuí-la a qualquer elemento que desejarmos. Por exemplo, vamos reutilizar nosso registration.component .

 <h2>Registration</h2> <label for="username">Username</label><br /> <input type="text" name="username" [(ngModel)]="username" /><br /> <label for="password">Password</label><br /> <input type="password" name="password" [(ngModel)]="password" /><br /> <app-terms-and-conditions></app-terms-and-conditions> <button (click)="onRegistrate()" logButton>Registrate</button>
Diretiva logButton usada no botão de formulários de registro

Agora, podemos dar uma olhada no segundo caso, no qual não devemos criar componentes separados, e isso é o oposto dos componentes de otimização de código . Se o componente recém-criado tornar seu código mais complicado e maior, não há necessidade de criá-lo. Vamos tomar como exemplo nosso registration.component . Um desses casos seria criar um componente separado para rótulo e campo de entrada com uma tonelada de parâmetros de entrada. Vamos dar uma olhada nesta má prática.

 // form-input-with-label.component.ts import { Component, Input} from '@angular/core'; @Component({ selector: 'app-form-input-with-label', templateUrl: 'form-input-with-label.component.html' }) export class FormInputWithLabelComponent { @Input() name: string; // Name of the field @Input() id: string; // Id of the field @Input() label: string; // Label of the field @Input() type: 'text' | 'password'; // Type of the field @Input() model: any; // Model of the field constructor() { } }
Lógica do form-input-with-label.component

E esta poderia ser a visão para este componente.

 <label for="{{ id }}">{{ label }}</label><br /> <input type="{{ type }}" name="{{ name }}" [(ngModel)]="model" /><br />
Visualização do form-input-with-label.component

Claro, a quantidade de código seria reduzida em registration.component , mas isso torna a lógica geral do código mais fácil de entender e legível? Acho que podemos ver claramente que isso torna o código desnecessariamente mais complexo do que era antes.

Próximos passos: Componentes angulares 102?

Para resumir: Não tenha medo de usar componentes; apenas certifique-se de ter uma visão clara sobre o que deseja alcançar. Os cenários que listei acima são os mais comuns e os considero os mais importantes e comuns; no entanto, seu cenário pode ser único e cabe a você tomar uma decisão informada. Espero que você tenha aprendido o suficiente para tomar uma boa decisão.

Se você quiser saber mais sobre as estratégias de detecção de alterações do Angular e a estratégia OnPush, recomendo ler Angular Change Detection e a estratégia OnPush. Está intimamente relacionado a componentes e, como já mencionei no post, pode melhorar significativamente o desempenho do aplicativo.

Como os componentes são apenas parte das diretivas que o Angular fornece, seria ótimo conhecer também diretivas de atributo e diretivas estruturais . Compreender todas as diretivas provavelmente tornará mais fácil para o programador escrever um código melhor.