Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:47:33 +08:00
commit e430757b63
23 changed files with 5332 additions and 0 deletions

336
skills/core/SKILL.md Normal file
View File

@@ -0,0 +1,336 @@
---
name: angular-core-implementation
description: Generate Angular components, services, modules, and directives. Implement dependency injection, lifecycle hooks, data binding, and build production-ready Angular architectures.
---
# Angular Core Implementation Skill
## Quick Start
### Component Basics
```typescript
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-user-card',
template: `
<div class="card">
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
<button (click)="onDelete()">Delete</button>
</div>
`,
styles: [`
.card { border: 1px solid #ddd; padding: 16px; }
`]
})
export class UserCardComponent {
@Input() user!: User;
@Output() deleted = new EventEmitter<void>();
onDelete() {
this.deleted.emit();
}
}
```
### Service Creation
```typescript
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root' // Singleton service
})
export class UserService {
private apiUrl = '/api/users';
constructor(private http: HttpClient) {}
getUsers(): Observable<User[]> {
return this.http.get<User[]>(this.apiUrl);
}
getUser(id: number): Observable<User> {
return this.http.get<User>(`${this.apiUrl}/${id}`);
}
createUser(user: User): Observable<User> {
return this.http.post<User>(this.apiUrl, user);
}
}
```
### Dependency Injection
```typescript
@Injectable()
export class NotificationService {
constructor(
private logger: LoggerService,
private config: ConfigService
) {}
notify(message: string) {
this.logger.log(message);
}
}
```
## Core Concepts
### Lifecycle Hooks
```typescript
export class UserListComponent implements
OnInit,
OnChanges,
OnDestroy
{
@Input() users: User[] = [];
ngOnInit() {
// Initialize component, fetch data
this.loadUsers();
}
ngOnChanges(changes: SimpleChanges) {
// Respond to input changes
if (changes['users']) {
this.onUsersChanged();
}
}
ngOnDestroy() {
// Cleanup subscriptions, remove listeners
this.subscription?.unsubscribe();
}
private loadUsers() { /* ... */ }
private onUsersChanged() { /* ... */ }
}
```
**Lifecycle Order:**
1. `ngOnChanges` - When input properties change
2. `ngOnInit` - After first ngOnChanges
3. `ngDoCheck` - Every change detection cycle
4. `ngAfterContentInit` - After content is initialized
5. `ngAfterContentChecked` - After content is checked
6. `ngAfterViewInit` - After view is initialized
7. `ngAfterViewChecked` - After view is checked
8. `ngOnDestroy` - When component is destroyed
### Modules
```typescript
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
@NgModule({
declarations: [
UserListComponent,
UserDetailComponent,
UserFormComponent
],
imports: [
CommonModule,
FormsModule
],
exports: [
UserListComponent,
UserDetailComponent
]
})
export class UserModule { }
```
### Lazy Loading
```typescript
const routes: Routes = [
{ path: 'users', loadChildren: () =>
import('./users/users.module').then(m => m.UsersModule)
}
];
```
## Advanced Patterns
### Content Projection
```typescript
// Parent component
<app-card>
<div class="header">Card Title</div>
<div class="content">Card content</div>
</app-card>
// Card component
@Component({
selector: 'app-card',
template: `
<div class="card">
<ng-content select=".header"></ng-content>
<ng-content select=".content"></ng-content>
<ng-content></ng-content>
</div>
`
})
export class CardComponent { }
```
### ViewChild and ContentChild
```typescript
@Component({
selector: 'app-form',
template: `<app-input #firstInput></app-input>`
})
export class FormComponent implements AfterViewInit {
@ViewChild('firstInput') firstInput!: InputComponent;
ngAfterViewInit() {
this.firstInput.focus();
}
}
```
### Custom Directive
```typescript
@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective {
constructor(private el: ElementRef) {
this.el.nativeElement.style.backgroundColor = 'yellow';
}
}
// Usage: <p appHighlight>Highlighted text</p>
```
## Encapsulation
### View Encapsulation Modes
```typescript
@Component({
selector: 'app-card',
template: `<div class="card">...</div>`,
styles: [`.card { color: blue; }`],
encapsulation: ViewEncapsulation.Emulated // Default
})
export class CardComponent { }
```
- **Emulated** (default): CSS scoped to component
- **None**: Global styles
- **ShadowDom**: Uses browser shadow DOM
## Change Detection
### OnPush Strategy
```typescript
@Component({
selector: 'app-user',
template: `<div>{{ user.name }}</div>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserComponent {
@Input() user!: User;
constructor(private cdr: ChangeDetectorRef) {}
manualDetection() {
this.cdr.markForCheck();
}
}
```
## Provider Patterns
### Multi-Provider
```typescript
@NgModule({
providers: [
{ provide: VALIDATORS, useValue: emailValidator, multi: true },
{ provide: VALIDATORS, useValue: minLengthValidator, multi: true }
]
})
export class ValidatorsModule { }
```
### Factory Pattern
```typescript
@NgModule({
providers: [
{
provide: ConfigService,
useFactory: (env: EnvironmentService) => {
return env.production ?
new ProdConfigService() :
new DevConfigService();
},
deps: [EnvironmentService]
}
]
})
export class AppModule { }
```
## Testing Components
```typescript
describe('UserCardComponent', () => {
let component: UserCardComponent;
let fixture: ComponentFixture<UserCardComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [UserCardComponent]
}).compileComponents();
fixture = TestBed.createComponent(UserCardComponent);
component = fixture.componentInstance;
});
it('should emit deleted when delete button clicked', () => {
spyOn(component.deleted, 'emit');
component.user = { id: 1, name: 'John', email: 'john@example.com' };
fixture.detectChanges();
fixture.debugElement.query(By.css('button')).nativeElement.click();
expect(component.deleted.emit).toHaveBeenCalled();
});
});
```
## Performance Optimization
1. **Use OnPush**: Reduces change detection cycles
2. **Unsubscribe**: Prevent memory leaks
3. **TrackBy**: Optimize *ngFor rendering
4. **Lazy Load**: Load modules on demand
5. **Avoid property binding in templates**: Use async pipe
```typescript
// Bad
users: User[] = [];
// Good
users$ = this.userService.getUsers();
<!-- Template -->
<app-user *ngFor="let user of users$ | async; trackBy: trackByUserId">
</app-user>
```
## Best Practices
1. **Smart vs Presentational**: Container components handle logic
2. **One Responsibility**: Each component has a single purpose
3. **Input/Output**: Use @Input/@Output for communication
4. **Services**: Handle business logic and HTTP
5. **DI**: Always use dependency injection
6. **OnDestroy**: Clean up subscriptions
## Resources
- [Angular Documentation](https://angular.io/docs)
- [Angular Best Practices](https://angular.io/guide/styleguide)
- [Component Interaction](https://angular.io/guide/component-interaction)

381
skills/forms/SKILL.md Normal file
View File

@@ -0,0 +1,381 @@
---
name: forms-implementation
description: Build reactive and template-driven forms, implement custom validators, create async validators, add cross-field validation, and generate dynamic forms for Angular applications.
---
# Forms Implementation Skill
## Quick Start
### Template-Driven Forms
```typescript
import { Component } from '@angular/core';
import { NgForm } from '@angular/forms';
@Component({
selector: 'app-contact',
template: `
<form #contactForm="ngForm" (ngSubmit)="onSubmit(contactForm)">
<input
[(ngModel)]="model.name"
name="name"
required
minlength="3"
/>
<input
[(ngModel)]="model.email"
name="email"
email
/>
<button [disabled]="!contactForm.valid">Submit</button>
</form>
`
})
export class ContactComponent {
model = { name: '', email: '' };
onSubmit(form: NgForm) {
if (form.valid) {
console.log('Form submitted:', form.value);
}
}
}
```
### Reactive Forms
```typescript
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-user-form',
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<input formControlName="name" placeholder="Name" />
<input formControlName="email" type="email" />
<input formControlName="password" type="password" />
<button [disabled]="form.invalid">Register</button>
</form>
`
})
export class UserFormComponent implements OnInit {
form!: FormGroup;
constructor(private fb: FormBuilder) {}
ngOnInit() {
this.form = this.fb.group({
name: ['', [Validators.required, Validators.minLength(3)]],
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]]
});
}
onSubmit() {
if (this.form.valid) {
console.log(this.form.value);
}
}
}
```
## Form Controls
### FormControl
```typescript
// Create standalone control
const nameControl = new FormControl('', Validators.required);
// Get value
nameControl.value
// Set value
nameControl.setValue('John');
nameControl.patchValue({ name: 'John' });
// Check validity
nameControl.valid
nameControl.invalid
nameControl.errors
// Listen to changes
nameControl.valueChanges.subscribe(value => {
console.log('Changed:', value);
});
```
### FormGroup
```typescript
const form = new FormGroup({
name: new FormControl('', Validators.required),
email: new FormControl('', [Validators.required, Validators.email]),
address: new FormGroup({
street: new FormControl(''),
city: new FormControl(''),
zip: new FormControl('')
})
});
// Access nested controls
form.get('address.street')?.setValue('123 Main St');
// Update multiple values
form.patchValue({
name: 'John',
email: 'john@example.com'
});
```
### FormArray
```typescript
const form = new FormGroup({
name: new FormControl(''),
emails: new FormArray([
new FormControl(''),
new FormControl('')
])
});
// Dynamic form array
const emailsArray = form.get('emails') as FormArray;
// Add control
emailsArray.push(new FormControl(''));
// Remove control
emailsArray.removeAt(0);
// Iterate
emailsArray.controls.forEach((control, index) => {
// ...
});
```
## Validation
### Built-in Validators
```typescript
import { Validators } from '@angular/forms';
new FormControl('', [
Validators.required,
Validators.minLength(3),
Validators.maxLength(50),
Validators.pattern(/^[a-z]/i),
Validators.email,
Validators.min(0),
Validators.max(100)
])
```
### Custom Validators
```typescript
// Simple validator
function noSpacesValidator(control: AbstractControl): ValidationErrors | null {
if (control.value && control.value.includes(' ')) {
return { hasSpaces: true };
}
return null;
}
// Cross-field validator
function passwordMatchValidator(group: FormGroup): ValidationErrors | null {
const password = group.get('password')?.value;
const confirm = group.get('confirmPassword')?.value;
return password === confirm ? null : { passwordMismatch: true };
}
// Usage
const form = new FormGroup({
username: new FormControl('', noSpacesValidator),
password: new FormControl(''),
confirmPassword: new FormControl('')
}, passwordMatchValidator);
```
### Async Validators
```typescript
function emailAvailableValidator(service: UserService): AsyncValidatorFn {
return (control: AbstractControl): Observable<ValidationErrors | null> => {
if (!control.value) {
return of(null);
}
return service.checkEmailAvailable(control.value).pipe(
map(available => available ? null : { emailTaken: true }),
debounceTime(300),
first()
);
};
}
// Usage
new FormControl('', {
validators: Validators.required,
asyncValidators: emailAvailableValidator(userService),
updateOn: 'blur'
});
```
## Form State
```typescript
const control = form.get('email')!;
// Pristine/Dirty
control.pristine // Not modified by user
control.dirty // Modified by user
// Touched/Untouched
control.untouched // Never focused
control.touched // Focused at least once
// Valid/Invalid
control.valid
control.invalid
control.errors
control.pending // Async validation in progress
// Status
control.status // 'VALID' | 'INVALID' | 'PENDING' | 'DISABLED'
// Value
control.value
control.getRawValue() // Include disabled controls
```
## Form Display
### Showing Errors
```typescript
<div *ngIf="form.get('email')?.hasError('required')">
Email is required
</div>
<div *ngIf="form.get('email')?.hasError('email')">
Invalid email format
</div>
<div *ngIf="form.get('email')?.hasError('emailTaken')">
Email already in use
</div>
```
### Dynamic Forms
```typescript
@Component({
template: `
<form [formGroup]="form">
<div formArrayName="items">
<div *ngFor="let item of items.controls; let i = index">
<input [formControlName]="i" />
<button (click)="removeItem(i)">Remove</button>
</div>
</div>
<button (click)="addItem()">Add Item</button>
</form>
`
})
export class DynamicFormComponent {
form!: FormGroup;
get items() {
return this.form.get('items') as FormArray;
}
addItem() {
this.items.push(new FormControl('', Validators.required));
}
removeItem(index: number) {
this.items.removeAt(index);
}
}
```
## Advanced Patterns
### FormBuilder Groups
```typescript
this.form = this.fb.group({
basicInfo: this.fb.group({
firstName: ['', Validators.required],
lastName: ['', Validators.required],
email: ['', [Validators.required, Validators.email]]
}),
address: this.fb.group({
street: [''],
city: [''],
zip: ['']
}),
preferences: this.fb.array([])
});
```
### Directives for Template Forms
```typescript
<form #form="ngForm">
<input
[(ngModel)]="user.name"
name="name"
required
minlength="3"
#nameField="ngModelGroup"
/>
<div *ngIf="nameField.invalid && nameField.touched">
<p *ngIf="nameField.errors?.['required']">Required</p>
<p *ngIf="nameField.errors?.['minlength']">Min length 3</p>
</div>
</form>
```
## Testing Forms
```typescript
describe('UserFormComponent', () => {
let component: UserFormComponent;
let fixture: ComponentFixture<UserFormComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [UserFormComponent],
imports: [ReactiveFormsModule]
}).compileComponents();
fixture = TestBed.createComponent(UserFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should submit valid form', () => {
component.form.patchValue({
name: 'John',
email: 'john@example.com'
});
expect(component.form.valid).toBe(true);
});
it('should show error on invalid email', () => {
component.form.get('email')?.setValue('invalid');
expect(component.form.get('email')?.hasError('email')).toBe(true);
});
});
```
## Best Practices
1. **Reactive Forms for Complex**: Use for validation, computed fields
2. **Template Forms for Simple**: Use for simple, data-binding heavy forms
3. **Always validate**: Server and client validation
4. **Disable submit until valid**: Better UX
5. **Show errors appropriately**: After touched/dirty
6. **Handle async validation**: Debounce, cancel on unsubscribe
7. **Test forms thoroughly**: Validation, submission, edge cases
## Resources
- [Angular Forms Guide](https://angular.io/guide/forms)
- [Reactive Forms](https://angular.io/guide/reactive-forms)
- [Form Validation](https://angular.io/guide/form-validation)

View File

@@ -0,0 +1,586 @@
---
name: modern-angular-implementation
description: Implement Angular 18+ features: Signals, standalone components, @defer blocks, SSR, zoneless change detection, new control flow syntax, and Material 3 integration.
---
# Modern Angular Implementation Skill
## Angular Signals
### Basic Signals
```typescript
import { Component, signal, computed, effect } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<button (click)="increment()">{{ count() }}</button>
<p>Double: {{ double() }}</p>
`
})
export class CounterComponent {
// Writable signal
count = signal(0);
// Computed signal (auto-updates)
double = computed(() => this.count() * 2);
constructor() {
// Effect (side effects)
effect(() => {
console.log('Count changed:', this.count());
});
}
increment() {
this.count.update(n => n + 1);
// or: this.count.set(this.count() + 1);
}
}
```
### Signal Store Pattern
```typescript
@Injectable({ providedIn: 'root' })
export class UserStore {
// Private state signal
private state = signal<{
users: User[];
loading: boolean;
error: string | null;
}>({
users: [],
loading: false,
error: null
});
// Public computed selectors
readonly users = computed(() => this.state().users);
readonly loading = computed(() => this.state().loading);
readonly error = computed(() => this.state().error);
readonly userCount = computed(() => this.users().length);
// Actions
async loadUsers() {
this.state.update(s => ({ ...s, loading: true }));
try {
const users = await this.http.get<User[]>('/api/users');
this.state.update(s => ({ ...s, users, loading: false }));
} catch (error) {
this.state.update(s => ({
...s,
error: error.message,
loading: false
}));
}
}
addUser(user: User) {
this.state.update(s => ({
...s,
users: [...s.users, user]
}));
}
}
```
### Signals vs RxJS
```typescript
// ❌ OLD: RxJS BehaviorSubject
private userSubject = new BehaviorSubject<User | null>(null);
user$ = this.userSubject.asObservable();
userName$ = this.user$.pipe(map(u => u?.name ?? 'Guest'));
ngOnDestroy() {
this.userSubject.complete();
}
// ✅ NEW: Angular Signals
user = signal<User | null>(null);
userName = computed(() => this.user()?.name ?? 'Guest');
// No cleanup needed!
```
## Standalone Components
### Basic Standalone Component
```typescript
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [CommonModule, RouterModule], // Import dependencies directly
template: `
<h1>Dashboard</h1>
<router-outlet />
`
})
export class DashboardComponent {}
```
### Standalone Bootstrap
```typescript
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes),
provideHttpClient(),
// Add other providers
]
});
```
### Standalone Routes
```typescript
// app.routes.ts
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: '',
loadComponent: () => import('./home/home.component').then(m => m.HomeComponent)
},
{
path: 'users',
loadChildren: () => import('./users/users.routes').then(m => m.USERS_ROUTES)
}
];
```
### Migration Command
```bash
# Automated migration to standalone
ng generate @angular/core:standalone
```
## Deferrable Views (@defer)
### Basic @defer
```typescript
@defer {
<app-heavy-component />
} @placeholder {
<div class="loading-skeleton"></div>
}
```
### Defer with Triggers
```typescript
// On viewport (when visible)
@defer (on viewport) {
<app-chart [data]="data" />
} @placeholder {
<div class="chart-placeholder"></div>
}
// On interaction (click or keydown)
@defer (on interaction) {
<app-advanced-editor />
} @placeholder {
<button>Load Editor</button>
}
// On hover
@defer (on hover) {
<app-tooltip [content]="tooltipContent" />
}
// On idle (browser idle)
@defer (on idle) {
<app-analytics-dashboard />
}
// On timer
@defer (on timer(5s)) {
<app-promotional-banner />
}
```
### Advanced @defer with States
```typescript
@defer (on interaction; prefetch on idle) {
<app-video-player [src]="videoUrl" />
} @loading (minimum 500ms; after 100ms) {
<app-spinner />
} @placeholder (minimum 1s) {
<button>Load Video Player</button>
} @error {
<p>Failed to load video player</p>
}
```
### Strategic Deferment
```typescript
<div class="page">
<!-- Critical content loads immediately -->
<app-header />
<app-hero-section />
<!-- Defer below-the-fold content -->
@defer (on viewport) {
<app-features-section />
}
@defer (on viewport) {
<app-testimonials />
}
<!-- Defer interactive widgets -->
@defer (on interaction; prefetch on idle) {
<app-chat-widget />
} @placeholder {
<button class="chat-trigger">Chat with us</button>
}
</div>
```
## New Control Flow
### @if (replaces *ngIf)
```typescript
// OLD
<div *ngIf="user">{{ user.name }}</div>
<div *ngIf="user; else loading">{{ user.name }}</div>
// NEW
@if (user) {
<div>{{ user.name }}</div>
}
@if (user) {
<div>{{ user.name }}</div>
} @else {
<div>Loading...</div>
}
```
### @for (replaces *ngFor)
```typescript
// OLD
<div *ngFor="let item of items; trackBy: trackById">
{{ item.name }}
</div>
// NEW
@for (item of items; track item.id) {
<div>{{ item.name }}</div>
} @empty {
<p>No items found</p>
}
```
### @switch (replaces *ngSwitch)
```typescript
// OLD
<div [ngSwitch]="status">
<p *ngSwitchCase="'loading'">Loading...</p>
<p *ngSwitchCase="'error'">Error occurred</p>
<p *ngSwitchDefault>Success</p>
</div>
// NEW
@switch (status) {
@case ('loading') {
<p>Loading...</p>
}
@case ('error') {
<p>Error occurred</p>
}
@default {
<p>Success</p>
}
}
```
### Combined Control Flow
```typescript
@if (users.length > 0) {
<ul>
@for (user of users; track user.id) {
<li>
{{ user.name }}
@if (user.isAdmin) {
<span class="badge">Admin</span>
}
</li>
} @empty {
<li>No users found</li>
}
</ul>
} @else {
<p>Loading users...</p>
}
```
## Server-Side Rendering (SSR)
### Enable SSR
```bash
# Add SSR to existing project
ng add @angular/ssr
# Or create new project with SSR
ng new my-app --ssr
```
### SSR Configuration
```typescript
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideClientHydration } from '@angular/platform-browser';
export const appConfig: ApplicationConfig = {
providers: [
provideClientHydration() // Enable hydration
]
};
```
### SSR-Safe Code
```typescript
import { Component, Inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
@Component({...})
export class MapComponent {
constructor(@Inject(PLATFORM_ID) private platformId: Object) {}
ngOnInit() {
// Only run in browser
if (isPlatformBrowser(this.platformId)) {
this.initializeMap();
this.loadGoogleMapsAPI();
}
}
private initializeMap() {
// Browser-specific code
const map = new google.maps.Map(document.getElementById('map'));
}
}
```
### Transfer State (Avoid Duplicate Requests)
```typescript
import { Component, makeStateKey, TransferState } from '@angular/core';
const USERS_KEY = makeStateKey<User[]>('users');
@Component({...})
export class UsersComponent {
constructor(
private http: HttpClient,
private transferState: TransferState
) {}
loadUsers() {
// Check if data exists in transfer state (from SSR)
const users = this.transferState.get(USERS_KEY, null);
if (users) {
// Use cached data from SSR
return of(users);
}
// Fetch from API and cache for hydration
return this.http.get<User[]>('/api/users').pipe(
tap(users => this.transferState.set(USERS_KEY, users))
);
}
}
```
## Zoneless Change Detection
### Enable Zoneless
```typescript
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideExperimentalZonelessChangeDetection } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [
provideExperimentalZonelessChangeDetection()
]
};
```
### Zoneless-Compatible Code
```typescript
@Component({...})
export class MyComponent {
count = signal(0); // Signals work great with zoneless!
// Manual change detection when needed
constructor(private cdr: ChangeDetectorRef) {}
onManualUpdate() {
this.legacyProperty = 'new value';
this.cdr.markForCheck(); // Trigger change detection manually
}
}
```
## Material 3
### Install Material 3
```bash
ng add @angular/material
```
### Material 3 Theme
```scss
// styles.scss
@use '@angular/material' as mat;
$my-theme: mat.define-theme((
color: (
theme-type: light,
primary: mat.$azure-palette,
),
));
html {
@include mat.all-component-themes($my-theme);
}
// Dark mode
html.dark-theme {
$dark-theme: mat.define-theme((
color: (
theme-type: dark,
primary: mat.$azure-palette,
),
));
@include mat.all-component-colors($dark-theme);
}
```
### Material 3 Components
```typescript
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
@Component({
standalone: true,
imports: [MatButtonModule, MatCardModule, MatIconModule],
template: `
<mat-card appearance="outlined">
<mat-card-header>
<mat-card-title>Material 3 Card</mat-card-title>
</mat-card-header>
<mat-card-content>
<p>Beautiful Material Design 3 components</p>
</mat-card-content>
<mat-card-actions>
<button mat-button>Action</button>
<button mat-raised-button color="primary">
<mat-icon>favorite</mat-icon>
Primary
</button>
</mat-card-actions>
</mat-card>
`
})
export class MaterialCardComponent {}
```
## Migration Patterns
### NgModule → Standalone
```typescript
// BEFORE: NgModule
@NgModule({
declarations: [UserComponent, UserListComponent],
imports: [CommonModule, RouterModule],
exports: [UserComponent]
})
export class UserModule {}
// AFTER: Standalone
export const USER_ROUTES: Routes = [{
path: '',
loadComponent: () => import('./user.component').then(m => m.UserComponent)
}];
@Component({
standalone: true,
imports: [CommonModule, RouterModule]
})
export class UserComponent {}
```
### RxJS → Signals
```typescript
// BEFORE: RxJS
class UserService {
private usersSubject = new BehaviorSubject<User[]>([]);
users$ = this.usersSubject.asObservable();
addUser(user: User) {
const current = this.usersSubject.value;
this.usersSubject.next([...current, user]);
}
}
// AFTER: Signals
class UserService {
users = signal<User[]>([]);
addUser(user: User) {
this.users.update(users => [...users, user]);
}
}
```
## Performance Optimization
### Bundle Size Reduction with @defer
```typescript
// Can reduce initial bundle by 40-60%!
@defer (on viewport) {
<app-heavy-chart-library />
}
```
### Zoneless Performance Gains
```typescript
// 20-30% performance improvement
provideExperimentalZonelessChangeDetection()
```
### SSR Core Web Vitals
```typescript
// Dramatically improves LCP, FCP, TTFB
provideClientHydration()
```
## Best Practices
1. **Use Signals for Simple State** - Perfect for component-local reactive state
2. **Keep RxJS for Complex Async** - Still best for HTTP, WebSockets, complex operators
3. **Strategic @defer** - Don't defer critical content, be strategic
4. **Gradual Migration** - Migrate to standalone incrementally
5. **SSR-Safe Guards** - Always check isPlatformBrowser for DOM access
6. **Zoneless-Ready** - Use Signals and OnPush to prepare for zoneless future
## Resources
- [Signals Documentation](https://angular.dev/guide/signals)
- [Standalone Migration](https://angular.dev/reference/migrations/standalone)
- [@defer Guide](https://angular.dev/guide/templates/defer)
- [SSR Guide](https://angular.dev/guide/ssr)
- [Material 3](https://material.angular.io)

437
skills/routing/SKILL.md Normal file
View File

@@ -0,0 +1,437 @@
---
name: routing-performance-implementation
description: Configure routing with lazy loading, implement route guards, set up preloading strategies, optimize change detection, analyze bundles, and implement performance optimizations.
---
# Routing & Performance Implementation Skill
## Quick Start
### Basic Routing
```typescript
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent, AboutComponent, NotFoundComponent } from './components';
const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'about', component: AboutComponent },
{ path: '**', component: NotFoundComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
```
### Navigation
```typescript
import { Component } from '@angular/core';
import { Router } from '@angular/router';
@Component({
template: `
<button (click)="goHome()">Home</button>
<a routerLink="/about">About</a>
<a routerLink="/users" [queryParams]="{ tab: 'active' }">Users</a>
`
})
export class NavComponent {
constructor(private router: Router) {}
goHome() {
this.router.navigate(['/']);
}
}
```
### Route Parameters
```typescript
const routes: Routes = [
{ path: 'users/:id', component: UserDetailComponent },
{ path: 'users/:id/posts/:postId', component: PostDetailComponent }
];
// Component
@Component({...})
export class UserDetailComponent {
userId!: string;
constructor(private route: ActivatedRoute) {
this.route.params.subscribe(params => {
this.userId = params['id'];
});
}
}
// Or with snapshot
ngOnInit() {
const id = this.route.snapshot.params['id'];
}
```
## Lazy Loading
### Feature Modules with Lazy Loading
```typescript
// app-routing.module.ts
const routes: Routes = [
{ path: '', component: HomeComponent },
{
path: 'users',
loadChildren: () => import('./users/users.module').then(m => m.UsersModule)
},
{
path: 'products',
loadChildren: () => import('./products/products.module').then(m => m.ProductsModule)
}
];
// users/users-routing.module.ts
const routes: Routes = [
{ path: '', component: UserListComponent },
{ path: ':id', component: UserDetailComponent }
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class UsersRoutingModule { }
```
### Lazy Loading with Standalone Components
```typescript
const routes: Routes = [
{
path: 'admin',
loadChildren: () => import('./admin/admin.routes').then(m => m.ADMIN_ROUTES)
}
];
// admin/admin.routes.ts
export const ADMIN_ROUTES: Routes = [
{ path: '', component: AdminDashboardComponent },
{ path: 'users', component: AdminUsersComponent }
];
```
## Route Guards
### CanActivate Guard
```typescript
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { AuthService } from './auth.service';
import { map } from 'rxjs/operators';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private authService: AuthService,
private router: Router
) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> {
return this.authService.isAuthenticated$.pipe(
map(isAuth => {
if (isAuth) return true;
this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
return false;
})
);
}
}
// Usage
const routes: Routes = [
{ path: 'admin', component: AdminComponent, canActivate: [AuthGuard] }
];
```
### CanDeactivate Guard
```typescript
export interface CanComponentDeactivate {
canDeactivate: () => Observable<boolean> | boolean;
}
@Injectable()
export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {
canDeactivate(component: CanComponentDeactivate): Observable<boolean> | boolean {
return component.canDeactivate();
}
}
// Component
@Component({...})
export class FormComponent implements CanComponentDeactivate {
form!: FormGroup;
canDeactivate(): Observable<boolean> | boolean {
return !this.form.dirty || confirm('Discard changes?');
}
}
// Usage
{ path: 'form', component: FormComponent, canDeactivate: [CanDeactivateGuard] }
```
### Resolve Guard
```typescript
@Injectable()
export class UserResolver implements Resolve<User> {
constructor(private userService: UserService) {}
resolve(route: ActivatedRouteSnapshot): Observable<User> {
return this.userService.getUser(route.params['id']);
}
}
// Usage
{
path: 'users/:id',
component: UserDetailComponent,
resolve: { user: UserResolver }
}
// Component receives data
@Component({...})
export class UserDetailComponent {
user!: User;
constructor(private route: ActivatedRoute) {
this.route.data.subscribe(data => {
this.user = data['user'];
});
}
}
```
## Query Parameters
```typescript
// Navigation
this.router.navigate(['/users'], {
queryParams: {
page: 1,
sort: 'name',
filter: 'active'
}
});
// Reading
this.route.queryParams.subscribe(params => {
const page = params['page'];
const sort = params['sort'];
});
// Template
<a [routerLink]="['/users']" [queryParams]="{ page: 2, sort: 'name' }">
Next Page
</a>
```
## Fragment (Hash)
```typescript
// Navigation
this.router.navigate(['/docs'], { fragment: 'section1' });
// Reading
this.route.fragment.subscribe(fragment => {
console.log('Fragment:', fragment);
});
// Template
<a routerLink="/docs" fragment="section1">Section 1</a>
```
## Preloading Strategies
```typescript
// Default: no preloading
RouterModule.forRoot(routes);
// Preload all lazy modules
RouterModule.forRoot(routes, {
preloadingStrategy: PreloadAllModules
});
// Custom preloading strategy
@Injectable()
export class SelectivePreloadingStrategy implements PreloadingStrategy {
preload(route: Route, load: () => Observable<any>): Observable<any> {
if (route.data && route.data['preload']) {
return load();
}
return of(null);
}
}
// Usage
const routes: Routes = [
{ path: 'users', loadChildren: '...', data: { preload: true } }
];
RouterModule.forRoot(routes, {
preloadingStrategy: SelectivePreloadingStrategy
})
```
## Route Reuse Strategy
```typescript
@Injectable()
export class CustomRouteReuseStrategy implements RouteReuseStrategy {
storedRoutes: { [key: string]: RouteData } = {};
shouldDetach(route: ActivatedRouteSnapshot): boolean {
return route.data['cache'] === true;
}
store(route: ActivatedRouteSnapshot, detachedTree: DetachedRouteHandle): void {
this.storedRoutes[route.url.join('/')] = { route, handle: detachedTree };
}
shouldAttach(route: ActivatedRouteSnapshot): boolean {
return !!this.storedRoutes[route.url.join('/')];
}
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {
return this.storedRoutes[route.url.join('/')]?.handle || null;
}
shouldReuseRoute(future: ActivatedRouteSnapshot, current: ActivatedRouteSnapshot): boolean {
return future.routeConfig === current.routeConfig;
}
}
```
## Performance Optimization
### Code Splitting
```typescript
// Only load admin module when needed
{
path: 'admin',
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
}
```
### Change Detection with Routes
```typescript
@Component({
selector: 'app-root',
template: `<router-outlet></router-outlet>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent { }
```
### Scroll Position
```typescript
// Scroll to top on route change
RouterModule.forRoot(routes, {
scrollPositionRestoration: 'top'
})
// Or custom scroll
export class ScrollToTopComponent implements OnInit {
constructor(private router: Router) {}
ngOnInit() {
this.router.events.pipe(
filter(event => event instanceof NavigationEnd)
).subscribe(() => {
window.scrollTo(0, 0);
});
}
}
```
## Advanced Patterns
### Auxiliary Routes
```typescript
// URL: /users/1(admin:admin-panel)
<router-outlet></router-outlet>
<router-outlet name="admin"></router-outlet>
// Navigation
this.router.navigate([
{ outlets: {
primary: ['users', userId],
admin: ['admin-panel']
}}
]);
```
### Child Routes with Components
```typescript
const routes: Routes = [
{
path: 'dashboard',
component: DashboardComponent,
children: [
{ path: 'stats', component: StatsComponent },
{ path: 'reports', component: ReportsComponent }
]
}
];
// DashboardComponent template
<nav>
<a routerLink="stats" routerLinkActive="active">Stats</a>
<a routerLink="reports" routerLinkActive="active">Reports</a>
</nav>
<router-outlet></router-outlet>
```
## Testing Routes
```typescript
describe('Routing', () => {
let router: Router;
let location: Location;
let fixture: ComponentFixture<AppComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AppRoutingModule, AppComponent]
}).compileComponents();
router = TestBed.inject(Router);
location = TestBed.inject(Location);
fixture = TestBed.createComponent(AppComponent);
});
it('should navigate to home', fakeAsync(() => {
router.navigate(['']);
tick();
expect(location.path()).toBe('/');
}));
});
```
## Best Practices
1. **Lazy load features**: Reduce initial bundle size
2. **Use route guards**: Control access and preload data
3. **Implement RouteReuseStrategy**: Cache components when needed
4. **Handle 404s**: Provide meaningful error pages
5. **Query params for filters**: Keep state in URL
6. **Preload strategically**: Balance performance vs initial load
7. **Use fragments for anchors**: Scroll to page sections
## Resources
- [Angular Routing Guide](https://angular.io/guide/router)
- [Route Guards](https://angular.io/guide/router-tutorial-toh)
- [Lazy Loading](https://angular.io/guide/lazy-loading-ngmodules)

339
skills/rxjs/SKILL.md Normal file
View File

@@ -0,0 +1,339 @@
---
name: rxjs-implementation
description: Implement RxJS observables, apply operators, fix memory leaks with unsubscribe patterns, handle errors, create subjects, and build reactive data pipelines in Angular applications.
---
# RxJS Implementation Skill
## Quick Start
### Observable Basics
```typescript
import { Observable } from 'rxjs';
// Create observable
const observable = new Observable((observer) => {
observer.next(1);
observer.next(2);
observer.next(3);
observer.complete();
});
// Subscribe
const subscription = observable.subscribe({
next: (value) => console.log(value),
error: (error) => console.error(error),
complete: () => console.log('Done')
});
// Unsubscribe
subscription.unsubscribe();
```
### Common Operators
```typescript
import { map, filter, switchMap, takeUntil } from 'rxjs/operators';
// Transformation
data$.pipe(
map(user => user.name),
filter(name => name.length > 0)
).subscribe(name => console.log(name));
// Higher-order
userId$.pipe(
switchMap(id => this.userService.getUser(id))
).subscribe(user => console.log(user));
```
## Subjects
### Subject Types
```typescript
import { Subject, BehaviorSubject, ReplaySubject } from 'rxjs';
// Subject - No initial value
const subject = new Subject<string>();
subject.next('hello');
// BehaviorSubject - Has initial value
const behavior = new BehaviorSubject<string>('initial');
behavior.next('new value');
// ReplaySubject - Replays N values
const replay = new ReplaySubject<string>(3);
replay.next('one');
replay.next('two');
```
### Service with Subject
```typescript
@Injectable()
export class NotificationService {
private messageSubject = new Subject<string>();
public message$ = this.messageSubject.asObservable();
notify(message: string) {
this.messageSubject.next(message);
}
}
// Usage
constructor(private notification: NotificationService) {
this.notification.message$.subscribe(msg => {
console.log('Notification:', msg);
});
}
```
## Transformation Operators
```typescript
// map - Transform values
source$.pipe(
map(user => user.name)
)
// switchMap - Switch to new observable (cancel previous)
userId$.pipe(
switchMap(id => this.userService.getUser(id))
)
// mergeMap - Merge all results
fileIds$.pipe(
mergeMap(id => this.downloadFile(id))
)
// concatMap - Sequential processing
tasks$.pipe(
concatMap(task => this.processTask(task))
)
// exhaustMap - Ignore new while processing
clicks$.pipe(
exhaustMap(() => this.longRequest())
)
```
## Filtering Operators
```typescript
// filter - Only pass matching values
data$.pipe(
filter(item => item.active)
)
// first - Take first value
data$.pipe(first())
// take - Take N values
data$.pipe(take(5))
// takeUntil - Take until condition
data$.pipe(
takeUntil(this.destroy$)
)
// distinct - Filter duplicates
data$.pipe(
distinct(),
distinctUntilChanged()
)
// debounceTime - Wait N ms
input$.pipe(
debounceTime(300),
distinctUntilChanged()
)
```
## Combination Operators
```typescript
import { combineLatest, merge, concat, zip } from 'rxjs';
// combineLatest - Latest from all
combineLatest([user$, settings$, theme$]).pipe(
map(([user, settings, theme]) => ({ user, settings, theme }))
)
// merge - Values from any
merge(click$, hover$, input$)
// concat - Sequential
concat(request1$, request2$, request3$)
// zip - Wait for all
zip(form1$, form2$, form3$)
// withLatestFrom - Combine with latest
click$.pipe(
withLatestFrom(user$),
map(([click, user]) => ({ click, user }))
)
```
## Error Handling
```typescript
// catchError - Handle errors
data$.pipe(
catchError(error => {
console.error('Error:', error);
return of(defaultValue);
})
)
// retry - Retry on error
request$.pipe(
retry(3),
catchError(error => throwError(error))
)
// timeout - Timeout if no value
request$.pipe(
timeout(5000),
catchError(error => of(null))
)
```
## Memory Leak Prevention
### Unsubscribe Pattern
```typescript
private destroy$ = new Subject<void>();
ngOnInit() {
this.data$.pipe(
takeUntil(this.destroy$)
).subscribe(data => {
this.processData(data);
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
```
### Async Pipe (Preferred)
```typescript
// Component
export class UserComponent {
user$ = this.userService.getUser(1);
constructor(private userService: UserService) {}
}
// Template - Async pipe handles unsubscribe
<div>{{ user$ | async as user }}
<p>{{ user.name }}</p>
</div>
```
## Advanced Patterns
### Share Operator
```typescript
// Hot observable - Share single subscription
readonly users$ = this.http.get('/api/users').pipe(
shareReplay(1) // Cache last result
);
// Now multiple subscriptions use same HTTP request
this.users$.subscribe(users => {...});
this.users$.subscribe(users => {...}); // Reuses cached
```
### Scan for State
```typescript
// Accumulate state
const counter$ = clicks$.pipe(
scan((count) => count + 1, 0)
)
// Complex state
const appState$ = actions$.pipe(
scan((state, action) => {
switch(action.type) {
case 'ADD_USER': return { ...state, users: [...state.users, action.user] };
case 'DELETE_USER': return { ...state, users: state.users.filter(u => u.id !== action.id) };
default: return state;
}
}, initialState)
)
```
### Forkjoin for Multiple Requests
```typescript
// Parallel requests
forkJoin({
users: this.userService.getUsers(),
settings: this.settingService.getSettings(),
themes: this.themeService.getThemes()
}).subscribe(({ users, settings, themes }) => {
console.log('All loaded:', users, settings, themes);
})
```
## Testing Observables
```typescript
import { marbles } from 'rxjs-marbles';
it('should map values correctly', marbles((m) => {
const source = m.hot('a-b-|', { a: 1, b: 2 });
const expected = m.cold('x-y-|', { x: 2, y: 4 });
const result = source.pipe(
map(x => x * 2)
);
m.expect(result).toBeObservable(expected);
}));
```
## Best Practices
1. **Always unsubscribe**: Use takeUntil or async pipe
2. **Use higher-order operators**: switchMap, mergeMap, etc.
3. **Avoid nested subscriptions**: Use operators instead
4. **Share subscriptions**: Use share/shareReplay for expensive operations
5. **Handle errors**: Always include catchError
6. **Type your observables**: `Observable<User>` not just `Observable`
## Common Mistakes to Avoid
```typescript
// ❌ Wrong - Creates multiple subscriptions
this.data$.subscribe(d => {
this.data$.subscribe(d2 => {
// nested subscriptions!
});
});
// ✅ Correct - Use switchMap
this.data$.pipe(
switchMap(d => this.otherService.fetch(d))
).subscribe(result => {
// handled
});
// ❌ Wrong - Memory leak
ngOnInit() {
this.data$.subscribe(data => this.data = data);
}
// ✅ Correct - Unsubscribe or async
ngOnInit() {
this.data$ = this.service.getData();
}
// In template: {{ data$ | async }}
```
## Resources
- [RxJS Documentation](https://rxjs.dev/)
- [Interactive Diagrams](https://rxmarbles.com/)
- [RxJS Operators](https://rxjs.dev/api)

View File

@@ -0,0 +1,470 @@
---
name: state-implementation
description: Implement NgRx store with actions and reducers, build selectors, create effects for async operations, configure entity adapters, and integrate HTTP APIs with state management.
---
# State Implementation Skill
## Quick Start
### Simple Service-Based State
```typescript
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class UserStore {
private usersSubject = new BehaviorSubject<User[]>([]);
users$ = this.usersSubject.asObservable();
constructor(private http: HttpClient) {}
loadUsers() {
this.http.get<User[]>('/api/users').subscribe(
users => this.usersSubject.next(users)
);
}
addUser(user: User) {
this.http.post<User>('/api/users', user).subscribe(
newUser => {
const current = this.usersSubject.value;
this.usersSubject.next([...current, newUser]);
}
);
}
}
// Usage
export class UserListComponent {
users$ = this.userStore.users$;
constructor(private userStore: UserStore) {}
}
```
### NgRx Basics
```typescript
// 1. Define actions
export const loadUsers = createAction('[User] Load Users');
export const loadUsersSuccess = createAction(
'[User] Load Users Success',
props<{ users: User[] }>()
);
export const loadUsersError = createAction(
'[User] Load Users Error',
props<{ error: string }>()
);
// 2. Create reducer
const initialState: UserState = { users: [], loading: false };
export const userReducer = createReducer(
initialState,
on(loadUsers, state => ({ ...state, loading: true })),
on(loadUsersSuccess, (state, { users }) => ({
...state,
users,
loading: false
})),
on(loadUsersError, (state, { error }) => ({
...state,
error,
loading: false
}))
);
// 3. Create effect
@Injectable()
export class UserEffects {
loadUsers$ = createEffect(() =>
this.actions$.pipe(
ofType(loadUsers),
switchMap(() =>
this.userService.getUsers().pipe(
map(users => loadUsersSuccess({ users })),
catchError(error => of(loadUsersError({ error })))
)
)
)
);
constructor(
private actions$: Actions,
private userService: UserService
) {}
}
// 4. Use in component
@Component({...})
export class UserListComponent {
users$ = this.store.select(selectUsers);
loading$ = this.store.select(selectLoading);
constructor(private store: Store) {
this.store.dispatch(loadUsers());
}
}
```
## NgRx Core Concepts
### Store
```typescript
// Dispatch action
this.store.dispatch(loadUsers());
// Select state
this.store.select(selectUsers).subscribe(users => {
console.log(users);
});
// Select with observable
this.users$ = this.store.select(selectUsers);
// Multiple selects
this.store.select(selectUsers, selectLoading).subscribe(([users, loading]) => {
// ...
});
```
### Selectors
```typescript
// Feature selector
export const selectUserState = createFeatureSelector<UserState>('users');
// Select from feature
export const selectUsers = createSelector(
selectUserState,
state => state.users
);
// Selector composition
export const selectActiveUsers = createSelector(
selectUsers,
users => users.filter(u => u.active)
);
// Memoized selector
export const selectUserById = (id: number) => createSelector(
selectUsers,
users => users.find(u => u.id === id)
);
// With props
export const selectUsersByRole = createSelector(
selectUsers,
(users: User[], { role }: { role: string }) =>
users.filter(u => u.role === role)
);
// Usage with props
this.store.select(selectUsersByRole, { role: 'admin' });
```
### Effects
```typescript
// Side effect - HTTP call
@Injectable()
export class UserEffects {
loadUsers$ = createEffect(() =>
this.actions$.pipe(
ofType(UserActions.loadUsers),
switchMap(() =>
this.userService.getUsers().pipe(
map(users => UserActions.loadUsersSuccess({ users })),
catchError(error => of(UserActions.loadUsersError({ error })))
)
)
)
);
// Non-dispatching effect
logActions$ = createEffect(
() => this.actions$.pipe(
tap(action => console.log(action))
),
{ dispatch: false }
);
constructor(
private actions$: Actions,
private userService: UserService
) {}
}
```
## Entity Adapter
### Setup
```typescript
export interface User {
id: number;
name: string;
email: string;
}
export const adapter = createEntityAdapter<User>({
selectId: (user: User) => user.id,
sortComparer: (a: User, b: User) => a.name.localeCompare(b.name)
});
export interface UserState extends EntityState<User> {
loading: boolean;
error: string | null;
}
const initialState = adapter.getInitialState({
loading: false,
error: null
});
```
### Reducer with Adapter
```typescript
export const userReducer = createReducer(
initialState,
on(loadUsers, state => ({ ...state, loading: true })),
on(loadUsersSuccess, (state, { users }) =>
adapter.setAll(users, { ...state, loading: false })
),
on(addUserSuccess, (state, { user }) =>
adapter.addOne(user, state)
),
on(updateUserSuccess, (state, { user }) =>
adapter.updateOne({ id: user.id, changes: user }, state)
),
on(deleteUserSuccess, (state, { id }) =>
adapter.removeOne(id, state)
)
);
// Export selectors
export const {
selectIds,
selectEntities,
selectAll,
selectTotal
} = adapter.getSelectors(selectUserState);
```
## Facade Pattern
```typescript
@Injectable()
export class UserFacade {
users$ = this.store.select(selectAllUsers);
loading$ = this.store.select(selectUsersLoading);
error$ = this.store.select(selectUsersError);
constructor(private store: Store) {}
loadUsers() {
this.store.dispatch(loadUsers());
}
addUser(user: User) {
this.store.dispatch(addUser({ user }));
}
updateUser(id: number, changes: Partial<User>) {
this.store.dispatch(updateUser({ id, changes }));
}
deleteUser(id: number) {
this.store.dispatch(deleteUser({ id }));
}
}
// Component usage simplified
@Component({...})
export class UserListComponent {
users$ = this.userFacade.users$;
loading$ = this.userFacade.loading$;
constructor(private userFacade: UserFacade) {
this.userFacade.loadUsers();
}
}
```
## Angular Signals
```typescript
import { signal, computed, effect } from '@angular/core';
// Create signal
const count = signal(0);
// Read value
console.log(count()); // 0
// Update value
count.set(1);
count.update(c => c + 1);
// Computed value
const doubled = computed(() => count() * 2);
// Effect
effect(() => {
console.log(`Count is ${count()}`);
console.log(`Doubled is ${doubled()}`);
});
// Signal-based state
@Component({...})
export class CounterComponent {
count = signal(0);
doubled = computed(() => this.count() * 2);
increment() {
this.count.update(c => c + 1);
}
}
```
## HTTP Integration
### HttpClient with Interceptor
```typescript
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private authService: AuthService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token = this.authService.getToken();
const authReq = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
return next.handle(authReq);
}
}
// Register
@NgModule({
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
}
]
})
export class AppModule { }
```
### Caching Strategy
```typescript
@Injectable()
export class CachingService {
private cache = new Map<string, any>();
get<T>(key: string, request: Observable<T>, ttl: number = 3600000): Observable<T> {
if (this.cache.has(key)) {
return of(this.cache.get(key));
}
return request.pipe(
tap(data => {
this.cache.set(key, data);
setTimeout(() => this.cache.delete(key), ttl);
})
);
}
}
// Usage
getUsers() {
return this.caching.get(
'users',
this.http.get<User[]>('/api/users'),
5 * 60 * 1000 // 5 minutes
);
}
```
## Testing State
```typescript
describe('User Store', () => {
let store: MockStore;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [StoreModule.forRoot({ users: userReducer })]
});
store = TestBed.inject(Store) as MockStore;
});
it('should load users', () => {
const action = loadUsers();
const completion = loadUsersSuccess({ users: mockUsers });
const effect$ = new UserEffects(
hot('a', { a: action }),
mockUserService
).loadUsers$;
const result = cold('b', { b: completion });
expect(effect$).toBeObservable(result);
});
it('should select users', (done) => {
store.setState({ users: { users: mockUsers } });
store.select(selectUsers).subscribe(users => {
expect(users).toEqual(mockUsers);
done();
});
});
});
```
## Best Practices
1. **Normalize State**: Flat structure, avoid nesting
2. **Single Responsibility**: Each reducer handles one feature
3. **Use Facades**: Simplify component-store interaction
4. **Memoize Selectors**: Prevent unnecessary recalculations
5. **Handle Errors**: Always include error states
6. **Lazy Load Stores**: Register feature stores when needed
7. **Time-Travel Debugging**: Use Redux DevTools
## Advanced Patterns
### Composition Pattern
```typescript
// Combine multiple stores
@Injectable()
export class AppFacade {
users$ = this.userFacade.users$;
products$ = this.productFacade.products$;
cart$ = this.cartFacade.cart$;
constructor(
private userFacade: UserFacade,
private productFacade: ProductFacade,
private cartFacade: CartFacade
) {}
}
```
### Feature Flags
```typescript
export const selectFeatureFlags = createFeatureSelector<FeatureFlags>('features');
export const selectFeatureEnabled = (feature: string) => createSelector(
selectFeatureFlags,
flags => flags[feature]?.enabled ?? false
);
// Component
<div *ngIf="featureEnabled$ | async">New Feature</div>
```
## Resources
- [NgRx Documentation](https://ngrx.io/)
- [Entity Adapter](https://ngrx.io/guide/entity)
- [DevTools](https://github.com/reduxjs/redux-devtools-extension)

480
skills/testing/SKILL.md Normal file
View File

@@ -0,0 +1,480 @@
---
name: testing-deployment-implementation
description: Write unit tests for components and services, implement E2E tests with Cypress, set up test mocks, optimize production builds, configure CI/CD pipelines, and deploy to production platforms.
---
# Testing & Deployment Implementation Skill
## Unit Testing Basics
### TestBed Setup
```typescript
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
describe('UserService', () => {
let service: UserService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [UserService]
});
service = TestBed.inject(UserService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
});
});
```
### Component Testing
```typescript
describe('UserListComponent', () => {
let component: UserListComponent;
let fixture: ComponentFixture<UserListComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [UserListComponent],
imports: [CommonModule, HttpClientTestingModule],
providers: [UserService]
}).compileComponents();
fixture = TestBed.createComponent(UserListComponent);
component = fixture.componentInstance;
});
it('should display users', () => {
const mockUsers: User[] = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
];
component.users = mockUsers;
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
const userElements = compiled.querySelectorAll('.user-item');
expect(userElements.length).toBe(2);
});
it('should call service on init', () => {
const userService = TestBed.inject(UserService);
spyOn(userService, 'getUsers').and.returnValue(of([]));
component.ngOnInit();
expect(userService.getUsers).toHaveBeenCalled();
});
});
```
### Testing Async Operations
```typescript
// Using fakeAsync and tick
it('should load users after delay', fakeAsync(() => {
const userService = TestBed.inject(UserService);
spyOn(userService, 'getUsers').and.returnValue(
of([{ id: 1, name: 'John' }]).pipe(delay(1000))
);
component.ngOnInit();
expect(component.users.length).toBe(0);
tick(1000);
expect(component.users.length).toBe(1);
}));
// Using waitForAsync
it('should handle async operations', waitForAsync(() => {
const userService = TestBed.inject(UserService);
spyOn(userService, 'getUsers').and.returnValue(
of([{ id: 1, name: 'John' }])
);
component.ngOnInit();
fixture.whenStable().then(() => {
expect(component.users.length).toBe(1);
});
}));
```
## Mocking Services
### HTTP Mocking
```typescript
it('should fetch users from API', () => {
const mockUsers: User[] = [{ id: 1, name: 'John' }];
service.getUsers().subscribe(users => {
expect(users.length).toBe(1);
expect(users[0].name).toBe('John');
});
const req = httpMock.expectOne('/api/users');
expect(req.request.method).toBe('GET');
req.flush(mockUsers);
});
// POST with error handling
it('should handle errors', () => {
service.createUser({ name: 'Jane' }).subscribe(
() => fail('should not succeed'),
(error) => expect(error.status).toBe(400)
);
const req = httpMock.expectOne('/api/users');
req.flush('Invalid user', { status: 400, statusText: 'Bad Request' });
});
```
### Service Mocking
```typescript
class MockUserService {
getUsers() {
return of([
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
]);
}
}
@Component({
selector: 'app-test',
template: '<div>{{ (users$ | async)?.length }}</div>'
})
class TestComponent {
users$ = this.userService.getUsers();
constructor(private userService: UserService) {}
}
describe('TestComponent with Mock', () => {
let component: TestComponent;
let fixture: ComponentFixture<TestComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [TestComponent],
providers: [
{ provide: UserService, useClass: MockUserService }
]
}).compileComponents();
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should render users', () => {
const div = fixture.nativeElement.querySelector('div');
expect(div.textContent).toContain('2');
});
});
```
## E2E Testing with Cypress
### Basic E2E Test
```typescript
describe('User List Page', () => {
beforeEach(() => {
cy.visit('/users');
});
it('should display user list', () => {
cy.get('[data-testid="user-item"]')
.should('have.length', 10);
});
it('should filter users by name', () => {
cy.get('[data-testid="search-input"]')
.type('John');
cy.get('[data-testid="user-item"]')
.should('have.length', 1)
.should('contain', 'John');
});
it('should navigate to user detail', () => {
cy.get('[data-testid="user-item"]').first().click();
cy.location('pathname').should('include', '/users/');
cy.get('[data-testid="user-detail"]').should('be.visible');
});
});
```
### Page Object Model
```typescript
// user.po.ts
export class UserPage {
navigateTo(path: string = '/users') {
cy.visit(path);
return this;
}
getUsers() {
return cy.get('[data-testid="user-item"]');
}
getUserByName(name: string) {
return cy.get('[data-testid="user-item"]').contains(name);
}
clickUser(index: number) {
this.getUsers().eq(index).click();
return this;
}
searchUser(query: string) {
cy.get('[data-testid="search-input"]').type(query);
return this;
}
}
// Test using PO
describe('User Page', () => {
const page = new UserPage();
beforeEach(() => {
page.navigateTo();
});
it('should find user by name', () => {
page.searchUser('John');
page.getUsers().should('have.length', 1);
});
});
```
## Build Optimization
### AOT Compilation
```typescript
// angular.json
{
"projects": {
"app": {
"architect": {
"build": {
"options": {
"aot": true,
"outputHashing": "all",
"sourceMap": false,
"optimization": true,
"buildOptimizer": true,
"namedChunks": false
}
}
}
}
}
}
```
### Bundle Analysis
```bash
# Install webpack-bundle-analyzer
npm install --save-dev webpack-bundle-analyzer
# Run analysis
ng build --stats-json
webpack-bundle-analyzer dist/app/stats.json
```
### Code Splitting
```typescript
// app-routing.module.ts
const routes: Routes = [
{ path: '', component: HomeComponent },
{
path: 'admin',
loadChildren: () =>
import('./admin/admin.module').then(m => m.AdminModule)
},
{
path: 'users',
loadChildren: () =>
import('./users/users.module').then(m => m.UsersModule)
}
];
```
## Deployment
### Production Build
```bash
# Build for production
ng build --configuration production
# Output directory
dist/app/
# Serve locally
npx http-server dist/app/
```
### Deployment Targets
**Firebase:**
```bash
npm install -g firebase-tools
firebase login
firebase init hosting
firebase deploy
```
**Netlify:**
```bash
npm run build
# Drag and drop dist/ folder to Netlify
# Or use CLI:
npm install -g netlify-cli
netlify deploy --prod --dir=dist/app
```
**GitHub Pages:**
```bash
ng build --output-path docs --base-href /repo-name/
git add docs/
git commit -m "Deploy to GitHub Pages"
git push
# Enable in repository settings
```
**Docker:**
```dockerfile
# Build stage
FROM node:18 as build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Serve stage
FROM nginx:alpine
COPY --from=build /app/dist/app /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
```
## CI/CD Pipelines
### GitHub Actions
```yaml
name: CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Build
run: npm run build
- name: Test
run: npm run test -- --watch=false --code-coverage
- name: E2E Test
run: npm run e2e
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
- name: Deploy
if: github.ref == 'refs/heads/main'
run: npm run deploy
```
## Performance Monitoring
### Core Web Vitals
```typescript
// Using web-vitals library
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';
getCLS(console.log);
getFID(console.log);
getFCP(console.log);
getLCP(console.log);
getTTFB(console.log);
```
### Error Tracking (Sentry)
```typescript
import * as Sentry from "@sentry/angular";
Sentry.init({
dsn: "https://examplePublicKey@o0.ingest.sentry.io/0",
integrations: [
new Sentry.BrowserTracing(),
new Sentry.Replay(),
],
tracesSampleRate: 1.0,
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
});
@NgModule({
providers: [
{
provide: ErrorHandler,
useValue: Sentry.createErrorHandler(),
},
],
})
export class AppModule {}
```
## Testing Best Practices
1. **Arrange-Act-Assert**: Clear test structure
2. **One Assertion per Test**: Keep tests focused
3. **Test Behavior**: Not implementation details
4. **Use Page Objects**: For E2E tests
5. **Mock External Dependencies**: Services, HTTP
6. **Test Error Cases**: Invalid input, failures
7. **Aim for 80% Coverage**: Don't obsess over 100%
## Coverage Report
```bash
# Generate coverage report
ng test --code-coverage
# View report
open coverage/index.html
```
## Resources
- [Jasmine Documentation](https://jasmine.github.io/)
- [Angular Testing Guide](https://angular.io/guide/testing)
- [Cypress Documentation](https://docs.cypress.io/)
- [Testing Best Practices](https://angular.io/guide/testing-best-practices)

251
skills/typescript/SKILL.md Normal file
View File

@@ -0,0 +1,251 @@
---
name: typescript-implementation
description: Implement TypeScript patterns, convert JavaScript to TypeScript, add type annotations, create generics, implement decorators, and enforce strict type safety in Angular projects.
---
# TypeScript Implementation Skill
## Quick Start
### Basic Types
```typescript
// Primitive types
let name: string = "Angular";
let version: number = 18;
let active: boolean = true;
// Union types
let id: string | number;
// Type aliases
type User = {
name: string;
age: number;
};
```
### Interfaces and Generics
```typescript
interface Component {
render(): string;
}
// Generic interface
interface Repository<T> {
getAll(): T[];
getById(id: number): T | undefined;
}
class UserRepository implements Repository<User> {
getAll(): User[] { /* ... */ }
getById(id: number): User | undefined { /* ... */ }
}
```
### Decorators (Essential for Angular)
```typescript
// Class decorator
function Component(config: any) {
return function(target: any) {
target.prototype.selector = config.selector;
};
}
// Method decorator
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`Calling ${propertyKey} with:`, args);
return originalMethod.apply(this, args);
};
return descriptor;
}
// Parameter decorator
function Required(target: any, propertyKey: string, parameterIndex: number) {
// Validation logic
}
```
## Essential Concepts
### Advanced Types
**Utility Types:**
- `Partial<T>` - Make all properties optional
- `Required<T>` - Make all properties required
- `Readonly<T>` - Make all properties readonly
- `Record<K, T>` - Object with specific key types
- `Pick<T, K>` - Select specific properties
- `Omit<T, K>` - Exclude specific properties
**Conditional Types:**
```typescript
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<number>; // false
```
**Mapped Types:**
```typescript
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
```
### Generic Constraints
```typescript
// Extend constraint
function processUser<T extends User>(user: T) {
console.log(user.name); // OK, T has 'name'
}
// Keyof constraint
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
```
## Advanced Features
### Type Guards
```typescript
// Type predicate
function isUser(value: unknown): value is User {
return typeof value === 'object' && value !== null && 'name' in value;
}
// Discriminated unions
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; side: number };
function getArea(shape: Shape) {
switch (shape.kind) {
case 'circle': return Math.PI * shape.radius ** 2;
case 'square': return shape.side ** 2;
}
}
```
### Module System
```typescript
// Export
export interface User { name: string; }
export const API_URL = 'https://api.example.com';
// Import
import { User, API_URL } from './types';
import * as Types from './types'; // Namespace import
```
## Async Programming
```typescript
// Promises
async function fetchUser(): Promise<User> {
const response = await fetch('/api/users/1');
return response.json();
}
// Error handling
async function safeRequest() {
try {
const result = await fetchUser();
} catch (error) {
console.error('Request failed:', error);
}
}
```
## Best Practices
1. **Avoid `any`**: Use `unknown` and type guards instead
2. **Use strict mode**: Enable `strict` in tsconfig.json
3. **Leverage utility types**: Reduce code duplication
4. **Document complex types**: Use JSDoc for clarity
5. **Test type definitions**: Use type-level tests
## Common Patterns
### Result Type Pattern
```typescript
type Result<T, E> =
| { ok: true; value: T }
| { ok: false; error: E };
function createUser(data: any): Result<User, string> {
try {
// validation and creation
return { ok: true, value: user };
} catch (e) {
return { ok: false, error: e.message };
}
}
```
### Builder Pattern
```typescript
class QueryBuilder<T> {
private query: any = {};
where(field: keyof T, value: any): this {
this.query[field] = value;
return this;
}
build() {
return this.query;
}
}
```
## Real-World Angular Examples
### Service Type Safety
```typescript
@Injectable()
export class UserService {
constructor(private http: HttpClient) {}
getUser(id: number): Observable<User> {
return this.http.get<User>(`/api/users/${id}`);
}
}
```
### Component Props
```typescript
interface ComponentProps {
title: string;
items: Item[];
onSelect: (item: Item) => void;
}
@Component({
selector: 'app-list',
template: `...`
})
export class ListComponent implements ComponentProps {
@Input() title!: string;
@Input() items: Item[] = [];
@Output() itemSelected = new EventEmitter<Item>();
onSelect(item: Item) {
this.itemSelected.emit(item);
}
}
```
## Performance Tips
- Use `const` assertions for literal types
- Leverage structural typing for flexibility
- Use discriminated unions for safe pattern matching
- Avoid circular type dependencies
- Use `omit` to reduce property access
## Resources
- [TypeScript Handbook](https://www.typescriptlang.org/docs/)
- [Advanced Types](https://www.typescriptlang.org/docs/handbook/2/types-from-types.html)
- [TypeScript Playground](https://www.typescriptlang.org/play)