- Arquitectura de inicio de proyecto
- Models
- Interfaces
- Types
- Directives
- Resolvers
- Guards
- Helpers
- Utils
- Interceptors
- Signals ⭐ nuevo
- Control-Flow ⭐ nuevo
- Eslint Prettier (Documentación Prettier) (Documentación Eslint)
- Automatic-Changelog (Documentacion Commits)
- SonnarQube (Documentación Oficial)
- Sentry (Documentación Oficial)
- Cypress (Documentación Oficial)
- Karma (Documentación Oficial)
- Documentacion-codigo
- Performance
- Bundle
frontend/src/
│ ├── app
│ │ ├── components
│ │ │ ├── component-name
│ │ │ │ ├── name.validation.component.ts
│ │ │ │ ├── name.component.ts
│ │ │ │ ├── name.component.html
│ │ │ │ └── name.component.scss
│ │ │ └── components.module.ts
│ │ ├── auth
│ │ └── auth.module.ts
│ │ ├── 404
│ │ ├── helpers
│ │ ├── services
│ │ ├── pipes
│ │ ├── utils
│ │ └── confirm-toast.ts
│ │ ├── interfaces
│ │ ├── resolvers
│ │ │ └── servicio.resolver.ts
│ │ ├── pages
│ │ │ ├── pages.routing.ts
│ │ │ └── pages.module.ts
│ │ ├── models
│ │ ├── shared
│ │ │ ├── navbar.ts
│ │ │ ├── breadcrumbs.ts
│ │ │ └── shared.module.ts
│ │ ├── guards
│ │ ├── app.module.ts
│ │ ├── app.routing.module.ts
│ │ ├── interceptors
│ │ └── app.component.ts
│ ├── enviroments
│ └── assets
│ │ ├── dictionarios
│ │ ├── images
│ │ ├── icons
│ │ ├── js
│ │ ├── mock
│ │ └── css
├── node_modules/
├── .github
│ └── workflows
│ ├── build.yml
│ └── lint.yml
├── sonar-project.properties
├── .eslintrc.json
├── .eslintignore
├── .env
├── .gitignore
├── package.json
├── package-lock.json
├── CHANGELOG.md
└── README.mdSi existe un componente complejo con demasiadas interfaces, se puede crear una carpeta de modelos y interfaces en el componente
├── components
│ ├── component-name
│ │ ├── models
│ │ ├── interfaces
│ │ ├── name.validation.component.ts
│ │ ├── name.component.ts
│ │ ├── name.component.html
│ │ └── name.component.scss{
"compileOnSave": false,
"exclude": ["karma.conf.js"],
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"sourceMap": true,
"declaration": false,
"experimentalDecorators": true,
"moduleResolution": "bundler",
"importHelpers": true,
"verbatimModuleSyntax": true,
"target": "ES2022",
"module": "ES2022",
"useDefineForClassFields": false,
"lib": [
"ES2022",
"dom"
]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}
"moduleResolution": "bundler"— reemplaza"node"en Angular 17+, optimizado para bundlers modernos (esbuild/Vite)."verbatimModuleSyntax": true— fuerza el uso deimport typecuando corresponde, mejorando el bundle automáticamente.
Se utilizan en formularios o se requiere una funcionalidad extra a solo la data, en este ejemplo con los datos obtenidos creamos el nombre completo del usuario
export class ProductosModel {
constructor(
public id : number,
public nombres : string,
public apellido : string,
public apellido2 : string,
) { }
get nombreCompleto() {
return `${nombres} ${apellido} ${apellido2}`
}
}Cuando tus datos no se veran modificados, datos provinientes del backend, un ejemplo sería la una petición http
export interface UsuarioInterface {
id : number;
nombres : string;
apellido : string;
apellido2 : string;
}return this.http.get<UsuarioInterface>('http://localhost:3200/api/getUsuarios');Utilizado cuando existe solo ciertos criterios o posibles casos
type EstadoCivil = 'soltero' | 'casado' | 'divorciado' | 'viudo' | 'union libre';
⚠️ Resolve<T>(clase) fue deprecado en Angular 14.2. Usá functional resolvers coninject().
Permite recuperar la información antes de que el componente se cargue. Ideal para precargar datos de un producto antes de navegar a su detalle.
// products.resolver.ts
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import type { ProductInterface } from '../interfaces/product.interface';
export const productsResolver: ResolveFn<ProductInterface[]> = (route, state) => {
return inject(ProductsService).getAll();
};// app.routing.ts
{
path: 'products',
component: ProductsComponent,
resolve: { products: productsResolver }
}// products.component.ts
import { inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import type { ProductInterface } from '../interfaces/product.interface';
export class ProductsComponent {
private route = inject(ActivatedRoute);
products: ProductInterface[] = this.route.snapshot.data['products'];
}Los Signals son el nuevo sistema reactivo de Angular (estable desde v17). Reemplazan gradualmente a @Input, @Output, @ViewChild y el modelo basado en Zone.js.
import { signal, computed, effect } from '@angular/core';
count = signal(0); // WritableSignal<number>
increment() {
this.count.update(v => v + 1); // actualiza con función
// o: this.count.set(5); // reemplaza el valor
}price = signal(100);
quantity = signal(3);
total = computed(() => this.price() * this.quantity()); // solo recalcula si sus dependencias cambianconstructor() {
effect(() => {
console.log('El total cambió:', this.total()); // se ejecuta cada vez que total() cambia
});
}import { input } from '@angular/core';
// Opcional con valor por defecto
title = input('Sin título');
// Requerido — lanza error en compilación si no se pasa
productId = input.required<number>();import { output } from '@angular/core';
productSelected = output<number>();
select(id: number) {
this.productSelected.emit(id);
}import { model } from '@angular/core';
// En el componente hijo
value = model<string>('');
// En el template del padre
<app-input [(value)]="mySignal" />import { viewChild, viewChildren, ElementRef } from '@angular/core';
inputRef = viewChild<ElementRef>('inputRef'); // Signal<ElementRef | undefined>
listItems = viewChildren<ElementRef>('item'); // Signal<readonly ElementRef[]>import { linkedSignal } from '@angular/core';
// Se recalcula cuando source cambia, pero también puede modificarse manualmente
selectedIndex = linkedSignal(() => this.items().length > 0 ? 0 : -1);import { resource, httpResource } from '@angular/core';
// httpResource — petición HTTP directa como signal
products = httpResource<ProductInterface[]>('/api/products');
// En el template
@if (products.isLoading()) { <span>Cargando...</span> }
@for (p of products.value() ?? []; track p.id) { <li>{{ p.name }}</li> }Angular 17+ reemplaza *ngIf, *ngFor y *ngSwitch con una nueva sintaxis de control de flujo nativa. Es más legible y tiene mejor rendimiento.
<!-- ❌ Antes -->
<div *ngIf="isLogged; else guestBlock">Bienvenido</div>
<ng-template #guestBlock><div>Invitado</div></ng-template>
<!-- ✅ Ahora -->
@if (isLogged) {
<div>Bienvenido</div>
} @else if (isPending) {
<div>Verificando...</div>
} @else {
<div>Invitado</div>
}<!-- ❌ Antes -->
<li *ngFor="let product of products; trackBy: trackById">{{ product.name }}</li>
<!-- ✅ Ahora — track es parte de la sintaxis, no opcional -->
@for (product of products; track product.id) {
<li>{{ product.name }}</li>
} @empty {
<li>No hay productos.</li>
}
@emptyes un bloque nuevo que se muestra cuando el array está vacío — en *ngFor necesitabas un *ngIf extra.
<!-- ❌ Antes -->
<div [ngSwitch]="status">
<span *ngSwitchCase="'active'">Activo</span>
<span *ngSwitchCase="'inactive'">Inactivo</span>
<span *ngSwitchDefault>Desconocido</span>
</div>
<!-- ✅ Ahora -->
@switch (status) {
@case ('active') { <span>Activo</span> }
@case ('inactive') { <span>Inactivo</span> }
@default { <span>Desconocido</span> }
}Cargá componentes pesados solo cuando sean necesarios. Angular crea un chunk separado automáticamente.
<!-- Carga cuando el bloque entra en el viewport -->
@defer (on viewport) {
<app-heavy-chart />
} @placeholder {
<div class="skeleton">Cargando gráfico...</div>
} @loading (minimum 300ms) {
<app-spinner />
} @error {
<p>Error al cargar el componente.</p>
}| Trigger | Descripción |
|---|---|
on viewport |
Cuando el placeholder entra en el viewport |
on idle |
Cuando el browser está ocioso |
on interaction |
Al hacer click o focus |
on hover |
Al hacer hover |
on timer(2s) |
Después de X tiempo |
when condition |
Cuando una condición es true |
ng add @angular-eslint/schematicsng add @angular-eslint/schematics@nextnpm i prettier prettier-eslint eslint-config-prettier eslint-plugin-prettier -Dfilename: .eslintrc.json
{
"root": true,
"ignorePatterns": [
"projects/**/*"
],
"overrides": [
{
"files": [
"*.ts"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@angular-eslint/recommended",
"plugin:@angular-eslint/template/process-inline-templates",
"strict"
],
"rules": {
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "app",
"style": "camelCase"
}
],
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "app",
"style": "kebab-case"
}
],
"max-len": [
"error",
{
"code": 130 ,
"ignoreComments": true,
"ignoreUrls": true,
"ignoreStrings": true,
"ignoreTemplateLiterals": true
}
],
"max-nested-callbacks": ["error", 3], // Maximo de anidación en Ifs, functions etc.
"max-lines-per-function": [ "warn", { "max": 20, "skipBlankLines": true, "skipComments": true } ],
"@typescript-eslint/no-inferrable-types": [
2,
{
"ignoreParameters": true,
"ignoreProperties": true
}
]
}
},
{
"files": [
"*.html"
],
"extends": [
"plugin:@angular-eslint/template/recommended"
],
"rules": {}
}
]
}
Filename .eslintignore
karma.conf.js
.gitignore
.scannerwork/
.husky/
.eslintrc.json
.angular
.github
dist/
cypress/Nos permite validar el eslint a la hora de subir el proyecto o mergearlo a la arma main
Filename liny.yml
name: CI
on: [push, pull_request]
# on:
# push: # Any time you push in master will check de compile
# branches: [ master ]
# pull_request:
# branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node: ['14.x']
steps:
- uses: actions/checkout@v1
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node }}
- name: Install dependencies
run: npm install
- name: Lint
run: npm run lintFilename: .prettierignore
dist
node_modules
dbaeumer.vscode-eslint
esbenp.prettier-vscode
{
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"editor.formatOnSave": false
},
"[typescript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"editor.formatOnSave": false
},
},
"editor.suggest.snippetsPreventQuickSuggestions": false,
"editor.inlineSuggest.enabled": truefilename: package.json
"scripts": {
"lint:fix": "ng lint --fix"
}root/
├── .husky/commit-msg
├── CHANGELOG.md
└── package.json
echo > CHANGELOG.md && mkdir .husky && cd .husky && echo npx commitlint --edit > commit-msg && cd ..echo > .gitignore "node_modules/ \n.husky \npackage-lock.json" npm i -D huskynpm initfilename: package.json
{
"name": "nombre_de_la_app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"release": "release-it"
},
"repository": {
"type": "git",
"url": "git+https://github.com/nombre_del_repositorio.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/nombre_del_repositorio/issues"
},
"homepage": "https://github.com/nombre_del_repositorio#readme",
"dependencies": {
"@commitlint/cli": "^17.0.3",
"@commitlint/config-conventional": "^17.0.3",
"@release-it/conventional-changelog": "^5.0.0",
"husky": "^8.0.1",
"release-it": "^15.1.1"
},
"release-it": {
"git": {
"commitMessage": "chore: release v${version}"
},
"github": {
"release": true
},
"npm": {
"publish": false
},
"plugins": {
"@release-it/conventional-changelog": {
"infile": "CHANGELOG.md",
"preset": {
"name": "conventionalcommits",
"types": [
{
"type": "feat",
"section": "Features"
},
{
"type": "fix",
"section": "Bug Fixes"
}
]
}
}
}
}
}npm run releaseimport { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
@Injectable({
providedIn: 'root'
})
export class AuthUserGuard {
export class AuthUserGuard {
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot): boolean {
return true;
}
}
export const IsAuthUserGuard: CanActivateFn = ( route: ActivatedRouteSnapshot, state: RouterStateSnapshot ):boolean => {
return inject(AuthUserGuard).canActivate(route, state);
}note: CanActivate fue depreciado en la versión 15 de Angular
export const IsAuthUserGuard: CanActivateFn = ( route: ActivatedRouteSnapshot, state: RouterStateSnapshot ):boolean => {
return true;
}Las funciones helpers resuelven, son una forma de agrupar funciones de uso común, destinadas a servir de ayuda a otros procesos, como lo son son formularios entre otros. En este ejemplo valida un formulario que sea valido y tambien toca todos los campos inputs para validar que no esten vacios
import { FormGroup } from '@angular/forms';
export function isValidForm(simpleForm:FormGroup):boolean {
if ( !simpleForm.invalid ) { return false; }
Object.values(simpleForm.controls).forEach((control) => {
if (control instanceof FormGroup)
Object.values(control.controls).forEach((control) => control.markAsTouched());
else
simpleForm.markAllAsTouched();
});
return true;
}Son generalmente utilidades con proposito de crear cosas genericas que pueden ser utilizadas en múltiples áreas del código. Un ejemplo claro podría ser alertas globales, toas globales que solo reciban como parametro algun mensaje, alguna promesa etc.
export class SweetAlertUtil {
successNotification( { title = 'Operación exitosa', html = '' } = {}) {
Swal.fire({
icon: 'success',
title,
html,
showConfirmButton: false,
timer: 1500
})
}
alertConfirmation(
title : string = '¿Deseas continuar con esta acción?',
text : string = 'Este proceso es irreversible.',
imageUrl : string = 'assets/icons2/verify.svg',
imageWidth : number = 400,
imageHeight : number = 200,
confirmButtonText : string = 'Aceptar',
cancelButtonText : string = 'Cancelar',
) : Promise<boolean> {
return new Promise( resolve => {
Swal.fire({
title,
text,
imageUrl,
imageWidth,
imageHeight,
showCancelButton: true,
reverseButtons: true,
cancelButtonText,
confirmButtonText,
cancelButtonColor: 'transparent',
confirmButtonColor: '#A8227F',
customClass: {
cancelButton: 'btn-outline-blue'
}
}).then((result) => {
if (result.value) {
Swal.fire({
icon: 'success',
title: 'Acción Realizada!',
showConfirmButton: false,
timerProgressBar: true,
timer: 1500
})
resolve(true);
} else if (result.dismiss === Swal.DismissReason.cancel) {
Swal.fire({
icon: 'error',
title: 'Cancelado',
showConfirmButton: false,
timerProgressBar: true,
timer: 1500
})
resolve(false);
}
});
});
}
errorNotification(message:string = 'Error 404') {
Swal.fire('Error', message, 'error');
}
}
⚠️ HttpInterceptor(clase) fue deprecado en Angular 18. Usá functional interceptors conHttpInterceptorFn.
// error-handler.interceptor.ts
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, throwError } from 'rxjs';
export const errorHandlerInterceptor: HttpInterceptorFn = (req, next) => {
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
const messages: Record<number, string> = {
400: error.error?.[0] ?? 'Bad Request',
401: `Error de autorización: ${error.error?.message}`,
404: 'Recurso no encontrado (404)',
422: error.error?.errors,
499: `Error de tiempo: ${error.error?.message}`,
500: `Error del servidor: ${error.message}`,
};
const message = messages[error.status] ?? `Error inesperado (${error.status}): ${error.message}`;
console.error(message);
return throwError(() => new Error(message));
})
);
};// auth.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from '../services/auth.service';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = inject(AuthService).getToken();
if (!token) return next(req);
return next(req.clone({
headers: req.headers.set('Authorization', `Bearer ${token}`)
}));
};import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './interceptors/auth.interceptor';
import { errorHandlerInterceptor } from './interceptors/error-handler.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withInterceptors([authInterceptor, errorHandlerInterceptor])
),
]
};Requisitos: [JDK 11]
Server docker run -d --name sonarqube -e SONAR_ES_BOOTSTRAP_CHECKS_DISABLE=true -p 9000:9000 sonarqube:latest2. Loguearte en el localhost con en http://localhost:9000/
user: admin | pass: adminnpm i sonar-scanner --save-devfile: package.json
"scripts": {
"sonar": "sonar-scanner"
}echo > sonar-project.propertiesfile: sonar-project.properties
# must be unique in a given SonarQube instance
sonar.projectKey=_nameapp
sonar.login=admin
sonar.password=root
# --- optional properties ---
# defaults to project key
sonar.projectName=_nameproyect
# defaults to 'not provided'
sonar.projectVersion=1.0
# Path is relative to the sonar-project.properties file. Defaults to .
sonar.sources=src
# Encoding of the source code. Default is default system encoding
sonar.sourceEncoding=UTF-8
sonar.exclusions=**/node_modules/**
sonar.test=src
npm run sonarnpm install @sentry/angular-ivynpm install @sentry/angularimport { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import { NgModule, ErrorHandler } from "@angular/core";
// import * as Sentry from "@sentry/angular"; // for Angular 10 and 11 instead
import * as Sentry from "@sentry/angular-ivy";
Sentry.init({
dsn: "https://<key>@sentry.io/<project>"
});
@NgModule({
// ...
providers: [
{
provide: ErrorHandler,
useValue: Sentry.createErrorHandler({
showDialog: true,
}),
},
],
// ...
})
class AppModule {}
platformBrowserDynamic()
.bootstrapModule(AppModule)
.then(success => console.log('Bootstrap success'))
.catch(err => console.error(err));npm install cypress --save-devFilename: Angular.json`
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"karmaConfig": "karma.conf.js",
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json"Filename: karma.conf.ts
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma'),
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true // removes the duplicated traces
},
coverageReporter: {
dir: require('path').join(__dirname, './coverage/flash'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
]
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome','ChromeHeadless'],
customLaunchers: {
ChromeHeadlessCI: {
base: 'ChromeHeadless',
flags: ['--no-sandbox'],
}
},
singleRun: false,
restartOnFileChange: true
});
};filename: angular.json
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"karmaConfig": "karma.conf.js",
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": []
}
}
⚠️ NgModuleyHttpClientModulefueron deprecados en Angular 19. En Angular 20 usá Standalone Components conprovideHttpClient.
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideZonelessChangeDetection } from '@angular/core';
import { routes } from './app.routes';
import { authInterceptor } from './interceptors/auth.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(
withInterceptors([authInterceptor])
),
provideZonelessChangeDetection(), // Angular 20 — sin Zone.js
]
};import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';
bootstrapApplication(AppComponent, appConfig).catch(console.error);import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
templateUrl: './app.component.html',
})
export class AppComponent {}La documentación permite tener comprensión en los procesos, metodos, variables etc. referencia: https://tsdoc.org/
En esta sección permite saber que tipos de usuario existen y donde se obtiene el tipo del usuario linkeando al metodo tambien podemos poner un @example
/**
* @property {string} tipoUsuario : Guarda el tipo de usuario {@link getUserInfo()}
* @example 'cliente' | 'marca'
*/
public tipoUsuario : string = '';/** Guarda el estado del usuario {@link isLogged()} */Podemos utilizar @author para saber quien creo la sección, la versión comunmente es mas utilizada en clases o componentes globales, un dato importante el @link solo funcionara si se encuentra dentro de la clase o si es una function fuera de la clase, también podemos usar function si no esta dentro de una clase
/**
* @author Chris Cobos
* @version 1.0.0
*
* @method {@link buscarProducto()} - Busca producto por evento change
* @method {@link obtenerProductos()} - Obtiene los productos por nombre con un limitante de 1 busqueda cada 1.5 segundos
* @method {@link buscarProductoEnter()} - Busca productos por evento de key press {enter}
* @method {@link navigate()} - Navega a la categoria seleccionada
*/ /**
* @param {CuponInterface} cupon - Cupon a aplicar
* @param {ProductoInterface[]} dataCarrito - Productos del carrito, se usaran para ver el descuento que tendra
* @param {MessageService} messageService - Servicio de mensajes de primeng
*/Podemos dar una descripción y los casos de usos que pueden existir en el componente
/**
* @author Chris Cobos
* @version 1.0.0
* ------------------------------------- DESCRIPCION -----------------------------------------------------------------------------
* se encarga de aplicar los descuentos de los cupones a los productos del carrito de compras
*
* ------------------------------------- ALCACNCE DE LOS CUPONES -----------------------------------------------------------------
* 1. Descuento por todos los productos marca, solo aplicara en productos de la marca
* 2. Descuento por producto especifico marca, solo aplicara en el producto especifico de la marca
* 3. Descuento por categoria, solo aplicara en productos de la categoria
*/En Angular 20, Zone.js es opcional. Sin Zone.js, Angular solo actualiza el DOM cuando un Signal cambia — sin overhead de monkey-patching de toda la plataforma. Es la estrategia más eficiente disponible.
// app.config.ts
import { provideZonelessChangeDetection } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [
provideZonelessChangeDetection()
]
};Remové
zone.jsde lospolyfillsenangular.jsoncuando uses zoneless.
Si todavía usás Zone.js, OnPush es el mínimo recomendado. Solo re-renderiza cuando cambia un @Input por referencia o se emite un Observable.
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'app-product-list',
standalone: true,
templateUrl: './product-list.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductListComponent {}Sin trackBy, Angular destruye y recrea TODOS los nodos del DOM cuando cambia el array. Con trackBy solo actualiza los elementos que realmente cambiaron.
// ❌ Sin trackBy — recrea todos los nodos
<li *ngFor="let product of products">{{ product.name }}</li>
// ✅ Con trackBy — solo actualiza lo que cambió
<li *ngFor="let product of products; trackBy: trackById">{{ product.name }}</li>trackById(index: number, product: ProductInterface): number {
return product.id;
}Los métodos en el template se ejecutan en CADA ciclo de detección de cambios. Un pipe puro solo se recalcula cuando cambia su input.
// ❌ Método en template — se ejecuta constantemente
<span>{{ formatPrice(product.price) }}</span>
// ✅ Pure pipe — solo cuando cambia product.price
<span>{{ product.price | formatPrice }}</span>@Pipe({ name: 'formatPrice', pure: true })
export class FormatPricePipe implements PipeTransform {
transform(value: number): string {
return `$${value.toFixed(2)}`;
}
}Cargá los módulos solo cuando el usuario navega a esa ruta.
// app.routing.ts
const routes: Routes = [
{
path: 'products',
loadChildren: () => import('./products/products.module').then(m => m.ProductsModule)
}
];Con @defer podés posponer la carga de componentes pesados hasta que sean visibles o necesarios.
@defer (on viewport) {
<app-heavy-chart />
} @placeholder {
<div>Cargando gráfico...</div>
}Cuando importás una interfaz o tipo, usá import type. TypeScript lo elimina completamente del bundle en tiempo de compilación porque no genera código JavaScript.
// ❌ Import normal — puede incluirse en el bundle
import { ProductsOrder } from "../interfaces/products-order-interface";
// ✅ import type — TypeScript lo elimina del bundle, cero overhead
import type { ProductsOrder } from "../interfaces/products-order-interface";Habilitalo automáticamente con
"verbatimModuleSyntax": trueentsconfig.json— TypeScript te va a forzar a usarimport typecuando corresponda.
// tsconfig.json
{
"compilerOptions": {
"verbatimModuleSyntax": true
}
}Un barrel export (index.ts) puede hacer que el bundler incluya más código del necesario si no tiene buena capacidad de tree-shaking.
// ❌ Barrel import — puede traer todo el módulo
import { formatDate, formatCurrency, formatNumber } from '@angular/common';
// ✅ Solo importá lo que usás — facilita el tree-shaking
import { formatDate } from '@angular/common';Usá providedIn: 'root' en lugar de declarar el servicio en providers[] del módulo. Si el servicio no se usa, Angular lo elimina del bundle.
// ❌ Declarado en el módulo — siempre se incluye en el bundle
@NgModule({
providers: [ProductService]
})
// ✅ Tree-shakeable — solo se incluye si hay un consumidor
@Injectable({
providedIn: 'root'
})
export class ProductService {}npm install source-map-explorer --save-dev// package.json
"scripts": {
"analyze": "ng build --source-map && source-map-explorer dist/**/*.js"
}npm run analyzeEsto abre un mapa visual para ver qué librerías están ocupando más espacio en tu bundle.
Los standalone components eliminan la necesidad de NgModule, lo que mejora el tree-shaking porque Angular solo incluye las dependencias que el componente declara explícitamente.
@Component({
selector: 'app-product-card',
standalone: true,
imports: [CommonModule, RouterModule], // solo lo que usa este componente
templateUrl: './product-card.component.html',
})
export class ProductCardComponent {}This project was generated with Angular CLI version 15.2.4.
Run ng serve for a dev server. Navigate to http://localhost:4200/. The application will automatically reload if you change any of the source files.
Run ng generate component component-name to generate a new component. You can also use ng generate directive|pipe|service|class|guard|interface|enum|module.
Run ng build to build the project. The build artifacts will be stored in the dist/ directory.
Run ng test to execute the unit tests via Karma.
Run ng e2e to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
To get more help on the Angular CLI use ng help or go check out the Angular CLI Overview and Command Reference page.