angular-21

star 0

Guía completa de Angular 21 con signals, control flow moderno, componentes standalone, zoneless change detection, SSR con hydration incremental, routing funcional, formularios reactivos, HTTP Client moderno y testing. Usar cuando el usuario trabaje con Angular, cree componentes, servicios, rutas, formularios, peticiones HTTP o pregunte sobre mejores prácticas de Angular.

dannielnavas By dannielnavas schedule Updated 2/25/2026

name: angular-21 description: Guía completa de Angular 21 con signals, control flow moderno, componentes standalone, zoneless change detection, SSR con hydration incremental, routing funcional, formularios reactivos, HTTP Client moderno y testing. Usar cuando el usuario trabaje con Angular, cree componentes, servicios, rutas, formularios, peticiones HTTP o pregunte sobre mejores prácticas de Angular.

Angular 21

Principios clave

  • Standalone es el default — nunca uses NgModules a menos que sea legado.
  • Signals son la API reactiva primaria — prefiere signal, computed, effect sobre observables donde sea posible.
  • Control flow moderno: usa @if, @for, @switch, @defer en templates.
  • Zoneless change detection es estable — configúralo en proyectos nuevos.
  • rsResource para realizar las peticiones fetch por defecto en el caso que cargue la informacion al inicio

Componentes

@Component({
  selector: "app-example",
  standalone: true,
  imports: [CommonModule],
  template: `...`,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ExampleComponent {
  // Signals de entrada/salida
  title = input<string>("default"); // reemplaza @Input
  count = input.required<number>(); // input obligatorio
  valueChange = output<string>(); // reemplaza @EventEmitter
  value = model<string>(""); // two-way binding con signal

  // Estado interno
  items = signal<string[]>([]);
  total = computed(() => this.items().length);

  // Efectos secundarios
  private logEffect = effect(() => {
    console.log("items changed:", this.items());
  });

  // ViewChild como signal (Angular 19+)
  inputRef = viewChild<ElementRef>("inputEl");
}

Mutations de signals

// Establecer valor
this.items.set(["a", "b"]);

// Mutar basado en valor actual
this.items.update((prev) => [...prev, "c"]);

Control Flow moderno

<!-- @if / @else if / @else -->
@if (user()) {
<p>Hola {{ user()!.name }}</p>
} @else {
<p>No hay usuario</p>
}

<!-- @for con track obligatorio -->
@for (item of items(); track item.id) {
<li>{{ item.name }}</li>
} @empty {
<li>Sin elementos</li>
}

<!-- @switch -->
@switch (status()) { @case ('active') { <span>Activo</span> } @case ('inactive') {
<span>Inactivo</span> } @default { <span>Desconocido</span> } }

<!-- @defer: carga diferida -->
@defer (on viewport) {
<heavy-component />
} @placeholder {
<p>Cargando...</p>
} @loading (minimum 500ms) {
<spinner />
} @error {
<p>Error al cargar</p>
}

Configuración de la aplicación

// main.ts
bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes, withComponentInputBinding()),
    provideHttpClient(withInterceptors([authInterceptor])),
    provideAnimationsAsync(),
    // Zoneless change detection
    provideZonelessChangeDetection(),
  ],
});

Routing

// app.routes.ts
export const routes: Routes = [
  {
    path: 'dashboard',
    loadComponent: () => import('./dashboard/dashboard.component').then(m => m.DashboardComponent),
    canActivate: [authGuard],
  },
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.routes').then(m => m.adminRoutes),
  },
];

// Guarda funcional
export const authGuard: CanActivateFn = (route, state) => {
  const auth = inject(AuthService);
  return auth.isLoggedIn() ? true : inject(Router).createUrlTree(['/login']);
};

// withComponentInputBinding: los route params llegan como @Input / input()
@Component({ ... })
export class ProductComponent {
  id = input<string>();  // se mapea automáticamente desde :id en la ruta
}

Servicios e inyección de dependencias

@Injectable({ providedIn: "root" })
export class UserService {
  private http = inject(HttpClient);
  private users = signal<User[]>([]);

  readonly users$ = this.users.asReadonly();

