Angular 19+ standalone conventions with signals, zoneless change detection, inject(), reactive forms, and modern patterns
From beenpx claudepluginhub george-popescu/bee-dev --plugin beeThis skill uses the workspace's default tool permissions.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Delivers DB-free sandbox API regression tests for Next.js/Vitest to catch AI blind spots in self-reviewed code changes like API routes and backend logic.
These standards apply when the project stack is angular. All agents and implementations must follow these conventions.
Also read skills/standards/frontend/SKILL.md for universal frontend standards (component architecture, accessibility, responsive design, CSS methodology, design quality) that apply alongside these Angular-specific conventions.
standalone: true is default in Angular 19+). No NgModules for components.ChangeDetectionStrategy.OnPush on EVERY component. No exceptions. This is critical for performance with signals..html for larger templates.selector follows kebab-case with app prefix: app-order-list, app-user-profile.import { ChangeDetectionStrategy, Component, input, output, computed } from '@angular/core';
@Component({
selector: 'app-order-card',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="card" [class.urgent]="isUrgent()">
<h3>{{ order().name }}</h3>
<span class="status">{{ order().status }}</span>
<button (click)="selected.emit(order())">View</button>
</div>
`,
})
export class OrderCardComponent {
readonly order = input.required<Order>();
readonly selected = output<Order>();
protected readonly isUrgent = computed(() => this.order().priority === 'high');
}
input() signal function for component inputs (replaces @Input() decorator).input.required<T>() for mandatory inputs.output() for event emitters (replaces @Output() + EventEmitter).model() for two-way binding inputs.computed() for derived state from signals — replaces getter properties.// Modern API (Angular 19+)
readonly name = input<string>(''); // optional with default
readonly userId = input.required<string>(); // required
readonly closed = output<void>(); // event emitter
readonly value = model<string>(''); // two-way [(value)]
readonly displayName = computed(() => this.name() || 'Anonymous');
// ❌ Legacy — do NOT use
@Input() name: string = '';
@Output() closed = new EventEmitter<void>();
signal(initialValue) — writable reactive primitive. Access value with (), update with .set() or .update().computed(() => expression) — derived signal, auto-tracks dependencies. Read-only.effect(() => { ... }) — side effect that runs when tracked signals change. Use sparingly.linkedSignal(() => source) — signal linked to another signal's value, resettable.// State management with signals
readonly count = signal(0);
readonly items = signal<Item[]>([]);
readonly total = computed(() => this.items().reduce((sum, i) => sum + i.price, 0));
readonly isEmpty = computed(() => this.items().length === 0);
// Update patterns
this.count.set(5);
this.count.update(c => c + 1);
this.items.update(items => [...items, newItem]);
computed() for derived state. Never store derived values in separate signals.effect() only for external side effects (logging, analytics, localStorage sync). Never use effect to set other signals — use computed() instead.untracked() to read signals without tracking inside computed/effect..update(). Never mutate in place.import { resource, signal } from '@angular/core';
readonly userId = signal<string>('123');
readonly userResource = resource({
request: () => ({ id: this.userId() }),
loader: async ({ request, abortSignal }) => {
const response = await fetch(`/api/users/${request.id}`, { signal: abortSignal });
return response.json() as Promise<User>;
},
});
// In template:
// @if (userResource.isLoading()) { <spinner /> }
// @if (userResource.value(); as user) { <user-card [user]="user" /> }
// @if (userResource.error()) { <error-message /> }
inject() Function (Preferred)inject() function instead of constructor injection. Cleaner, works with signals.@Injectable({ providedIn: 'root' }) for app-wide singletons.providers: [MyService] in @Component for component-level DI.@Component({ ... })
export class OrderListComponent {
private readonly orderService = inject(OrderService);
private readonly router = inject(Router);
private readonly route = inject(ActivatedRoute);
readonly orders = signal<Order[]>([]);
constructor() {
// Load data in constructor or use resource()
this.loadOrders();
}
private async loadOrders() {
const data = await firstValueFrom(this.orderService.getOrders());
this.orders.set(data);
}
}
OrderService, AuthService, NotificationService.Observable<T> from HTTP methods. Use HttpClient with typed responses.DestroyRef + takeUntilDestroyed() for subscription cleanup.@Injectable({ providedIn: 'root' })
export class OrderService {
private readonly http = inject(HttpClient);
getOrders(): Observable<Order[]> {
return this.http.get<Order[]>('/api/orders');
}
getOrder(id: string): Observable<Order> {
return this.http.get<Order>(`/api/orders/${id}`);
}
createOrder(data: CreateOrderDto): Observable<Order> {
return this.http.post<Order>('/api/orders', data);
}
}
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(
withInterceptors([authInterceptor, errorInterceptor]),
),
provideZonelessChangeDetection(), // if using zoneless
],
};
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService);
const token = authService.getToken();
if (token) {
req = req.clone({
setHeaders: { Authorization: `Bearer ${token}` },
});
}
return next(req);
};
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
inject(Router).navigate(['/login']);
}
return throwError(() => error);
}),
);
};
export const routes: Routes = [
{ path: '', component: HomeComponent },
{
path: 'orders',
children: [
{ path: '', component: OrderListComponent },
{ path: ':id', component: OrderDetailComponent },
{ path: ':id/edit', component: OrderEditComponent },
],
},
{
path: 'admin',
canActivate: [authGuard],
loadChildren: () => import('./admin/admin.routes').then(m => m.ADMIN_ROUTES),
},
{ path: '**', component: NotFoundComponent },
];
export const authGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isAuthenticated()) return true;
return router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url } });
};
export const orderResolver: ResolveFn<Order> = (route) => {
const orderService = inject(OrderService);
return orderService.getOrder(route.paramMap.get('id')!);
};
// In routes config:
{ path: ':id', component: OrderDetailComponent, resolve: { order: orderResolver } }
// In component:
readonly order = toSignal(inject(ActivatedRoute).data.pipe(map(d => d['order'] as Order)));
@Component({
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<input formControlName="name" />
@if (form.controls.name.errors?.['required'] && form.controls.name.touched) {
<span class="error">Name is required</span>
}
<input formControlName="email" type="email" />
<button type="submit" [disabled]="form.invalid">Save</button>
</form>
`,
})
export class OrderFormComponent {
private readonly fb = inject(FormBuilder);
private readonly orderService = inject(OrderService);
readonly form = this.fb.group({
name: ['', [Validators.required, Validators.minLength(3)]],
email: ['', [Validators.required, Validators.email]],
notes: [''],
});
onSubmit() {
if (this.form.valid) {
this.orderService.createOrder(this.form.getRawValue()).subscribe();
}
}
}
function passwordMatchValidator(group: AbstractControl): ValidationErrors | null {
const password = group.get('password')?.value;
const confirm = group.get('confirmPassword')?.value;
return password === confirm ? null : { passwordMismatch: true };
}
<!-- @if / @else -->
@if (isLoading()) {
<app-spinner />
} @else if (error()) {
<app-error [message]="error()" />
} @else {
<app-order-list [orders]="orders()" />
}
<!-- @for with required track -->
@for (order of orders(); track order.id) {
<app-order-card [order]="order" />
} @empty {
<p>No orders found</p>
}
<!-- @switch -->
@switch (status()) {
@case ('active') { <span class="badge-green">Active</span> }
@case ('pending') { <span class="badge-yellow">Pending</span> }
@default { <span class="badge-gray">Unknown</span> }
}
<!-- @defer for lazy loading -->
@defer (on viewport) {
<app-heavy-chart [data]="chartData()" />
} @placeholder {
<div class="skeleton-chart"></div>
} @loading (minimum 500ms) {
<app-spinner />
}
Detect what the project uses — check package.json for installed state management libraries and follow THAT library's conventions.
createFeature(), createActionGroup(), typed selectors. Never dispatch actions from services directly — use effects.signalStore() with withState(), withComputed(), withMethods(). Preferred for new NgRx projects.If no external store is installed: Use Angular signals in @Injectable services. Services hold signal() state, expose computed() selectors and methods for updates.
// Simple signal-based store (no external library)
@Injectable({ providedIn: 'root' })
export class CartStore {
readonly items = signal<CartItem[]>([]);
readonly total = computed(() => this.items().reduce((sum, i) => sum + i.price * i.qty, 0));
readonly count = computed(() => this.items().length);
addItem(item: CartItem) {
this.items.update(items => [...items, item]);
}
removeItem(id: string) {
this.items.update(items => items.filter(i => i.id !== id));
}
}
TestBed.configureTestingModule() for DI setup in tests.TestBed.inject() to get service instances.HttpTestingController for mocking HTTP calls.describe('OrderListComponent', () => {
let component: OrderListComponent;
let fixture: ComponentFixture<OrderListComponent>;
let httpTesting: HttpTestingController;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [OrderListComponent],
providers: [
provideHttpClient(),
provideHttpClientTesting(),
],
}).compileComponents();
fixture = TestBed.createComponent(OrderListComponent);
component = fixture.componentInstance;
httpTesting = TestBed.inject(HttpTestingController);
});
it('should display orders after loading', () => {
fixture.detectChanges(); // triggers initial load
const req = httpTesting.expectOne('/api/orders');
req.flush([{ id: '1', name: 'Order A' }, { id: '2', name: 'Order B' }]);
fixture.detectChanges();
const items = fixture.nativeElement.querySelectorAll('.order-card');
expect(items.length).toBe(2);
});
});
ng) for scaffolding, building, testing, serving.ng serve for dev server with HMR.ng build for production build (tree-shaking, AOT compilation, minification).ng generate component/service/guard/pipe for scaffolding.environment.ts / environment.prod.ts for env-specific config.tsconfig.json paths (e.g., @app/*, @shared/*)."strict": true in tsconfig.json. No exceptions.ChangeDetectionStrategy.Default — always OnPush with signals.@Input() / @Output() decorators — use input(), output(), model() signal functions.ngOnInit for simple initialization — use constructor or effect().ngClass or ngStyle — use direct [class.x] and [style.x] bindings.*ngIf, *ngFor, *ngSwitch — use @if, @for, @switch control flow.any type — define proper interfaces and types.track in @for — it's mandatory. Use a stable unique identifier.takeUntilDestroyed() or toSignal().imports.inject() function.EventEmitter directly — use output()..update().effect() to set other signals — use computed() for derived state."strict": true in tsconfig.json. No escape hatches.ChangeDetectionStrategy.OnPush.signal(), computed(), input(), output() for all reactive state.inject() for DI. All dependency injection uses the inject() function.FormBuilder + ReactiveFormsModule. No template-driven forms.@if, @for, @switch, @defer — no structural directives.toSignal() for Observable-to-Signal. Convert RxJS observables to signals for template use.@defer for heavy components. Lazy-load analytics, charts, editors with viewport trigger.loadChildren or loadComponent for lazy routes.DestroyRef + takeUntilDestroyed() for subscription cleanup in services.index.ts. Feature directories export public API through barrel files.package.json — follow NgRx/Akita/Elf patterns, don't introduce new ones.provideZonelessChangeDetection() for new projects — eliminates Zone.js overhead.resource() for async data. Use the Resource API for declarative async data loading with signals.track in @for. Forgetting track causes runtime error. Always: @for (item of items(); track item.id)..subscribe() without takeUntilDestroyed() causes memory leaks on component destroy.items().push(newItem) doesn't trigger updates. Use items.update(i => [...i, newItem]).effect() for derived state. effect(() => this.total.set(this.items().length)) — use computed() instead.afterNextRender().imports array — they won't render but no compile error.this.http.get<T>() without catchError — unhandled errors crash the observable chain.route.snapshot.paramMap once — stale when params change. Use route.paramMap observable or toSignal().constructor(private service: MyService) — use inject() function instead.*ngIf / *ngFor structural directives. Legacy syntax. Use @if / @for control flow.any type. Disables TypeScript checking. Define proper interfaces.document.querySelector() or ElementRef.nativeElement directly. Use Angular APIs.@Input() chains. Prop drilling through 3+ components. Use a signal-based service or state management library.BehaviorSubject when signal() works.src/app/
features/
orders/
order-list.component.ts
order-detail.component.ts
order.service.ts
order.model.ts
order.routes.ts
index.ts
auth/
login.component.ts
auth.service.ts
auth.guard.ts
auth.interceptor.ts
index.ts
shared/
components/
pipes/
directives/
core/
services/
interceptors/
guards/
order-list.component.ts, auth.service.ts, auth.guard.ts.OrderListComponent, AuthService, OrderDetailResolver..component.ts, .service.ts, .guard.ts, .pipe.ts, .directive.ts, .interceptor.ts.app- selector prefix. All component selectors start with app- (configurable in angular.json).index.ts. Import from @app/features/orders not deep paths.When looking up framework documentation, use these Context7 library identifiers:
/websites/v20_angular_dev — components, signals, DI, routing, forms, testing, best practices/angular/angular — source-level API detailsReactiveX/rxjs — observables, operators, subjectsngrx/platform — store, effects, signals store, entityvitest-dev/vitest — test runner, assertions, mockingAlways check Context7 for the latest Angular API — signals, control flow, and zoneless change detection are evolving rapidly between versions.