angular-expert

Expert-level Angular patterns for components, RxJS, state management, and performance.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "angular-expert" with this command: npx skills add nguyenthienthanh/aura-frog/nguyenthienthanh-aura-frog-angular-expert

Angular Expert Skill

Expert-level Angular patterns for components, RxJS, state management, and performance.

Auto-Detection

This skill activates when:

  • Working with Angular projects

  • Detected angular.json or @angular/core in package.json

  • Working with *.component.ts , *.service.ts files

  • Using RxJS, NgRx, or Angular Material

  1. Component Best Practices

Standalone Components (Angular 17+)

// ✅ GOOD - Standalone component @Component({ selector: 'app-user-card', standalone: true, imports: [CommonModule, RouterLink], template: &#x3C;div class="user-card"> &#x3C;h3>{{ user.name }}&#x3C;/h3> &#x3C;p>{{ user.email }}&#x3C;/p> &#x3C;a [routerLink]="['/users', user.id]">View Profile&#x3C;/a> &#x3C;/div> , changeDetection: ChangeDetectionStrategy.OnPush, }) export class UserCardComponent { @Input({ required: true }) user!: User; @Output() selected = new EventEmitter<User>(); }

Signal-Based Components (Angular 17+)

// ✅ GOOD - Signals for reactive state @Component({ selector: 'app-counter', standalone: true, template: &#x3C;div> &#x3C;p>Count: {{ count() }}&#x3C;/p> &#x3C;p>Double: {{ doubleCount() }}&#x3C;/p> &#x3C;button (click)="increment()">+&#x3C;/button> &#x3C;/div> , changeDetection: ChangeDetectionStrategy.OnPush, }) export class CounterComponent { count = signal(0); doubleCount = computed(() => this.count() * 2);

increment() { this.count.update(c => c + 1); } }

Smart vs Dumb Components

// ✅ GOOD - Container (Smart) component @Component({ selector: 'app-users-container', standalone: true, imports: [UserListComponent], template: &#x3C;app-user-list [users]="users()" [loading]="loading()" (userSelected)="onUserSelected($event)" /> , }) export class UsersContainerComponent { private userService = inject(UserService);

users = signal<User[]>([]); loading = signal(false);

constructor() { this.loadUsers(); }

private async loadUsers() { this.loading.set(true); this.users.set(await this.userService.getUsers()); this.loading.set(false); }

onUserSelected(user: User) { this.userService.selectUser(user); } }

// ✅ GOOD - Presentational (Dumb) component @Component({ selector: 'app-user-list', standalone: true, imports: [CommonModule], template: @if (loading) { &#x3C;div class="loading">Loading...&#x3C;/div> } @else { &#x3C;ul> @for (user of users; track user.id) { &#x3C;li (click)="userSelected.emit(user)"> {{ user.name }} &#x3C;/li> } &#x3C;/ul> } , changeDetection: ChangeDetectionStrategy.OnPush, }) export class UserListComponent { @Input() users: User[] = []; @Input() loading = false; @Output() userSelected = new EventEmitter<User>(); }

  1. Services & Dependency Injection

Injectable Services

// ✅ GOOD - Service with inject() @Injectable({ providedIn: 'root' }) export class UserService { private http = inject(HttpClient); private baseUrl = inject(API_BASE_URL);

getUsers(): Observable<User[]> { return this.http.get<User[]>(${this.baseUrl}/users); }

getUser(id: string): Observable<User> { return this.http.get<User>(${this.baseUrl}/users/${id}); }

createUser(user: CreateUserDto): Observable<User> { return this.http.post<User>(${this.baseUrl}/users, user); } }

Injection Tokens

// ✅ GOOD - Injection tokens for config export const API_BASE_URL = new InjectionToken<string>('API_BASE_URL');

// In app.config.ts export const appConfig: ApplicationConfig = { providers: [ { provide: API_BASE_URL, useValue: environment.apiUrl }, ], };

  1. RxJS Best Practices

Declarative Streams

// ✅ GOOD - Declarative with signals @Component({...}) export class UsersComponent { private userService = inject(UserService); private route = inject(ActivatedRoute);

// Derived state from route params private userId = toSignal( this.route.paramMap.pipe(map(params => params.get('id'))) );

user = toSignal( toObservable(this.userId).pipe( filter((id): id is string => id != null), switchMap(id => this.userService.getUser(id)), ) ); }

Error Handling

// ✅ GOOD - catchError with recovery getUsers(): Observable<User[]> { return this.http.get<User[]>('/api/users').pipe( retry({ count: 3, delay: 1000 }), catchError(error => { console.error('Failed to fetch users', error); return of([]); // Return empty array on error }), ); }

// ✅ GOOD - Error handling in component @Component({...}) export class UsersComponent { users$ = this.userService.getUsers().pipe( catchError(error => { this.errorMessage.set(error.message); return EMPTY; }), );

errorMessage = signal<string | null>(null); }

Unsubscribe Patterns

// ✅ GOOD - takeUntilDestroyed @Component({...}) export class MyComponent { private destroyRef = inject(DestroyRef);

