From aura-frog
Provides expert Angular/TypeScript patterns for standalone components, signals, RxJS, NgRx state management, smart/dumb components, and performance.
How this skill is triggered — by the user, by Claude, or both
Slash command
/aura-frog:angular-expertThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Expert-level Angular patterns for components, RxJS, state management, and performance.
Expert-level Angular patterns for components, RxJS, state management, and performance.
This skill activates when:
angular.json or @angular/core in package.json*.component.ts, *.service.ts files// ✅ GOOD - Standalone component
@Component({
selector: 'app-user-card',
standalone: true,
imports: [CommonModule, RouterLink],
template: `
<div class="user-card">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
<a [routerLink]="['/users', user.id]">View Profile</a>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserCardComponent {
@Input({ required: true }) user!: User;
@Output() selected = new EventEmitter<User>();
}
// ✅ GOOD - Signals for reactive state
@Component({
selector: 'app-counter',
standalone: true,
template: `
<div>
<p>Count: {{ count() }}</p>
<p>Double: {{ doubleCount() }}</p>
<button (click)="increment()">+</button>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CounterComponent {
count = signal(0);
doubleCount = computed(() => this.count() * 2);
increment() {
this.count.update(c => c + 1);
}
}
// ✅ GOOD - Container (Smart) component
@Component({
selector: 'app-users-container',
standalone: true,
imports: [UserListComponent],
template: `
<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) {
<div class="loading">Loading...</div>
} @else {
<ul>
@for (user of users; track user.id) {
<li (click)="userSelected.emit(user)">
{{ user.name }}
</li>
}
</ul>
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserListComponent {
@Input() users: User[] = [];
@Input() loading = false;
@Output() userSelected = new EventEmitter<User>();
}
// ✅ 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);
}
}
// ✅ 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 },
],
};
// ✅ 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)),
)
);
}
// ✅ 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);
}
// ✅ 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) {
<app-user-list [users]="users" />
}
`,
})
export class UsersComponent {
users$ = this.userService.getUsers();
}
// ✅ 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;
// ✅ 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 }>(),
},
});
// ✅ 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 },
);
// ✅ 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);
}
}
}
<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>
// ✅ 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),
},
],
},
];
// ✅ 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 },
});
};
// ✅ 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 },
}
// ✅ GOOD - Always use OnPush
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MyComponent {}
// ✅ GOOD - trackBy for lists
@Component({
template: `
@for (user of users; track user.id) {
<app-user-card [user]="user" />
}
`,
})
export class UsersComponent {
users: User[] = [];
}
// ✅ GOOD - Defer heavy components
@Component({
template: `
@defer (on viewport) {
<app-heavy-component />
} @placeholder {
<div class="skeleton"></div>
} @loading {
<div class="spinner"></div>
}
`,
})
export class MyComponent {}
// ✅ 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])),
],
};
// ✅ 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: '[email protected]' };
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: '[email protected]' };
jest.spyOn(component.selected, 'emit');
fixture.nativeElement.querySelector('.user-card').click();
expect(component.selected.emit).toHaveBeenCalledWith(component.user);
});
});
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
npx claudepluginhub nguyenthienthanh/aura-frog --plugin aura-frogProvides Angular 14+ expertise for standalone components, signals, RxJS patterns, NgRx state management, dependency injection, lazy loading routes, OnPush change detection, and testing.
Generates Angular code and provides architectural guidance on signals, forms, DI, routing, SSR, accessibility, animations, styling, testing, and CLI tooling.
Provides expertise for modern Angular v20+ apps using Signals, Standalone Components, Zoneless change detection, SSR/Hydration, and reactive patterns. Ideal for new builds and performance optimization.