# Diretrizes de desenvolvimento ## Sumário [Design System](#design-system) * [Standalone Components](#standalone-components) * [Organização do Projeto](#organizacao-projeto) * [Lazy-load nas rotas ](#lazy-loading) * [Pré-carregamento de Dados com Resolvers](#resolvers) [Padrões de Nomeclatura](#nomeclatura) [Reatividade](#reatividade) * [RxJS](#rxjs) * [Gerenciamento de Estado e Subjects](#rxjs-gerenciamento-estado) * [Encapsulamento e Compartilhamento de Subjects](#rxjs-compartilhamento-subjects) * [Pureza e Efeitos Colaterais](#rxjs-efeitos-coleterais) * [Gerenciamento de Subscriptions](#rxjs-gerenciamento-subscriptions) * [Combinação de Múltiplos Observables](#rxjs-combinacao-observables) * [Signals](#signals) * [ReactiveForms](#reactive-forms) * [Estrutura e Tipagem](#reactive-forms-estrutura) * [Validação e Feedback ao Usuário](#creactive-forms-validacaos) * [Reatividade](#reactive-forms-reativade) [Componentização](#componentizacao) * [CONTAINER (Smart Component)](#componentizacao-smart-component) * [PRESENTER (Dumb Component - Feature Específica)](#componentizacao-dumb-component) * [SHARED COMPONENT (Dumb Component - Reutilizável)](#componentizacao-shared-component) [Comunicação entre Componentes](#comunicacao-components) * [Comunicação Vertical](#comunicacao-components-vertical) * [Comunicação Horizontal/Global](#comunicacao-components-horizontal) * [Práticas a Evitar](#comunicacao-components-evitar) * [Acesso Direto](#comunicacao-components-evitar-acesso-direto) * [Anti-Performance e Impureza](#comunicacao-components-evitar-acesso-impureza) [Estilização](#estilizacao) * [Tema PrimeNG](#estilizacao-tema) * [Estrutura](#estilizacao-tema-estrutura) * [Palette](#estilizacao-tema-palette) * [Spacing](#estilizacao-tema-spacing) * [Typography](#estilizacao-tema-typography) * [Theme](#estilizacao-tema-theme) * [Components](#estilizacao-tema-components) * [Index](#estilizacao-tema-index) * [Tailwind CSS](#estilizacao-tailwind) [Comunicação com Back-End](#comunicacao-back) * [API Layer](#comunicacao-back-api-layer) * [API Facade Layer](#comunicacao-back-api-facade-layer) * [DTOs de Request](#comunicacao-back-dto-request) * [DTOs de Response](#comunicacao-back-dto-response) * [DTOs internos](#comunicacao-back-dto-interno) [Tratamento de Exceções](#tratamento-excecoes) [Testes](#teste) * [Imports](#testes-imports) * [Boas práticas](#testes-boas-praticas) * [Organização e Nomeclatura](#testes-organizacao-nomeclatura ) [Melhores Práticas de Implementação](#melhores-praticas-de-implementacao) [Glossário](#glossario) ___ ## Design System<a name="design-system"></a> ### Standalone Components<a name="standalone-components"></a> **Deve:** - Criar componentes com `standalone: true` (Padrão na versão 19 do angular). - Desde o Angular 17+, a abordagem standalone components permite eliminar a necessidade de NgModule, simplificando a estrutura e reduzindo acoplamento. Cada feature folder pode conter componentes standalone e importar diretamente dependências específicas, sem módulos intermediários. - Importar dependências diretamente no `imports` do decorador `@Component`. - O gerenciamento de dependências e provedores globais, incluindo as configurações de roteamento e serviços HTTP, deve ser centralizado no array providers do `app.config.ts` as funções auxiliares específicas do Angular, como `provideHttpClient` e `provideRouter`. **Exemplo:** ```ts export const appConfig: ApplicationConfig = { providers: [ provideHttpClient(), provideAnimationsAsync(), provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes) ], }; ``` ___ ## Padrões de nomeclatura<a name="nomeclatura"></a> * A idioma padrão para escrita de nomes de métodos, atributos, classes e arquivos é o inglês. | Elemento | Case / Formato | Convenção e Padrão | Exemplo | | :--- | :--- | :--- | :--- | | **Arquivo** | `kebab-case` | `feature.type.ts` | `user-list.component.ts` | | **Classe** | `PascalCase` | Sufixo correspondente ao tipo. | `UserListComponent` | | **Interface** | `PascalCase` | Nome da entidade (com prefixo `I`). | `interface IUser { ... }` | | **Enum** | `PascalCase` | Nome do conjunto de valores. | `enum StatusOrder { ... }` | | **Atributo (Público)** | `camelCase` | Substantivo descritivo. | `username: string;` | | **Atributo (Privado)** | `_camelCase` | Prefixo `_` uso interno. | `private _selectedItems: Item[];` | | **Atributo (Observable)** | `camelCase$` | Sufixo `$` para indicar um stream. | `users$: Observable<Users[]>;` | | **Constante de Classe** | `UPPER_SNAKE_CASE` | Para valores estáticos imutáveis. | `static readonly API_URL = '...';` | | **Método (Público)** | `camelCase()` | Verbo ou frase verbal. | `getData()` | | **Método (Privado)** | `_camelCase()` | Prefixo `_` para uso interno. | `private _validateForm()` | | **Método (Handler)** | `on` + `NomeEvento` | Responde a eventos. | `onClick(event: Event)` | | **Booleano** | `is/has/should` | Prefixo para clareza. | `isVisible: boolean;` | ___ ### Organização do Projeto (Folder-by-Feature) ?<a name="organizacao-projeto"></a> - Usar nomes individuais para arquivos individuais como `cidadao.service.ts` - Plurais para agrupadores de multiplos itens com o `cidadaos/` #### 📁 app/core Contém todos os **recursos essenciais da aplicação**, carregados uma única vez no `bootstrap`. **Inclui:** - **services/** – serviços globais (autenticação, logger, configuração da API, etc.); - **interceptors/** – interceptadores HTTP (ex: AuthInterceptor, LoadingInterceptor); - **guards/** – rotas protegidas (`AuthGuard`, `RoleGuard`); - **layout/** – componentes de layout global (navbar, sidebar, footer); - **config/** – constantes e tokens de injeção global (`API_URL`, `ENVIRONMENT`). > 🔹 O módulo configurado no `app.config.ts`. #### 📁 app/features Agrupa cada **módulo funcional** da aplicação de forma isolada. Cada subpasta representa uma **feature independente**, com suas próprias **rotas, componentes, serviços e estado**. **Exemplo: `/features/users`** ``` src/ ├── app/ │ ├── core/ # Módulo central (singleton services e interceptors) │ │ ├── guards/ │ │ │ ├── auth.guard.ts │ │ │ ├── role.guard.ts │ │ ├── interceptors/ │ │ │ ├── auth.interceptor.ts │ │ │ ├── error-handler.interceptor.ts │ │ ├── services/ │ │ │ ├── logging.service.ts │ │ │ ├── auth.service.ts │ │ ├── config/ │ │ │ ├── app.config.ts │ │ └── core.providers.ts │ │ │ ├── shared/ # Componentes, diretivas e pipes reutilizáveis │ │ ├── components/ │ │ │ ├── button/ │ │ │ │ ├── button.component.ts │ │ │ │ ├── button.component.html │ │ │ │ └── button.component.scss │ │ ├── pipes/ │ │ │ ├── cpf-mask.pipe.ts │ │ ├── directives/ │ │ │ ├── autofocus.directive.ts │ │ └── utils/ │ │ └── date.util.ts │ │ │ ├── features/ # Áreas funcionais do sistema │ │ ├── cidadaos/ │ │ │ ├── pages/ │ │ │ │ ├── cidadao-list/ │ │ │ │ │ ├── cidadao-list-table/ │ │ │ │ │ │ ├── cidadao-list-table.presenter.ts │ │ │ │ │ │ ├── cidadao-list-table.presenter.html │ │ │ │ │ │ └── cidadao-list-table.presenter.scss │ │ │ │ │ ├── cidadao-list.container.ts │ │ │ │ │ ├── cidadao-list.container.html │ │ │ │ │ ├── cidadao-list.container.scss │ │ │ │ │ └── cidadao-list.routes.ts │ │ │ │ │ │ │ ├── services/ │ │ │ │ └── cidadao.service.ts │ │ │ ├── models/ │ │ │ │ └── cidadao.model.ts │ ├── app.routes.ts # Configuração das rotas standalone │ ├── app.config.ts # Configuração global (bootstrapApplication) │ └── main.ts ``` > 🔹 Facilita o **lazy loading** e o isolamento de responsabilidades. > 🔹 Cada feature pode evoluir ou ser removida sem quebrar o restante da aplicação. --- #### 📁 app/shared Contém **elementos reutilizáveis** entre múltiplos módulos e recursos. **Inclui:** - **components/** – componentes genéricos (ex: `ModalComponent`, `LoadingSpinner`); - **pipes/** – pipes de uso geral (`formatDate`, `maskCpf`); - **directives/** – diretivas compartilhadas (`debounceClick`, `autoFocus`); - **utils/** – helpers e funções utilitárias puras; > 🔹 Nenhum componente aqui deve depender de um “feature” específico. --- ### Lazy-load nas rotas<a name="lazy-loading"></a> - Carregar módulos da aplicação sob demanda (apenas quando o usuário acessa uma rota específica) e não no carregamento inicial. Isso reduz o tamanho do bundle inicial e melhora o tempo de carregamento da aplicação. **Deve**: - Utilizar **loadComponent** para carregar **Componentes** e **loadChildren** para carregar **Arquivos que definem as rotas de módulos**. ```ts export const routes: Routes = [ { path: Rotas.CLIENTES.listagem, loadChildren: () => import('./features/clientes/cliente.routes').then((routes) => routes.default) }, { path: Rotas.LOGIN.login, loadChildren: () => import('./features/login/login.routes').then((routes) => routes.default) }, { path: '', redirectTo: Rotas.CLIENTES.listagem, pathMatch: 'full', }, ]; ``` ### Diretrizes de Pré-carregamento de dados<a name="resolvers"></a> **DEVE:** - Utilizar Resolvers para **pré-carregar dados essenciais** para a renderização de um componente antes que a navegação para a rota seja concluída. **NÃO DEVE:** - Colocar lógica de negócio complexa ou chamadas excessivamente demoradas em um Resolver, pois isso pode bloquear a navegação e dar a impressão de que a aplicação está travada. **Exemplo de Uso:** - Criar a função Resolver: ```ts export const produtoDetalheResolver: ResolveFn<ProdutoDto | null> = (route, state) => { const produtoService = inject(ProdutoService); const produtoId = route.paramMap.get('id'); if (!produtoId) { return of(null); } return produtoService.getById(+produtoId).pipe( catchError(error => { console.error('Falha ao buscar produto no resolver:', error); // Redirecionar para uma página de erro return of(null); }) ); }; ``` - Aplicar o resolver na rota: ```ts export const PRODUTOS_ROUTES: Routes = [ { path: ':id', component: ProdutoDetalheComponent, resolve: { // A chave 'produto' será usada para acessar o dado resolvido produto: produtoDetalheResolver } } ]; ``` - Acessar o dado no componente: ```ts export class ProdutoDetalheComponent { private _route = inject(ActivatedRoute); // Acessando o dado resolvido de forma reativa protected produto$: Observable<ProdutoDto | null> = this._route.data.pipe( map(data => data['produto']) ); // Acessando o dado resolvido de forma instantânea protected produto: <ProdutoDto | null> = this._route.snapshot.data['produto'] } ``` --- ## Diretrizes de Reatividade<a name="reatividade"></a> ### RxJS<a name="reatividade"></a> O **RxJS** (Reactive Extensions for JavaScript) DEVE ser utilizado em cenários de **manipulação de streams de dados assíncronos** ou para a coordenação de eventos baseada em tempo. **Nomenclatura:** `usuarioLogado$` #### Gerenciamento de Estado e Subjects<a name="rxjs-gerenciamento-estado"></a> | Elemento | DEVE | Contexto/Razão |**Exemplo de Uso Prático**| | :--- | :--- | :--- | :--- | | **Observable** | Utilizar para expor **streams de dados assíncronos** que são **somente leitura**. | <br>É o contrato fundamental de reatividade e promovem a imutabilidade para os consumidores. | Serviço que fornece uma lista de usuários obtida via `HttpClient.get()`.| | **BehaviorSubject** | Utilizar para gerenciar estados que **precisam de um valor inicial**. | Essencial para estados globais, pois garante que novos assinantes recebam o estado atual imediatamente. | <br>State de autenticação que guarda o usuário logado.| | **Subject** | Utilizar para eventos sem a necessidade de um estado inicial. | Ideal para eventos efêmeros (ex: cliques) onde apenas os assinantes ativos no momento da emissão devem receber o valor. | <br>Disparar evento quando um modal deve ser fechado. <br>| **Exemplos:** - Observable: ```ts protected users$: Observable<Array<User>> = this.http.get<Array<User>>('/api/users') ``` - Subject: ```ts const closeModal$ = new Subject<void>(); protected fecharModal(): void { this.closeModal$.next(); } ``` - BehaviorSubject: ```ts private userSubject = new BehaviorSubject<User | null>(null); public login(user: User): void { this.userSubject.next(user); } ``` #### Encapsulamento e Compartilhamento de Subjects<a name="rxjs-compartilhamento-subjects"></a> Para garantir o encapsulamento do estado (impedindo que componentes externos emitam valores): **DEVE** - Expor `Subject` e `BehaviorSubject` para o exterior **somente** após a conversão em um `Observable`, somente leitura. * **Método:** Utilizar o operador **`asObservable()`**. ```ts private _isLoading = new BehaviorSubject<boolean>(false); public isLoading$: Observable<boolean> = this._isLoading.asObservable(); ``` - Considerar a conversão de *Observables* que representam estado persistente para **Signals** utilizando **`toSignal()`**. ```ts protected loggedUser = toSignal(this.authService.loggedUser$, { initialValue: null }); ``` #### Pureza e Efeitos Colaterais<a name="rxjs-efeitos-coleterais"></a> **DEVE** - Utilizar o operador **`tap()`** exclusivamente para realizar efeitos colaterais (*side-effects*), como logs, debug ou chamadas não reativas, sem modificar o *stream* principal de dados. ```ts public login(email: string, password: string): Observable<LoginResponse> { return this.http.post<LoginResponse>(this.API, { email, password }).pipe( tap({ next: (response) => { localStorage.setItem('auth_token', response.token); console.log('Token armazenado com sucesso!'); }, error: (err) => console.error('Falha no login:', err) }) ); } ``` #### Gerenciamento de Subscriptions (Cancelamento)<a name="rxjs-gerenciamento-subscriptions"></a> - **SEMPRE** cancelar a inscrição (unsubscribe) de **Observables** que não completam naturalmente, a fim de prevenir **vazamentos de memória**. | Local de Uso | Método Preferencial | Detalhe | | :--- | :--- | :--- | | **Template (HTML)** | **AsyncPipe ($\text{Pipe Assíncrono}$)** | **MÉTODO PREFERENCIAL.** Lida automaticamente com a inscrição e o cancelamento no ciclo de vida do componente. | **Componentes/Serviços** | **`toSignal()`** | Cria um Signal reativo a partir de um Observable. <br>A **subscription é limpa automaticamente** quando o componente é destruído.<br>| | | **Componentes/Serviços** | Operadores do RxJS: **`takeUntil(destroy$)`** | Ideal para cancelar a inscrição na destruição do componente. <br>Permite cancelar várias inscrições ao mesmo tempo <br>| | **Componentes/Serviços** | Operadores do RxJS: **`take(N)`** | Ideal para chamadas HTTP ou outras operações que completam após um número (N) de emissões. | **Componentes/Serviços** | Operadores do RxJS: **`takeUntilDestroyed(destroyRef)`** | **Método moderno para Angular >=16.** Cancela automaticamente a inscrição quando o componente ou diretiva é destruído, sem necessidade de criar um `Subject` manualmente. **Exemplos:** * **AsyncPipe:** ```ts //ts public loading$: Observable<boolean> = this._loadingService.loading$; //html @if(loading$ | async){ <p-progress-spinner ariaLabel="loading" /> } ``` * **toSignal():** ```ts //ts protected weather = toSignal(this.weatherService.getWeather(), { initialValue: 'Carregando...' }); //html {{ weather() }} ``` * **takeUntil(destroy$):** ```ts export class TimerComponent implements OnDestroy { private _destroy$ = new Subject<void>(); private _counter = 0; constructor() { interval(1000) .pipe( tap(() => console.log('tick')), takeUntil(this._destroy$) ) .subscribe((n) => (this._counter = n)); } ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); } } ``` * **take(N):** ```ts public buscarUsuarios(): Observable<Array<User>> { return this.http.get<User[]>('/api/users').pipe(take(1)) } ``` * **takeUntilDestroyed(destroyRef):** ```ts import { destroyRef, inject } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; const destroyRef = inject(DestroyRef); observable$.pipe(takeUntilDestroyed(destroyRef)).subscribe(value => console.log(value)); ``` #### Combinação de Múltiplos Observables<a name="rxjs-combinacao-observables"></a> **DEVE:** - Utilizar operadores de combinação como `forkJoin`, `combineLatest` ou `zip` para orquestrar a execução de múltiplos Observables de forma declarativa. **NÃO DEVE:** - Aninhar subscribes (callback hell). | Operador | Caso de Uso Principal | Comportamento da Emissão | | :--- | :--- | :--- | | **forkJoin** | Executar **requisições HTTP paralelas e independentes** onde você precisa aguardar a conclusão de **todas** elas para prosseguir. | Emite um único valor (um array com o último valor de cada stream) somente **quando todos** os Observables de entrada completam. Se algum falhar, o `forkJoin` falha imediatamente. | **combineLatest** | Cenários de reatividade onde você precisa de um valor combinado sempre que **qualquer um** dos Observables de entrada emitir um novo valor (após todos terem emitido pelo menos uma vez). | Emite um valor combinado (array) sempre que um dos Observables emite.| | | **zip** | Quando é necessário combinar emissões de múltiplos Observables em pares, na ordem em que foram emitidas. | Emite um valor combinado (array) apenas quando cada Observable de entrada emitiu um valor no mesmo índice.| **Exemplos:** - **forkJoin:** ```ts export class DashboardComponent implements OnInit { private usuarioService = inject(UsuarioService); private permissoesService = inject(PermissoesService); private produtosService = inject(ProdutosService); protected dadosTela = toSignal( this.carregarDadosIniciais(), { initialValue: { usuario: null, permissoes: [], produtos: [] } } ); private carregarDadosIniciais(): Observable<{ usuario: any; permissoes: any[]; produtos: any[] }> { return forkJoin({ usuario: this.usuarioService.getUsuarioLogado(), permissoes: this.permissoesService.getPermissoes(), produtos: this.produtosService.getProdutosDestaque() }).pipe( map(({ usuario, permissoes, produtos }) => { // A lógica só executa quando todas as 3 chamadas retornarem com sucesso console.log('Todos os dados foram carregados com sucesso!'); return { usuario, permissoes, produtos }; }), catchError(error => { console.error('Ocorreu um erro ao carregar dados iniciais:', error); return of({ usuario: null, permissoes: [], produtos: [] }); }) ); } } ``` - **combineLatest:** ```ts const precos = { 'Notebook': 5000, 'Mouse': 150, 'Teclado': 300 }; const taxasCambio = { 'BRL': 1, 'USD': 0.18, 'EUR': 0.17 }; const produtoSelecionado$ = new BehaviorSubject<string>('Notebook'); const moedaSelecionada$ = new BehaviorSubject<string>('BRL'); const precoFinal$ = combineLatest([ produtoSelecionado$, moedaSelecionada$ ]).pipe( map(([produto, moeda]) => { const precoBase = precos[produto]; const taxa = taxasCambio[moeda]; const precoConvertido = (precoBase * taxa).toFixed(2); console.log(`Calculando: Produto=${produto}, Moeda=${moeda}`); return `${moeda} ${precoConvertido}`; }) ); ``` - **zip:** ```ts const cliques$ = fromEvent(document, 'click'); const teclasPressionadas$ = fromEvent(document, 'keydown').pipe( map((event: KeyboardEvent) => event.key) // Extrai apenas o nome da tecla ); const combinacaoCliqueTecla$ = zip( cliques$, teclasPressionadas$ ); combinacaoCliqueTecla$.subscribe(([eventoClique, nomeTecla]) => { console.log(`Clique registrado enquanto a tecla "${nomeTecla}" havia sido a última pressionada.`); }); ``` ___ ### Signals<a name="signals"></a> - Os **Signals** são primitivas de reatividade simples e síncronas que garantem a propagação de mudanças de maneira eficiente e rastreável. **DEVE:** - Ser a **opção preferencial** para estados reativos **locais** de um componente e para **leituras síncronas** no *template*. - Utilizar a função `signal()` para gerenciar o estado mutável principal. - Utilizar **`computed()`** para derivar valores de outros *Signals* de forma **declarativa e memorizada** (só recalcula quando as dependências mudam). - Utilizar a função **`effect()`** exclusivamente para lidar com **efeitos colaterais** (side-effects) que não podem ser expressos no fluxo de dados. **Exemplos:** ```ts //Singal protected count: Signal<number> = signal(0); protected increment(): void { this.count.update((value) => value + 1); } protected reset(): void { this.count.set(0); } // Computed protected unitPrice = signal(50); protected quantity = signal(1); // Valor derivado (recalcula apenas quando quantity ou unitPrice mudam) protected total = computed(() => this.unitPrice() * this.quantity()); public increase(): void { this.quantity.update((q) => q + 1); } public decrease(): void { this.quantity.update((q) => Math.max(1, q - 1)); } //effect protected theme = signal(localStorage.getItem('theme') || 'light'); constructor() { // Side-effect: salvar no localStorage quando o theme muda effect(() => { const current = this.theme(); localStorage.setItem('theme', current); console.log(`Tema atualizado para: ${current}`); }); } toggleTheme() { this.theme.update((t) => (t === 'light' ? 'dark' : 'light')); } ``` ___ ### ReactiveForms (FormBuilder, FormGroup e Validators)<a name="reactive-forms"></a> Os Formulários Reativos (Reactive Forms) **DEVEM** ser a escolha padrão para a gestão de dados complexos, validações e reatividade em formulários. #### Estrutura e Tipagem<a name="design-system"></a> * **DEVE:** - Utilizar o **`FormBuilder`** (ou `NonNullableFormBuilder`) para construir **`FormGroup`**, **`FormControl`** e **`FormArray`**, garantindo concisão e legibilidade. ```ts export class UsuarioFormComponent { formUsuario!: FormGroup; private _fb = inject(NonNullableFormBuilder); constructor() { this.buildForm(); } private buildForm(): void { this.formUsuario = this._fb.group<IFormUsuario>({ nome: this._fb.control('', { validators: [Validators.required] }), email: this._fb.control('', { validators: [Validators.required, Validators.pattern('[0-9]{10}')]}), age: this._fb.control(0, { validators: [Validators.required, Validators.min(20),Validators.max(50)]}), }); } public salvar(): void { const formClienteValue: IFormUser = this.formCliente.getRawValue(); } } ``` - Tipar o `FormGroup` utilizando uma **Interface** ou **Type** que reflita a estrutura do formulário. Isso garante segurança de tipo ao acessar e manipular os valores, com a criação do type ou interface seguindo o padrão de nomeclatura abaixo: **Nomenclatura:** `IFormUsuario` ou `FormUsuario` ```ts interface IFormUsuario { nome: string; email: string; age: number; } ``` - A lógica de construção do `FormGroup` deve ser encapsulada em um **método privado** do componente (ex: `this.buildForm()`) ou isolada em um **serviço dedicado** para formulários complexos. - Utilizar **`FormArray`** para a gestão de listas dinâmicas e repetíveis de controles ou grupos de controles. #### Validação e Feedback ao Usuário<a name="design-system"></a> **DEVE:** - Criar **Validadores customizados** quando a lógica de validação de um campo não puder ser atendida pelos validadores padrão disponibilizados pelo framework. ```ts import { AbstractControl, ValidationErrors } from "@angular/forms"; export const dataNascimento = (controle: AbstractControl): ValidationErrors | null => { let dataNascimentoFutura = 'A data de nascimento não pode ser futura.'; let dataNascimentoAnteriorA1900 = 'Não deve ser anterior a 1900.'; const dataNascimento = controle.value; const erros: ValidationErrors = {}; if (!dataNascimento) { return null; } const hoje = new Date(); if (dataNascimento > hoje) { erros['dataInvalidaFuturo'] = dataNascimentoFutura; } const ano1900 = new Date(1900, 0, 1); if (dataNascimento < ano1900) { erros['dataNascimentoAnteriorA1900'] = dataNascimentoAnteriorA1900; } return Object.keys(erros).length ? erros : null; } ``` - Utilizar **Validadores Assíncronos** (Async Validators) para verificações que dependem de operações assíncronas (ex: checagem de nome de usuário via API). ```ts import { inject } from '@angular/core'; import { AbstractControl, AsyncValidatorFn, ValidationErrors } from "@angular/forms"; import { Observable, of } from "rxjs"; import { debounceTime, switchMap, map, catchError, first } from "rxjs/operators"; import { CpfService } from "../services/cpf.service"; export const cpfJaExisteValidator: AsyncValidatorFn = ( controle: AbstractControl ): Observable<ValidationErrors | null> => { const cpfService = inject(CpfService); if (!controle.value || controle.invalid) { return of(null); } return controle.valueChanges.pipe( debounceTime(500), first(), switchMap(cpf => cpfService.verificarCpf(cpf).pipe( map(response => { return response.emUso ? { cpfJaExiste: 'Este CPF já está cadastrado.' } : null; }), catchError(() => of(null)) ) ) ); }; ``` #### Reatividade<a name="design-system"></a> **DEVE:** - Utilizar **`valueChanges`** ou **`statusChanges`** em um `FormControl` para implementar lógica reativa complexa (ex: habilitar ou desabilitar campos, ou alterar a lista de validadores, com base no valor de outro campo). > 💡 **Importante:** > Sempre utilize uma estratégia para **encerrar a inscrição (`unsubscribe`)** das observables de `valueChanges` e `statusChanges`, pois essas streams não se completam automaticamente. Recomenda-se o uso de **`DestroyRef`** com o operador **`takeUntilDestroyed()`** — que realiza o unsubscribe automaticamente quando o componente é destruído, evitando **vazamentos de memória**. ```ts export class UsuarioFormComponent { private _fb = inject(FormBuilder); protect formUsuario!: FormGroup; private _destroyRef = inject(DestroyRef); constructor() { this.buildForm(); } private buildForm(): void { this.formUsuario = this._fb.group({ nome: this._fb.control('', { validators: [Validators.required] }), doc: this._fb.control(CPF,{ validators: [Validators.required] } ) email: this._fb.control('', { validators: [Validators.required, Validators.pattern('[0-9]{10}')]}), age: this._fb.control(0, { validators: [Validators.required, Validators.min(20),Validators.max(50)]}), }); } } private criarEventosFormulario(): void { this.form.controls.tipo.valueChanges.pipe(takeUntilDestroyed(this._destroyRef)).subscribe((docType) => { const doc = this.form.controls.doc; if (tipo === 'CPF') { doc.addValidators(cpfValidator); } else { doc.clearValidators(); } doc.updateValueAndValidity(); }); } } ``` --- ## Diretrizes de Componentização<a name="componentizacao"></a> - Container/Presenter/Component Pattern ### CONTAINER (Smart Component)<a name="componentizacao-smart-component"></a> **Nomenclatura:** `cadastro-inscricao-divida.container.ts` **O que DEVE ter:** - Injeção de Services (DividaService, DevedorService, etc) - Lógica de negócio e validações complexas - Chamadas HTTP (via services) - Gerenciamento de estado (RxJS, NgRx, Signals) - Navegação entre rotas - Manipulação de formulários complexos **O que NÃO deve ter:** - Estilização complexa (delegar para presenters) - Lógica de apresentação pura - Conhecimento de estrutura HTML detalhada **Exemplo:** ```ts export class DividasContainer implements OnInit { private dividaService = inject(DividaService); private devedorService = inject(DevedorService); private router = inject(Router); private refresh$ = new BehaviorSubject<void>(undefined); private isLoading$ = new BehaviorSubject<boolean>(false); dividas$: Observable<DividaDto[]>; devedores$: Observable<DevedorDto[]>; ngOnInit(): void { this.devedores$ = this.devedorService.getTodos(); this.dividas$ = this.refresh$.pipe( tap(() => this.isLoading$.next(true)), switchMap(() => this.dividaService.getTodas()), tap(() => this.isLoading$.next(false)) ); } onSalvar(form: FormGroup): void { this.isLoading$.next(true); this.dividaService.criar(form.value).subscribe({ next: () => { alert('Dívida salva com sucesso!'); form.reset(); this.refresh$.next(); }, error: (err) => { alert('Erro ao salvar dívida.'); console.error(err); this.isLoading$.next(false); } }); } onEditar(divida: DividaDto): void { this.router.navigate(['/dividas/editar', divida.id]); } onExcluir(id: number): void { if (confirm('Tem certeza que deseja excluir esta dívida?')) { this.isLoading$.next(true); this.dividaService.excluir(id).subscribe(() => { alert('Dívida excluída com sucesso!'); this.refresh$.next(); }); } } } ``` ### PRESENTER (Dumb Component - Feature Específica)<a name="componentizacao-dumb-component"></a> **Nomenclatura:** `cadastro-inscricao-divida-list.presenter.ts` **O que DEVE ter:** - Decoradores @Input() para receber dados - Decoradores @Output() para emitir eventos - Lógica de apresentação simples (formatação, condicionais visuais) - Validações de UI (não de negócio) - Responsividade e acessibilidade **O que NÃO deve ter:** - Injeção de Services de negócio - Chamadas HTTP diretas - Lógica de negócio complexa - Navegação entre rotas - Manipulação de estado global **Exemplo:** ```ts export class GerenciarDividasPresenter implements OnInit { @Input() dividas: DividaDto[] | null = []; @Input() devedores: DevedorDto[] | null = []; @Input() isLoading = false; @Output() salvarDivida = new EventEmitter<FormGroup>(); @Output() editarDivida = new EventEmitter<DividaDto>(); @Output() excluirDivida = new EventEmitter<number>(); private fb = inject(FormBuilder) formDivida: FormGroup; colunasTabela: ColunaTabela[] = [ { chave: 'id', rotulo: '#' }, { chave: 'nomeDevedor', rotulo: 'Devedor' }, { chave: 'valor', rotulo: 'Valor (R$)' } ]; ngOnInit(): void { this.formDivida = this.fb.group({ devedorId: [null, [Validators.required]], valor: [null, [Validators.required, Validators.min(0.01)]], descricao: [''] }); } ... } ``` ```html <form [formGroup]="formDivida" (ngSubmit)="onSubmit()"> <h2>Cadastrar Nova Dívida</h2> <select formControlName="devedorId"> <option [ngValue]="null" disabled>Selecione o devedor</option> <option *ngFor="let devedor of devedores" [value]="devedor.id"> {{ devedor.nome }} </option> </select> <input type="number" formControlName="valor" placeholder="Valor da Dívida"> <button type="submit" [disabled]="isLoading"> {{ isLoading ? 'Salvando...' : 'Salvar Dívida' }} </button> </form> <hr> <h2>Dívidas Cadastradas</h2> <div *ngIf="isLoading" class="spinner">Carregando...</div> ``` ### SHARED COMPONENT (Dumb Component - Reutilizável)<a name="componentizacao-shared-component"></a> **Nomenclatura:** `tabela-dados.component.ts` Componentes genéricos que podem ser usados em múltiplas features do projeto. **O que DEVE ter:** - Decoradores @Input() para receber dados - Decoradores @Output() para emitir eventos - Altamente configurável e genérico - NÃO conhece DTOs ou domínio de negócio - Lógica de apresentação agnóstica de contexto - Validações de UI genéricas - Responsividade e acessibilidade - Documentação clara de uso (JSDoc/Storybook) **O que NÃO deve ter:** - Injeção de Services de negócio - Chamadas HTTP diretas - Lógica de negócio complexa - Navegação entre rotas - Manipulação de estado global - Conhecimento de DTOs específicos (use tipos genéricos) - Dependência de features específicas ```ts export class BotaoPrimarioComponent { @Input() texto: string = 'Clique-me'; @Input() tipo: 'button' | 'submit' | 'reset' = 'button'; @Input() carregando: boolean = false; @Input() desabilitado: boolean = false; @Output() clique = new EventEmitter<void>(); onClick(): void { if (!this.carregando && !this.desabilitado) { this.clique.emit(); } } } ``` ```html <button [type]="tipo" [disabled]="carregando || desabilitado" (click)="onClick()" class="botao-primario"> <span *ngIf="carregando" class="spinner"></span> <span *ngIf="!carregando">{{ texto }}</span> </button> ``` --- ## Diretrizes de Comunicação entre Componentes<a name="comunicacao-components"></a> A comunicação entre componentes **DEVE** ser clara, reativa e seguir o princípio do **fluxo de dados unidirecional** sempre que possível, garantindo baixo acoplamento e alta manutenibilidade. ### Comunicação Vertical (Pai $\leftrightarrow$ Filho)<a name="comunicacao-components-vertical"></a> | Fluxo | Mecanismo DEVE | Detalhes e Boas Práticas | | :--- | :--- | :--- | | **Pai → Filho** | **Inputs** (`@Input()` ou `input()`) | **PREFERIR** a função `input()` (baseada em **Signals**), que é o padrão moderno do Angular. Utilizar para passar **dados simples** ou **configurações** do componente pai para o filho. | | **Filho → Pai** | **Outputs** (`@Output()` $\rightarrow$ `EventEmitter`) | O `Output` DEVE ser utilizado para **emitir eventos** quando uma ação ocorre no filho. Emitir **dados tipados e brutos**, evitando lógica de negócios complexa. | **Exemplo:** * Pai → Filho: ```ts //TS componente pai export class ListagemClienteComponent { public clientes: WritableSignal<Array<ClienteListagemDto>> = signal([]); ... } //html componente pai //Propriedade de input <tabela-cliente [clientes]="clientes()" /> // TS componente filho export class TabelaClienteComponent { public clientes = input<Array<ClienteListagemDto>>(); ... } ``` * Filho → Pai: ```ts export class FiltrosClienteComponent{ public onPesquisar = output<IFormCliente>(); protected form!: FormGroup<IFormCliente>; public pesquisar(): void { const formValue = this.form.getRawValue(); this.onPesquisar.emit(formValue); }) } ... } //html componente pai <filtros-cliente (onPesquisar)="pesquisar($event)" /> //TS componente pai ... public pesquisar(fitros: IFormCliente): void { this._apiService.pesquisar(fitros).pipe(take(1)).subscribe((response ) => {...}); } ... ``` ### Comunicação Horizontal (Mesmo Nível e Global)<a name="comunicacao-components-horizontal"></a> * **Serviço de Estado:** **DEVE** ser utilizado um **Serviço de Estado** injetável (`StateService`) quando os componentes não têm uma relação direta de Pai/Filho. * **Reatividade:** O `StateService` DEVE implementar a reatividade utilizando: * **BehaviorSubject** (RxJS) para armazenar o estado global de forma reativa. * **Signals** para expor e gerenciar o estado reativo compartilhado. * **Serviços de API:** **DEVE**-se usar serviços injetáveis para toda comunicação com APIs externas, isolando o componente da lógica de *fetch* e manipulação de dados brutos. ```ts export class UsuarioLogadoService { private _usuarioLogado: BehaviorSubject<UsuarioLogadoDto> = new BehaviorSubject<UsuarioLogadoDto>(new UsuarioLogadoDto({})); public adicionarUsuarioLogado(usuarioLogado: UsuarioLogadoDto): void { this._usuarioPreLogado.next(usuarioLogado); } public get usuarioLogado$(): Observable<UsuarioLogadoDto> { return this._usuarioLogado.asObservable(); } } ``` ### Práticas a Evitar (Antipatterns)<a name="comunicacao-components-evitar"></a> #### Excesso de camadas de interação * Evite criar longas cadeias de comunicação vertical (@Input e @Output) através de múltiplos níveis hierárquicos. * Quando houver mais de um nível de distância entre o componente que origina a ação e o que a consome, DEVE ser utilizado um Serviço de Estado (State Service) para desacoplá-los. **Exemplo**: `pagina-perfil (Avô)` → `detalhes-usuario` (Pai) → `card-endereco-usuario` (Filho) ```html <app-avô (evento)="aoOcorrer($event)"> <app-pai (evento)="aoOcorrer($event)"> <app-filho (evento)="aoOcorrer($event)"> ... </app-filho> </app-pai> </app-avô> ``` * O componente `card-endereco-usuario` possui o método excluir endereço, que cria um evento repassado para `detalhes-usuario` e depois para`pagina-perfil (Avô)` #### Emissão de Objetos Complexos sem Tipagem * Emitir eventos com objetos genéricos (any) ou estruturas complexas não tipadas, causa perda de segurança de tipo e quebra fácil após refatorações. ```ts @Output() usuarioAlterado = new EventEmitter<any>(); this.usuarioAlterado.emit({ nome: 'Teste', idade: 30 }); ``` #### Acesso Direto (Alto Acoplamento)<a name="comunicacao-components-evitar-acesso-direto"></a> * **Evitar o Uso de `ViewChild` e `ViewChildren`:** * Acessar elementos ou instâncias de componentes filhos diretamente com **`ViewChild`** **QUEBRA** o encapsulamento e cria um **acoplamento forte** entre os componentes. ```ts @ViewChild(FiltroComponent) filtro!: FiltroComponent; aplicarFiltro() { this.filtro.atualizarLista(); // chamada direta de método interno } ``` #### Anti-Performance e Impureza<a name="comunicacao-components-evitar-acesso-impureza"></a> * **Evitar o Acesso a Métodos de Serviço no Template:** * Chamar métodos de serviço diretamente no template (ex: `{{ service.getDados() }}`) pode causar **processamento desnecessários** ou chamadas de API repetidas durante o ciclo de detecção de mudanças. * A lógica DEVE ser movida para o componente (`.ts`), e os dados devem ser expostos como **Observable (AsyncPipe)** ou **Signal**. --- ## Diretrizes de Estilização<a name="estilizacao"></a> ### Tema PrimeNG<a name="estilizacao-tema"></a> - Adotar uma arquitetura modular de design tokens e temas, centralizando cores, tipografia, espaçamentos e componentes personalizados. - Preferir a personalização é feita a partir dos design tokens do PrimeNG. #### Estrutura<a name="estilizacao-tema-estrutura"></a> ``` src/ └── styles/ ├── theme.scss # Tema global: gera variáveis CSS e aplica fontes, espaçamentos e cores. ├── components/ │ ├── index.scss # Ponto de entrada para os estilos de componentes. (Expõe o CSS para o thema) │ ├── buttons/ # Estilos específicos de botões. │ ├── form/ # Campos de formulário. │ ├── tooltips/ # Tooltips customizados. │ └── message/ # Estilos de mensagens de validação ou feedback. └── variables/ ├── _pallete.scss # Tokens de cor. ├── _spacing.scss # Tokens de espaçamento e borda. ├── _typography.scss # Tokens e mixins de tipografia. ``` #### _palette.scss <a name="estilizacao-tema-palette"></a> - Define o sistema de cores da aplicação. - Cada cor (como $teal, $red, $gray) é um mapa de 10 tonalidades (50–950). **Exemplo**: ```scss $teal: ( 50: #f1fdfa, 100: #d1faf1, 200: #a4f5e4, 300: #73e8d3, 400: #50d2be, 500: #3eb6a5, 600: #309387, 700: #27756e, 800: #215d59, 900: #1e4d4a, 950: #0c2f2e, ); ``` - As cores principais do sistema são expostas utilizando variáveis definidas nesse arquivo: ```scss $primary: $teal; $success: $green; $info: $sky; $warning: $amber; $danger: $red; $gray: $slate; $black: #000; $white: #fff; ``` - Prefira usar tokens do PrimeNG como `--p-primary-color`, `--p-inputtext-border-color`, etc. - Sempre extraia valores com map.get(), por exemplo: **Exemplo** ```scss @use "sass:map"; @use "../../variables/pallete" as pallete; --p-button-primary-background: #{map.get(pallete.$primary, 50)}; ``` #### _spacing.scss <a name="estilizacao-tema-spacing"></a> - Centraliza unidades de espaçamento, bordas e sombras. ```scss $spacing-unit: 8px; $spacing: ( xxs: $spacing-unit * 0.25, xs: $spacing-unit * 0.5, sm: $spacing-unit * 1, md: $spacing-unit * 1.5, lg: $spacing-unit * 2, xl: $spacing-unit * 3, xxl: $spacing-unit * 4, xxxl: $spacing-unit * 6, ); ``` **Exemplo:** ```scss @use "sass:map"; @use "../../variables/spacing" as spacing; --p-textarea-padding-x: #{map.get(spacing.$spacing, md)}; --p-textarea-padding-y: #{map.get(spacing.$spacing, lg)}; --p-textarea-border-radius: #{spacing.$border-radius}; ``` #### _typography.scss <a name="estilizacao-tema-typography"></a> - Controla a escala tipográfica e hierarquia de texto. - Inclui - Mapas `$typography` e `$typography-scale`; - Mixins: `@mixin apply-font($level, $variant)` e `@mixin generate-typography-vars($map)` ```scss @mixin generate-typography-vars($map) { @each $level, $weights in $map { @each $variant, $props in $weights { $size: map.get($props, size); $line-height: map.get($props, line-height); $weight: map.get($props, weight); --font-#{$level}-#{$variant}-size: #{$size}; --font-#{$level}-#{$variant}-line-height: #{$line-height}; --font-#{$level}-#{$variant}-weight: #{$weight}; } } } @mixin apply-font($level, $variant) { font-family: $font-family-base; font-size: var(--font-#{$level}-#{$variant}-size); line-height: var(--font-#{$level}-#{$variant}-line-height); font-weight: var(--font-#{$level}-#{$variant}-weight); } ``` **Exemplos**: ```scss @use "../../variables/typography" as typo; @include typo.apply-font(2, regular); line-height: #{typo.$field-line-height}; min-height: #{typo.$field-min-height}; ``` - Níveis de 1 a 8 correspondem a tamanhos crescentes de texto. Use regular, medium ou bold para variar o peso. #### thema.scss <a name="estilizacao-tema-theme"></a> - É o núcleo de integração com o PrimeNG. - Importa as variáveis de cor, espaçamento e tipografia. - - Gera variáveis CSS --p-* que o PrimeNG reconhece como design tokens. - Define estilos base (`body`,`h1–h6`,`p` ,`small`). **Exemplo:** ```scss @use "sass:map"; @use "./variables/pallete" as colors; @use "./variables/spacing" as spacing; @use "./variables/typography" as typography; @use "components/index" as components; :root { --p-primary-color: #{map.get(colors.$primary, 500)}; --p-text-color: #{colors.$text-color}; @include typography.generate-typography-vars(typography.$typography-scale); } body { background-color: colors.$surface-ground; color: colors.$text-color; @include typography.apply-font(2, regular); } ``` #### Diretório Components <a name="estilizacao-tema-components"></a> - Organiza estilos específicos de cada componente customizado (ex: app-textfield, app-dropdown, app-message). **Exemplo:** ```scss! app-textfield { input[pInputText] { @include typo.apply-font(2, regular); --p-inputtext-border-color: #{map.get(pallete.$gray, 300)}; --p-inputtext-focus-border-color: #{map.get(pallete.$primary, 500)}; } } ``` #### index.scss <a name="estilizacao-tema-index"></a> - Funciona como ponto de agregação para todos os estilos de componentes: ```scss! @use "./buttons/button.scss"; @use "./form/text-field.scss"; @use "tooltips/tooltip.scss"; @use "message/message.scss"; ``` - Esse arquivo é importado diretamente no theme.scss, o que garante que todos os componentes customizados herdem os tokens e estilos globais. ### Tailwind CSS<a name="estilizacao-tailwind"></a> - Utilizar classes do Tailwind para CSS utilitários e responsividade. - Criar tokens CSS (design-system) para cores, espaçamentos, tipografia e mapear para Tailwind config (custom properties). --- ## Diretrizes de Comunicação com Back-End<a name="comunicacao-back"></a> ### API Layer (**.api.ts)<a name="comunicacao-back-api-layer"></a> **Responsabilidade:** Executar chamadas HTTP puras, sem qualquer lógica de negócio. **✅ O que DEVE ter:** - Chamadas HTTP diretas (httpClient.get/post/put/delete) - Configuração de URLs e endpoints - Configuração de headers HTTP - Tipagem com DTOs de Request/Response - Link para documentação Swagger do backend **❌ O que NÃO deve ter:** - Tratamento de exceções (delegado ao Facade) - Transformação de dados (delegado ao Facade) - Lógica de negócio - Retry/fallback - Cache **Exemplo**: ```ts @Injectable({ providedIn: 'root', }) export class ProdutoApi { private readonly baseUrl = environment.url; private http = inject(HttpClient); public getById(id: number): Observable<ProdutoApiResponse> { return this.http.get<ProdutoApiResponse>(`${this.baseUrl}/${id}`); } public create(request: CriarProdutoApiRequest): Observable<void> { return this.http.post<void>(this.baseUrl, request); } } ``` ### API Facade Layer (**.api.facade.ts)<a name="comunicacao-back-api-facade-layer"></a> **Responsabilidade:** Orquestrar chamadas à API, tratar exceções e transformar dados entre DTOs externos e internos. **✅ O que DEVE ter:** - Injeção do serviço API correspondente - Tratamento de exceções HTTP - Transformação de DTOs de Request (Internal → Request) - Transformação de DTOs de Response (Response → Internal) - Lógica de retry/fallback (quando necessário) - Cache local (quando necessário) - Mensagens de erro amigáveis **❌ O que NÃO deve ter:** - Chamadas HTTP diretas (delegado ao API) - Lógica de negócio complexa (delegado a Services) - Acesso direto a componentes **Exemplo**: ```ts @Injectable({ providedIn: 'root', }) export class ProdutoApiFacade { private produtoApi = inject(ProdutoApi); private notificacaoService = inject(NotificacaoService); public getProdutoById(id: number): Observable<ProdutoDto | null> { return this.produtoApi.getById(id).pipe( map((response: ProdutoApiResponse) => this.transformResponseToDto(response)), catchError((error) => { console.error('Falha ao buscar produto:', error); this.notificacaoService.exibirErro('Não foi possível carregar o produto. Tente novamente.'); return of(null); }) ); } public criarProduto(produto: ProdutoDto): Observable<boolean> { const request = this.transformDtoToRequest(produto); return this.produtoApi.create(request).pipe( tap(() => this.notificacaoService.exibirSucesso('Produto criado com sucesso!')), map(() => true), catchError((error) => { console.error('Falha ao criar produto:', error); this.notificacaoService.exibirErro('Não foi possível criar o produto.'); return of(false); }) ); } private transformResponseToDto(res: ProdutoApiResponse): ProdutoDto { return new ProdutoDto({ id: res.id_produto, nome: res.nome_produto, precoUnitario: res.preco_unitario, disponivel: res.disponivel_em_estoque, dataCadastro: new Date(res.data_cadastro), }); } private transformDtoToRequest(dto: ProdutoDto): CriarProdutoApiRequest { return { nome_produto: dto.nome, preco_unitario: dto.precoUnitario, disponivel: dto.disponivel, }; } } ``` ### DTOs de Request (`**.api.request.ts`)<a name="comunicacao-back-dto-request"></a> **Responsabilidade:** Espelhar exatamente os DTOs de Request do backend Java. **✅ Regras:** - **Espelhar exatamente** a estrutura do backend - **Nomes devem refletir** os nomes do backend - **Apenas leitura** - não devem ser modificados em componentes - **Documentação** com link para o DTO Java correspondente **Exemplo**: ```ts /** * @see endereço_do_swagger */ export interface CriarProdutoApiRequest { nome_produto: string; preco_unitario: number; disponivel: boolean; } ``` ### DTOs de Response (`**.api.response.ts`)<a name="comunicacao-back-dto-response"></a> **Responsabilidade:** Espelhar exatamente os DTOs de Response do backend Java. **✅ Regras:** - **Espelhar exatamente** a estrutura do backend - **Nomes devem refletir** os nomes do backend - **Apenas leitura** - não devem ser modificados em componentes - **Documentação** com link para o DTO Java correspondente **Exemplo**: ```ts /** * @see endereço_do_swagger */ export interface ProdutoApiResponse { id_produto: number; nome_produto: string; preco_unitario: number; disponivel_em_estoque: boolean; data_cadastro: string; } ``` ### DTOs Internos (`**.dto.ts`)<a name="comunicacao-back-dto-interno"></a> **Responsabilidade:** DTOs usados internamente no frontend, podem ter propriedades calculadas e formatos específicos do Angular. **✅ Características:** - Podem ter propriedades **derivadas** ou **calculadas** - Podem ter formatação específica do frontend - Podem ter métodos auxiliares - São usados entre componentes, services e facades **Exemplo**: ```ts export class ProdutoDto { id: number; nome: string; precoUnitario: number; disponivel: boolean; dataCadastro: Date; constructor(data: Partial<ProdutoDto>) { Object.assign(this, data); } get statusLabel(): string { return this.disponivel ? 'Disponível' : 'Indisponível'; } } ``` --- ## Diretrizes para o Tratamento de Exceções<a name="tratamento-excecoes"></a> ### Camadas de tratamento de erros: | Camada | Mecanismo Principal | Tipo de Erro Capturado | Objetivo Principal | :--- | :--- | :--- | :--- | | **Serviços/Componentes** | **RxJS operador catchError** | Erros **específicos** da lógica, validação, falha de API. | Recuperação, Retries, manter o Contexto. | **Rede (Global)** | **HttpInterceptor** | HttpErrorResponse |Lidar com autenticação/autorização, erros transversais, e logging global | | **Aplicação (Global)** | **CustomErrorHandler** | Erros de Runtime Não Capturados, Erros de Componentes, Erros do Framework | Logging de telemetria, fallback final, alerta de estado crítico | - O operador catchError do RxJS deve ser o mecanismo primário para interceptar erros em Observables (como aqueles retornados pelo HttpClient) dentro da camada de Serviços. **Exemplo:** ```ts public getData(): Observable<Data> { return this.http.get<Data>('/api/data').pipe( retry(3), catchError((error: HttpErrorResponse) => { if (error.status === 503) { console.warn('Serviço temporariamente indisponível, tente novamente em alguns minutos', error); return of(); } return throwError(() => error); }) ); } ``` - Interceptors para tratamento centralizado, devem transformar erros HTTP em classes padrão de erro. **Deve**: * Capturar erros 401 (Não Autorizado) e 403 (Proibido) para implementar logout automático, purga de tokens e redirecionamento para a página de login. * Enviar logs de erro de rede para ferramentas de telemetria, garantindo que todos os erros HTTP sejam registrados. * Exibir notificações de erro genéricas para falhas críticas de servidor que não exigem tratamento específico pelo Serviço. ```ts export const errorHandlerInterceptor: HttpInterceptorFn = (req, next) => { const TOASTER_SERVICE = inject(ToasterService); const ROUTER = inject(Router); return next(req).pipe( catchError((errorResponse: HttpErrorResponse) => { if (errorResponse instanceof HttpErrorResponse) { TOASTER_SERVICE.exibirMensagemErro({ titulo: 'TITULO_ERRO', mensagem: 'MENSAGEM_ERRO', }); if (errorResponse.status == 401) { ROUTER.navigate([Rotas.LOGIN.login], { queryParams: { returnUrl: ROUTER.routerState.snapshot.url } }); } } return throwError(() => errorResponse); }) ); }; ``` - O ErrorHandler captura erros lançados em contextos onde o framework está no controle, como durante a detecção de mudanças ou a inicialização de componentes.. **Exemplo:** ```ts @Injectable() export class CustomErrorHandler implements ErrorHandler { private _environment = environment; private _logger: AppLoggerService = inject(AppLoggerService); private _notificationService: NotificationService = inject(NotificationService); public handleError(error: any): void { this._notificationService.notifyCriticalError('Ocorreu um erro inesperado. Tente novamente.'); this._logger.logError(error); if(!this._environment.isProduction){ console.error('Erro não tratado capturado pelo CustomErrorHandler:', error); } } } ``` **Deve:** - Exibir mensagens visuais consistentes. - Evitar a quebra da aplicação por exceções não tratadas. - Registrar erros relevantes para análise posterior. ___ ## Diretrizes para implementação de testes<a name="testes"></a> ### Configuração de Testes com o Array imports<a name="testes-imports"></a> * A principal alteração na configuração de testes para componentes standalone reside no TestBed.configureTestingModule. Em vez de importar um NgModule que declara o componente, o próprio componente standalone é adicionado diretamente ao array imports. ```ts await TestBed.configureTestingModule({ imports:, providers: [ //... mocks de serviços ] }).compileComponents(); ``` ### Padrões para Testar componentes: <a name="testes-padroes"></a> * Diretivas de Atributo: * A abordagem padrão para testar diretivas é criar um componente de teste mínimo(_HostComponent_) ```ts @Component({ template: `<div appHighlight>Texto a ser destacado</div>` }) class TestHostComponent {} describe('HighlightDirective', () => { let fixture: ComponentFixture<TestHostComponent>; let divElement: DebugElement; beforeEach(() => { TestBed.configureTestingModule({ declarations: }); fixture = TestBed.createComponent(TestHostComponent); divElement = fixture.debugElement.query(By.css('div')); }); }); ``` ### Boas Práticas <a name="testes-boas-praticas"></a> **Deve** * Utilizar o padrão AAA: Arrange(Organizar), Act (Agir) e Assert (Afirmar) para montar os cenários. * Os testes devem validar a funcionalidade esquecendo detalhes de implementação. **Exemplo:** `it('deve chamar o método privado _recalcularTotal ao adicionar um item',...)` Frágil. `it('deve exibir o novo valor total no DOM quando um item é adicionado',...)` * Ao testar componentes 'Conteiner' usar overrideComponente para remover a importação do componente filho e usar um mock em seu lugar. ```ts @Component({ selector: 'app-filho-real', //-> Mesmo seletor do componente real standalone: true, template: '<div>Componente Filho Mockado</div>' }) class FilhoMockComponent {} // No cenário de teste await TestBed.configureTestingModule({ imports: [PaiComponent], }) .overrideComponent(PaiComponent, { remove: { imports: }, add: { imports: [FilhoMockComponent] } }) .compileComponents(); ``` **Não deve:** * Depender de estados deixados por testes anteriores. * Agrupar vários cenários em um único teste. * Precisar de uma quantidade excessiva de mocks. Esse ponto é um alerta que a classe ou componente possui muitas responsabilidades. * Ser instáveis, deve manter o resultado de forma consistentente. #### Organização e Nomeclatura <a name="testes-organizacao-nomeclatura"></a> * Por convenção do Angular CLI, os arquivos de teste devem ser nomeados com o sufixo .spec.ts e colocados ao lado do arquivo que estão testando `usuario-listagem.component.ts` -> `usuario-listagem.component.spec.ts` * As descrições nos blocos describe e it devem ser claras e formar uma frase legível que documente o comportamento esperado. ```ts describe('UnidadeSobTeste', () => { it('deve [fazer algo] quando [em uma certa condição]', () => {... }); });. ``` ___ ## Melhores Práticas de Implementação<a name="melhores-praticas-de-implementacao"></a> **Deve**: - Usar apenas os atributos html/template e css necessários para evitar acumulo de "lixo" no código. - Resolver todos os code smells apontados pelo Sonarqube. - Tentar resolver todos os warnings de qualidade de código relevantes apontados pela IDE. - Manter tipagem concisa e reutilizavel. - Usar `trackBy` em @For para performance. - Configurar tsconfig.json da forma que melhor enforce as boas praticas de programação com Typescript. - Configurar angular.json da forma que melhor enforce as boas praticas de programação com Typescript. - Usar `const` em vez de `let` quando o valor não será reatribuído, reforçando intenção de imutabilidade. **Não Deve**: - Não Acessar DOM Diretamente. Usar Renderer2 ou HostBinding - Não Realizar Lógica JavaScript no Template. Mover lógica para o componente ou classe responsável. - Inserir HTML ou URLs diretamente no templete sem sanitização. - Realizar operações custosas dentro do ciclo de vida do Angular (ex.: loops complexos em _ngOnInit_ ou _ngAfterViewInit_). - Carregar módulos sem _lazy loading_. - Encadear um subscribe dentro de outro. (Use operadores de encadeamento do RxJs). - Usar subscribes encadeados dentro de outros. - Usar`any` sem necessidade. ## Glossário<a name="glossario"></a> | Tópico/Seção | Convenção de Nomenclatura | Exemplo(s) | | :--- | :--- | :--- | | **Reatividade (RxJS)** | Variáveis do tipo `Observable` devem terminar com o sufixo `$` (dólar). | `dividas$`, `isLoading$`, `usuarioLogado$` | | **Reatividade (ReactiveForms)** | Interfaces ou Types que definem a estrutura de um `FormGroup` devem ser prefixados com `IForm` ou `Form`. | `IFormUsuario`, `FormCliente` | | **Componentização (Container)** | Arquivos de *Smart Components* (Containers) devem usar o sufixo `.container.ts`. | `cadastro-inscricao-divida.container.ts` | | **Componentização (Presenter)** | Arquivos de *Dumb Components* específicos de uma feature (Presenters) devem usar o sufixo `.presenter.ts`. | `cadastro-inscricao-divida-list.presenter.ts` | | **Comunicação com Back-End (API Layer)** | Arquivos que realizam chamadas HTTP puras devem usar o sufixo `.api.ts`. A classe segue o padrão `[NomeDoRecurso]Api`. | `ProdutoApi` em `produto.api.ts` | | **Comunicação com Back-End (API Facade)** | Arquivos que orquestram a camada de API, tratam erros e transformam dados devem usar o sufixo `.api.facade.ts`. A classe segue o padrão `[NomeDoRecurso]ApiFacade`. | `ProdutoApiFacade` em `produto.api.facade.ts` | | **Comunicação com Back-End (DTOs de Request)** | Arquivos de DTOs de *Request* que espelham o backend devem usar o sufixo `.api.request.ts`. A interface segue o padrão `[Acao][Recurso]ApiRequest`. | `CriarProdutoApiRequest` em `criar-produto.api.request.ts` | | **Comunicação com Back-End (DTOs de Response)** | Arquivos de DTOs de *Response* que espelham o backend devem usar o sufixo `.api.response.ts`. A interface segue o padrão `[Recurso]ApiResponse`. | `ProdutoApiResponse` em `produto.api.response.ts` | | **Comunicação com Back-End (DTOs Internos)** | Arquivos de DTOs para uso interno no frontend devem usar o sufixo `.dto.ts`. A classe segue o padrão `[Recurso]Dto`. | `ProdutoDto` em `produto.dto.ts` | | **Testes (Arquivos)** | Arquivos de teste devem usar o sufixo `.spec.ts` e estar localizados ao lado do arquivo que estão testando. | `usuario-listagem.container.spec.ts` | | **Testes (Descrições)** | As descrições nos blocos `describe` e `it` devem ser claras e formar uma frase legível que documente o comportamento esperado. | `describe('UnidadeSobTeste', () => { it('deve [fazer algo] quando [em uma certa condição]', ...); });` |