ngOnInit() { this.someObservable$ .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(value => { // Handle value }); } }

// ✅ GOOD - async pipe (auto-unsubscribes) @Component({ template: @if (users$ | async; as users) { &#x3C;app-user-list [users]="users" /> } , }) export class UsersComponent { users$ = this.userService.getUsers(); }

  1. State Management (NgRx)

Feature Store

// ✅ GOOD - NgRx feature with createFeature export const usersFeature = createFeature({ name: 'users', reducer: createReducer( initialState, on(UsersActions.loadUsers, state => ({ ...state, loading: true })), on(UsersActions.loadUsersSuccess, (state, { users }) => ({ ...state, users, loading: false, })), on(UsersActions.loadUsersFailure, (state, { error }) => ({ ...state, error, loading: false, })), ), });

export const { selectUsers, selectLoading, selectError, } = usersFeature;

Actions

// ✅ GOOD - createActionGroup export const UsersActions = createActionGroup({ source: 'Users', events: { 'Load Users': emptyProps(), 'Load Users Success': props<{ users: User[] }>(), 'Load Users Failure': props<{ error: string }>(), 'Select User': props<{ userId: string }>(), }, });

Effects

// ✅ GOOD - Functional effects export const loadUsers = createEffect( (actions$ = inject(Actions), userService = inject(UserService)) => { return actions$.pipe( ofType(UsersActions.loadUsers), exhaustMap(() => userService.getUsers().pipe( map(users => UsersActions.loadUsersSuccess({ users })), catchError(error => of(UsersActions.loadUsersFailure({ error: error.message })) ), ), ), ); }, { functional: true }, );

  1. Forms

Reactive Forms

// ✅ GOOD - Typed reactive forms @Component({...}) export class UserFormComponent { private fb = inject(NonNullableFormBuilder);

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

onSubmit() { if (this.form.valid) { const value = this.form.getRawValue(); // value is typed: { email: string; name: string; password: string } this.save(value); } } }

Template

<form [formGroup]="form" (ngSubmit)="onSubmit()"> <div> <label for="email">Email</label> <input id="email" formControlName="email" type="email"> @if (form.controls.email.errors?.['required']) { <span class="error">Email is required</span> } @if (form.controls.email.errors?.['email']) { <span class="error">Invalid email format</span> } </div>

<button type="submit" [disabled]="form.invalid">Submit</button> </form>

  1. Routing

Lazy Loading

// ✅ GOOD - Lazy loaded routes export const routes: Routes = [ { path: 'users', loadComponent: () => import('./users/users.component').then(m => m.UsersComponent), children: [ { path: ':id', loadComponent: () => import('./users/user-detail.component').then(m => m.UserDetailComponent), }, ], }, ];

Route Guards

// ✅ GOOD - Functional guards 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 }, }); };

Resolvers

// ✅ GOOD - Functional resolver export const userResolver: ResolveFn<User> = (route) => { const userService = inject(UserService); const userId = route.paramMap.get('id')!; return userService.getUser(userId); };

// Usage in routes { path: ':id', component: UserDetailComponent, resolve: { user: userResolver }, }

  1. Performance Optimization

OnPush Change Detection

// ✅ GOOD - Always use OnPush @Component({ changeDetection: ChangeDetectionStrategy.OnPush, }) export class MyComponent {}

trackBy for ngFor

// ✅ GOOD - trackBy for lists @Component({ template: @for (user of users; track user.id) { &#x3C;app-user-card [user]="user" /> } , }) export class UsersComponent { users: User[] = []; }

Defer Loading

// ✅ GOOD - Defer heavy components @Component({ template: @defer (on viewport) { &#x3C;app-heavy-component /> } @placeholder { &#x3C;div class="skeleton">&#x3C;/div> } @loading { &#x3C;div class="spinner">&#x3C;/div> } , }) export class MyComponent {}

  1. HTTP Interceptors

// ✅ GOOD - Functional interceptor 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); };

// In app.config.ts export const appConfig: ApplicationConfig = { providers: [ provideHttpClient(withInterceptors([authInterceptor])), ], };

  1. Testing

// ✅ GOOD - Component testing describe('UserCardComponent', () => { let component: UserCardComponent; let fixture: ComponentFixture<UserCardComponent>;

beforeEach(async () => { await TestBed.configureTestingModule({ imports: [UserCardComponent], }).compileComponents();

fixture = TestBed.createComponent(UserCardComponent);
component = fixture.componentInstance;

});

it('should display user name', () => { component.user = { id: '1', name: 'John', email: 'john@example.com' }; fixture.detectChanges();

const nameElement = fixture.nativeElement.querySelector('h3');
expect(nameElement.textContent).toContain('John');

});

it('should emit when clicked', () => { component.user = { id: '1', name: 'John', email: 'john@example.com' }; jest.spyOn(component.selected, 'emit');

fixture.nativeElement.querySelector('.user-card').click();

expect(component.selected.emit).toHaveBeenCalledWith(component.user);

}); });

Quick Reference

checklist[12]{pattern,best_practice}: Components,Standalone + OnPush + Signals State,Signals for local NgRx for global Forms,NonNullableFormBuilder typed RxJS,takeUntilDestroyed + async pipe Routes,Lazy loading + functional guards DI,inject() function Lists,@for with track Defer,@defer for heavy components HTTP,Functional interceptors Testing,ComponentFixture + TestBed Errors,catchError with recovery Smart/Dumb,Container vs presentational

Version: 1.3.0

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

stitch-design

No summary provided by upstream source.

Repository SourceNeeds Review
General

visual-pixel-perfect

No summary provided by upstream source.

Repository SourceNeeds Review
General

nativewind-generator

No summary provided by upstream source.

Repository SourceNeeds Review
General

seo-expert

No summary provided by upstream source.

Repository SourceNeeds Review