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-standalone
Execução do projeto com o comando:
nx serve
Criaçã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=css
Visualização do gráfico de dependências do projeto com o comando:
nx graph
Criado 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-interactive
Consumo da biblioteca de layout no projeto principal:
- Importado o módulo
LayoutModule
emapp.component.ts
; - Adicionado o componente
ecommerce-header
emapp.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-staged
Para configurar:
- Substituir a instrução
npm test
pornpx lint-staged
no arquivo.husky/pre-commit
; - Criar um arquivo
.lintstagedrc
na 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
rules
do arquivoeslintrc.base.json
para permitir o uso deconsole.warn
econsole.error
no 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-provided
O 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
js
ecss
donx lint
; - Acrescentei
--passWithNoTests
nonx 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=false
Em 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 --all
Reestilizei 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=ecommerce
Em 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=css
Os 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
subscriptSizing
do campo de busca paradynamic
para alinhar o componente verticalmente; - Removi a fonte Roboto da
index.html
porque já havia configurado a Montserrat; - Temporariamente, coloquei um ícone no lugar do texto "Login" no
app.component.html
até 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=css
Discutimos 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=css
Habilitamos 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-access
deve ser capaz de importar detype:data-access
;type:feature
deve ser capaz de importar detype:feature
,type:ui
etype:data-access
;type:ui
deve ser capaz de importar detype:ui
etype: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.json
no arraydepConstraints
do 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-provided
Vimos 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/interceptors
Utilizamos 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/state
E, 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:feature
E 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-access
Criamos 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=false
Criamos 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 e2e
Rodamos Cypress via interface gráfica com o comando:
npx cypress open --project ./e2e
Obs. 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