  getUsers() {
    return this.http
      .get<User[]>("/api/users")
      .pipe(tap((users) => this.users.set(users)));
  }
}

// inject() fuera del constructor (en funciones, guardas, etc.)
const service = inject(UserService);

HTTP Client

// Interceptor funcional
export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const token = inject(AuthService).token();
  const authReq = token
    ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })
    : req;
  return next(authReq);
};

// Uso en componente
private http = inject(HttpClient);

data = toSignal(
  this.http.get<Item[]>('/api/items'),
  { initialValue: [] }
);

Formularios reactivos

@Component({
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
      <input formControlName="email" />
      @if (form.get("email")?.invalid && form.get("email")?.touched) {
        <span>Email inválido</span>
      }
      <button type="submit" [disabled]="form.invalid">Enviar</button>
    </form>
  `,
})
export class LoginComponent {
  private fb = inject(FormBuilder);

  form = this.fb.nonNullable.group({
    email: ["", [Validators.required, Validators.email]],
    password: ["", [Validators.required, Validators.minLength(8)]],
  });

  onSubmit() {
    if (this.form.valid) {
      console.log(this.form.getRawValue());
    }
  }
}

Interoperabilidad Signals ↔ RxJS

import { toSignal, toObservable } from "@angular/core/rxjs-interop";

// Observable → Signal
data = toSignal(this.http.get<Item[]>("/api/items"), { initialValue: [] });

// Signal → Observable
count$ = toObservable(this.count);

SSR e Hydration incremental

// app.config.ts (SSR)
export const appConfig: ApplicationConfig = {
  providers: [
    provideClientHydration(withIncrementalHydration()),
    provideRouter(routes),
  ],
};
<!-- Hidratar solo cuando el componente entra en viewport -->
@defer (hydrate on viewport) {
<analytics-widget />
}

Testing

// Configuración de TestBed con standalone
describe("ExampleComponent", () => {
  let fixture: ComponentFixture<ExampleComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [ExampleComponent],
      providers: [
        provideHttpClientTesting(),
        { provide: AuthService, useValue: { isLoggedIn: signal(true) } },
      ],
    }).compileComponents();

    fixture = TestBed.createComponent(ExampleComponent);
    fixture.detectChanges();
  });

  it("should render title", () => {
    const el = fixture.nativeElement.querySelector("h1");
    expect(el.textContent).toContain("Expected Title");
  });

  it("should update on signal change", () => {
    fixture.componentRef.setInput("count", 5);
    fixture.detectChanges();
    expect(fixture.componentInstance.total()).toBe(5);
  });
});

Patrones comunes

Estado compartido con signals

@Injectable({ providedIn: "root" })
export class CartStore {
  private _items = signal<CartItem[]>([]);

  readonly items = this._items.asReadonly();
  readonly total = computed(() =>
    this._items().reduce((sum, i) => sum + i.price * i.quantity, 0),
  );

  add(item: CartItem) {
    this._items.update((prev) => [...prev, item]);
  }

  remove(id: string) {
    this._items.update((prev) => prev.filter((i) => i.id !== id));
  }
}

Carga con estado de loading/error

export class DataComponent {
  private svc = inject(DataService);

  protected vm = toSignal(
    this.svc.getData().pipe(
      map((data) => ({ data, loading: false, error: null })),
      startWith({ data: null, loading: true, error: null }),
      catchError((err) => of({ data: null, loading: false, error: err.message })),
    ),
  );
}

Anti-patrones a evitar

Evitar Usar en su lugar
NgModules en código nuevo Standalone components
*ngIf, *ngFor @if, @for
@Input() / @Output() input() / output() / model()
constructor(private svc: Service) svc = inject(Service)
ngOnChanges para reaccionar a inputs effect(() => { this.myInput(); })
ChangeDetectorRef.detectChanges() Signals + zoneless
Subscriptions manuales sin takeUntilDestroyed takeUntilDestroyed(this.destroyRef)

Recursos adicionales

  • Para patrones avanzados de signals y estado, ver reference.md
Install via CLI
npx skills add https://github.com/dannielnavas/focus-loop --skill angular-21
Repository Details
star Stars 0
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator
dannielnavas
dannielnavas Explore all skills →