Gentleman-Skills angular-core
install
source · Clone the upstream repo
git clone https://github.com/Gentleman-Programming/Gentleman-Skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Gentleman-Programming/Gentleman-Skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/curated/angular/core" ~/.claude/skills/gentleman-programming-gentleman-skills-angular-core && rm -rf "$T"
manifest:
curated/angular/core/SKILL.mdsource content
Standalone Components (REQUIRED)
Components are standalone by default. Do NOT set
standalone: true.
@Component({ selector: 'app-user', imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `...` }) export class UserComponent {}
Input/Output Functions (REQUIRED)
// ✅ ALWAYS: Function-based readonly user = input.required<User>(); readonly disabled = input(false); readonly selected = output<User>(); readonly checked = model(false); // Two-way binding // ❌ NEVER: Decorators @Input() user: User; @Output() selected = new EventEmitter<User>();
Signals for State (REQUIRED)
readonly count = signal(0); readonly doubled = computed(() => this.count() * 2); // Update this.count.set(5); this.count.update(prev => prev + 1); // Side effects effect(() => localStorage.setItem('count', this.count().toString()));
NO Lifecycle Hooks (REQUIRED)
Signals replace lifecycle hooks. Do NOT use
ngOnInit, ngOnChanges, ngOnDestroy.
// ❌ NEVER: Lifecycle hooks ngOnInit() { this.loadUser(); } ngOnChanges(changes: SimpleChanges) { if (changes['userId']) { this.loadUser(); } } // ✅ ALWAYS: Signals + effect readonly userId = input.required<string>(); readonly user = signal<User | null>(null); private userEffect = effect(() => { // Runs automatically when userId() changes this.loadUser(this.userId()); }); // ✅ For derived data, use computed readonly displayName = computed(() => this.user()?.name ?? 'Guest');
When to Use What
| Need | Use |
|---|---|
| React to input changes | watching the input signal |
| Derived/computed state | |
| Side effects (API calls, localStorage) | |
| Cleanup on destroy | + |
// Cleanup example private readonly destroyRef = inject(DestroyRef); constructor() { const subscription = someObservable$.subscribe(); this.destroyRef.onDestroy(() => subscription.unsubscribe()); }
inject() Over Constructor (REQUIRED)
// ✅ ALWAYS private readonly http = inject(HttpClient); // ❌ NEVER constructor(private http: HttpClient) {}
Native Control Flow (REQUIRED)
@if (loading()) { <spinner /> } @else { @for (item of items(); track item.id) { <item-card [data]="item" /> } @empty { <p>No items</p> } } @switch (status()) { @case ('active') { <span>Active</span> } @default { <span>Unknown</span> } }
RxJS - Only When Needed
Signals are the default. Use RxJS ONLY for complex async operations.
| Use Signals | Use RxJS |
|---|---|
| Component state | Combining multiple streams |
| Derived values | Debounce/throttle |
| Simple async (single API call) | Race conditions |
| Input/Output | WebSockets, real-time |
| Complex error retry logic |
// ✅ Simple API call - use signals readonly user = signal<User | null>(null); readonly loading = signal(false); async loadUser(id: string) { this.loading.set(true); this.user.set(await firstValueFrom(this.http.get<User>(`/api/users/${id}`))); this.loading.set(false); } // ✅ Complex stream - use RxJS readonly searchResults$ = this.searchTerm$.pipe( debounceTime(300), distinctUntilChanged(), switchMap(term => this.http.get<Results>(`/api/search?q=${term}`)) ); // Convert to signal when needed in template readonly searchResults = toSignal(this.searchResults$, { initialValue: [] });
Zoneless Angular (REQUIRED)
Angular is zoneless. Use
provideZonelessChangeDetection().
bootstrapApplication(AppComponent, { providers: [provideZonelessChangeDetection()] });
Remove ZoneJS:
npm uninstall zone.js
Remove from
angular.json polyfills: zone.js and zone.js/testing.
Zoneless Requirements
- Use
change detectionOnPush - Use signals for state (auto-notifies Angular)
- Use
for observablesAsyncPipe - Use
when neededmarkForCheck()