Este documento contém os exercícios feitos em aula + minhas notas pessoais sobre a Mentoria Angular Pro de Paolo Almeida e Andrew Rosário.
Em aula, os mentores utilizam scss. Eu optei por usar css por ter mais prática com ele. Os scripts do Nx que criam libs com scss, na mentoria, foram alterados aqui para css.
Se este documento for útil para você, considere deixar uma ⭐ no repositório.
Explicação do escopo do projeto: trata-se de um e-commerce com as funcionalidades de cadastro/login, uma home e um catálogo de produtos. O projeto contempla o front-end desenvolvido em Angular com Nx e o uso de uma API fake disponível no https://mockapi.io.
Criação do projeto utilizando o comando:
npx create-nx-workspace@latest ecommerce --preset=angular-standaloneExecução do projeto com o comando:
nx serveCriação da biblioteca de layout com o comando:
nx g @nx/angular:library --name=layout --directory=modules/feature/layout --projectNameAndRootFormat=as-provided --standalone=false --style=cssVisualização do gráfico de dependências do projeto com o comando:
nx graphCriado componente header usando Nx Console a partir da pasta modules/feature/layout/src/lib passando as opções export=true e standalone=false. O comando gerado a partir do Nx Console foi:
npx nx generate @nx/angular:component --name=header --directory=header --export=true --standalone=false --nameAndDirectoryFormat=as-provided --no-interactiveConsumo da biblioteca de layout no projeto principal:
- Importado o módulo
LayoutModuleemapp.component.ts; - Adicionado o componente
ecommerce-headeremapp.component.html; - Além disso, foi excluído o teste que não passava (fazendo referência a um título que não existe).
Feito isso, nx lint e nx test foram executados para garantir que o projeto está funcionando corretamente.
Primeiro, foi estilizado o componente header. Em seguida, foram escritos testes unitários para o componente header:
it(`should contain title`, () => {
const header: HTMLHeadElement =
fixture.nativeElement.querySelector('header');
expect(header.textContent).toBe('Ecommerce');
});E para o componente app:
it(`should contain header`, () => {
const header: HTMLElement = fixture.nativeElement.querySelector('header');
expect(header).toBeTruthy();
});Instalado husky + lint-staged para rodar nx lint e nx test antes de cada commit. Seguem comandos:
npx husky-init && npm install
npm install lint-stagedPara configurar:
- Substituir a instrução
npm testpornpx lint-stagedno arquivo.husky/pre-commit; - Criar um arquivo
.lintstagedrcna raiz da aplicação com o seguinte conteúdo:
{
"{src,modules}/**/*.{js,ts,jsx,tsx,json,html,css,scss}": [
"nx affected:lint --fix --uncommitted",
"nx affected:test",
"nx format:write --uncommited"
]
}- Adicionar a regra abaixo na seção
rulesdo arquivoeslintrc.base.jsonpara permitir o uso deconsole.warneconsole.errorno código mas não permitirconsole.log:
"no-console": [
"error", {
"allow": ["warn", "error"]
}
],- Testar o commit.
Criado o módulo product-data-access com o comando:
npx nx g @nx/angular:library --name=product-data-access --directory=modules/data-access/product --projectNameAndRootFormat=as-providedO componente product-data-access foi excluído e removido da index.ts.
Notas
Para que o commit funcionasse, precisei alterar na configuração do lint-staged:
- Removi
jsecssdonx lint; - Acrescentei
--passWithNoTestsnonx test.
Criada a model Product em modules/data-access/product/src/lib/models/product.ts:
export type Product = {
createdAt: string;
name: string;
price: string;
description: string;
image: string;
id: string;
quantity: number;
};Criado o serviço para busca de produtos com o comando:
npx nx g @schematics/angular:service --name=product-search --project=product-data-access --flat=falseEm src/app/app.config.ts foi importado o HttpClient via provideHttpClient().
Por último, foi implementado o teste através do HttpClientTestingModule e HttpTestingController para o serviço ProductSearchService.
Alterei meu .prettierrc para usar 4 espaços em vez de 2 nas formatações:
{
"singleQuote": true,
"useTabs": false,
"tabWidth": 4
}Rodei o comando abaixo para reformatar todos os arquivos do projeto com o novo espaçamento:
nx format:write --allReestilizei alguns componentes e apliquei uma nova fonte ao projeto:
- Adicionei a fonte Montserrat na
index.html; - Alterei
modules/feature/layout/src/lib/header/header.component.css; - Alterei
styles.css.
🌸 Floreei 🌸 este README.md.
Foi instalado/adicionado ao projeto o Angular Material com os comandos abaixo:
npm install @angular/material
npx nx g @angular/material:ng-add --project=ecommerceEm seguida, foi criado o módulo Product Search com o comando:
npx nx g @nx/angular:library --name=product-search --directory=modules/feature/product/search --projectNameAndRootFormat=as-provided --style=cssOs dados do Data Access foram exportados via modules/data-access/product/src/lib/index.ts:
export * from './lib/mocks/product.mock';
export * from './lib/product-search/product-search.service';O componente product-search foi implementado usando o componente Autocomplete do Angular Material;
O padrão de composição foi aplicado no componente header:
<header class="header">
<h1 class="logo">{{ title }}</h1>
<ng-content></ng-content>
<ng-content select="[right]"></ng-content>
</header>E o componente foi então consumido no app.component.html:
<ecommerce-header title="e-Commerce">
<ecommerce-product-search></ecommerce-product-search>
<p right>Login</p>
</ecommerce-header>
<router-outlet></router-outlet>Por fim, para os testes rodarem corretamente, foram desabilitadas as animações do Angular Material no product.search.component.spec.ts:
import { NoopAnimationsModule } from '@angular/platform-browser/animations';- Setei a propriedade
subscriptSizingdo campo de busca paradynamicpara alinhar o componente verticalmente; - Removi a fonte Roboto da
index.htmlporque já havia configurado a Montserrat; - Temporariamente, coloquei um ícone no lugar do texto "Login" no
app.component.htmlaté definirmos o próximo componente.
Foi implementada a busca de produtos no componente product-search com o uso do FormControl e operadores do RxJS para evitar requisições desnecessárias e foi utilizado pipe async para subscrever o observable no template.
this.products$ = this.control.valueChanges.pipe(
debounceTime(333),
distinctUntilChanged(),
filter((text) => text.length > 1),
switchMap((text) => this.productSearchService.searchByName(text))
);Foram implementados testes para o componente product-search e para o serviço ProductSearchService. Utilizamos FakeAsync + tick para simular o tempo de espera da requisição e usamos spy para verificar se o método searchByName foi chamado.
Nesta aula, criamos o módulo home com o comando:
nx g @nx/angular:library --name=home --directory=modules/feature/home --lazy=true --routing=true --projectNameAndRootFormat=as-provided --style=cssDiscutimos as estratégias de preloading disponíveis no Angular e implementamos o lazy loading do módulo home. Aprendi que o lazy loading pode ser configurado usando loadChildren apontando para o módulo de rotas:
export const appRoutes: Route[] = [
{ path: '', redirectTo: 'home', pathMatch: 'full' },
{
path: 'home',
loadChildren: () => import('@ecommerce/home').then((r) => r.homeRoutes),
},
];Vimos o lazy loading em ação ao inspecionar a aplicação no navegador:
Mais sobre as estratégias de preloading pode ser visto neste post.
Implementamos a seção de produtos recomendados, por enquanto com mock de dados. Conversamos sobre HTML semântico e a importância de usar tags apropriadas para melhorar a acessibilidade e SEO. Famos sobre o padrão BEM para nomenclatura de classes do CSS.
Mais sobre padrões de acessibilidade pode ser visto neste link.
Criamos o serviço de produtos recomendados agora buscando da API (antes usávamos mock) e refatoramos a home para separar o código responsável pelo card de produto.
Criamos a lib para exibir os detalhes de um produto com o comando:
npx nx g @nx/angular:library --name=product-detail --directory=modules/feature/product/detail --lazy=true --routing=true --projectNameAndRootFormat=as-provided --style=cssHabilitamos a captura de parâmetros através de input no componente adicionando a função withComponentInputBinding no app.config.ts:
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(appRoutes, withComponentInputBinding()),
provideHttpClient(),
provideAnimationsAsync(),
],
};Para os testes passarem, foi necessário adicionar o RouterTestingModule em product-detail.component.spec.ts.
Implementamos module boundaries para garantir as regras:
type:data-accessdeve ser capaz de importar detype:data-access;type:featuredeve ser capaz de importar detype:feature,type:uietype:data-access;type:uideve ser capaz de importar detype:uietype:data-access
Para aplicar as regras, precisamos dos dois passos:
- Atribuir um identificador às nossas libs. Isso é feito adicionando-se tags no arquivo
project.json(exemplo abaixo para a libproduct-data-access):
"tags": ["type:data-access"]- Definir as regras de importação no arquivo
.eslintrc.base.jsonno arraydepConstraintsdo plugin@nx/enforce-module-boundaries:
{
"sourceTag": "type:data-access",
"onlyDependOnLibsWithTags": ["type:data-access"]
},
{
"sourceTag": "type:feature",
"onlyDependOnLibsWithTags": [
"type:feature",
"type:ui",
"type:data-access"
]
},
{
"sourceTag": "type:ui",
"onlyDependOnLibsWithTags": [
"type:ui",
"type:data-access"
]
}Mais sobre module boundaries pode ser visto neste link e neste link.
Criamos um pipe customizado e vimos a diferença entre um pipe puro e um pipe impuro. O pipe customizado foi criado com o comando:
nx g @nx/angular:pipe --name=quantity-description --directory=modules/feature/product/detail/src/lib/pipes/quantity-description --nameAndDirectoryFormat=as-providedVimos a importância de utilizar o pipe em vez de chamar funções diretamente no template pois o change detection do Angular é mais eficiente. Mais sobre pipes pode ser visto neste link.
Criamos um interceptor para tratar erros nas requisições HTTP. O interceptor foi criado com o comando:
nx g @schematics/angular:interceptor --name=http-errors --project=ecommerce --flat=false --path=src/app/interceptorsUtilizamos o snack bar do Angular Material para exibir mensagens de erro ao usuário. O código final do interceptor ficou assim:
export const httpErrorsInterceptor: HttpInterceptorFn = (req, next) => {
const snackBar = inject(MatSnackBar);
return next(req).pipe(
catchError((err) => {
snackBar.open('Ops, ocorreu um erro', 'Fechar', {
duration: 5000,
});
return throwError(() => err);
})
);
};No teste do interceptor, forçamos um erro na requisição e verificamos se o snack bar foi chamado:
it('should open notification on http error', () => {
jest.spyOn(snackBar, 'open');
httpClient.get('/test').subscribe();
const request = httpMock.expectOne('/test');
request.error(new ProgressEvent('error'));
expect(snackBar.open).toHaveBeenCalled();
});Discutimos sobre gerenciamento de estado e falamos sobre as libs disponíveis no mercado para isso no Angular. No entanto, concordamos que na maioria das vezes é possível gerenciar o estado apenas com RxJS ou Signals, fugindo da complexidade que estas libs trazem.
Criamos uma service para gerenciar o estado do carrinho de compras com o comando:
nx g @schematics/angular:service --name=cart --project=product-data-access --flat=false --path=modules/data-access/product/src/lib/stateE, nesta service, implementamos o gerenciamento de estado a) primeiro com RxJS:
private cartSubject$ = new BehaviorSubject<Product[]>([]);
cart$ = this.cartSubject$.asObservable();
quantity$ = this.cart$.pipe(map((products) => products.length));
addToCart(product: Product) {
const cart = this.cartSubject$.getValue();
this.cartSubject$.next([...cart, product]);
}b) e depois com Signals:
private cartSignal = signal<Product[]>([]);
cart = this.cartSignal.asReadonly();
quantity = computed(() => this.cart().length);
addToCart(product: Product) {
this.cartSignal.update((cart) => [...cart, product]);
}Criamos o componente CartComponent para exibir a quantidade de itens no carrinho usando o Badge do Angular Material. Para termos acesso ao estado do carrinho, utilizamos o serviço CartService com o signal já implementado anteriormente. O componente foi, então, consumido em app.component.html.
Criado primeiro script de integração contínua disponível em .github/workflows/ci.yml que executa formatação, lint e testes no projeto a cada push na main ou novo pull request.
Configuramos uma conta na Vercel e importamos o projeto do GitHub para a plataforma. A partir de agora, o deploy será feito automaticamente a cada push na branch main.
Também criamos a nova lib que será usada para construir o formulário para autenticação do usuário:
npx nx g @nx/angular:library --name=auth-form --directory=modules/feature/auth/form --lazy=true --routing=true --projectNameAndRootFormat=as-provided --style=css --tags=type:featureE adicionamos nova rota no appRoutes para o módulo de autenticação:
{
path: 'auth',
loadChildren: () =>
import('@ecommerce/auth-form').then((r) => r.authFormRoutes),
},Começamos a construir um formulário reativo em etapas utilizando um componente como orquestrador tendo seu próprio router-outlet. Cada etapa do formulário é um componente filho separado com rotas configuradas no arquivo lib.routes.ts da lib de autenticação.
Os componentes filhos conseguem acessar o componente pai via injeção de dependência (sim, de componentes!) e, assim, compartilhar informações entre si. Para saber mais, acesse este link.
Para cenários mais genéricos, é possível criar uma classe abstrata que os orquestradores implementam para serem injetadas nos componentes filhos. Para saber mais, acesse este link.
Formulários complexos também podem ser divididos com ControlContainer e com ControlValueAccessor.
Finalizamos o formulário iniciado na aula anterior e implementamos os testes. Foi necessário fornecer rotas com provideRouter e importar o NoopAnimationsModule para os testes passarem. Para testarmos a interação com o HTML do form, substituímos a manipulação do FormControl como mostrado a seguir:
it('should display email error message', () => {
// component.control.setValue('teste');
const input: HTMLInputElement =
fixture.nativeElement.querySelector('input');
input.value = 'teste';
input.dispatchEvent(new Event('input'));
component.control.markAllAsTouched();
fixture.detectChanges();
const error = fixture.nativeElement.querySelector(
'[data-testid="error-email"]'
);
expect(error).toBeTruthy();
});Onde data-testid é um atributo customizado que usamos para identificar elementos no HTML.
Criamos uma nova lib para gerenciar autenticação com o comando:
nx g @nx/angular:library --name=auth-data-access --directory=modules/data-access/auth --projectNameAndRootFormat=as-provided --standalone=false --tags=type:data-accessCriamos uma nova service para armazenar o estado de autenticação com o comando:
npx nx g @schematics/angular:service --name=auth --project=auth-data-access --flat=falseCriamos a função authGuard para ser utilizada como guarda da rota de login que redireciona para a home caso o usuário já esteja autenticado ou retorna true no canActivate caso contrário, permitindo o acesso à tela de autenticação.
Criamos, implementamos alguns casos de uso e escrevemos testes para a diretiva Log. Mais sobre diretivas pode ser visto neste link e neste link.
Implementamos alguns testes E2E com Cypress. Rodamos o Cypress em modo headless com o comando:
nx e2e e2eRodamos Cypress via interface gráfica com o comando:
npx cypress open --project ./e2eObs. 1: o parâmetro --watch foi descontinuado.
Obs. 2: precisei reconfigurar o tsconfig.json do Cypress para que o TypeScript reconhecesse os tipos do Cypress.
Alteramos o workflow de CI para executar os testes E2E adicionando o comando a seguir:
- run: npx nx affected -t e2e --parallel=3 --configuration=ci