Initial commit
This commit is contained in:
260
commands/create-component.md
Normal file
260
commands/create-component.md
Normal file
@@ -0,0 +1,260 @@
|
||||
---
|
||||
name: create-component
|
||||
description: Generate a complete Angular 17+ standalone component with template, styles, and optional tests following best practices
|
||||
---
|
||||
|
||||
Generate a production-ready Angular component with all necessary files and configurations.
|
||||
|
||||
## Component Types
|
||||
|
||||
Ask the user which type:
|
||||
|
||||
1. **Smart Component (Container)** - Manages data and business logic
|
||||
2. **Dumb Component (Presentational)** - Displays data only
|
||||
3. **Form Component** - Handles form input
|
||||
4. **Layout Component** - App structure (header, sidebar, etc.)
|
||||
|
||||
## Information Needed
|
||||
|
||||
1. **Component name** - kebab-case (e.g., `user-profile`)
|
||||
2. **Component type** - Smart or Dumb
|
||||
3. **Location** - Feature folder or shared
|
||||
4. **Include tests?** - Yes/No
|
||||
5. **Data to display** - What data does it work with?
|
||||
6. **Actions** - What actions can users perform?
|
||||
|
||||
## Generated Files
|
||||
|
||||
```
|
||||
component-name/
|
||||
├── component-name.component.ts # TypeScript class
|
||||
├── component-name.component.html # Template
|
||||
├── component-name.component.scss # Styles
|
||||
└── component-name.component.spec.ts # Tests (optional)
|
||||
```
|
||||
|
||||
## Smart Component Template
|
||||
|
||||
```typescript
|
||||
import { Component, signal, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ComponentNameService } from './services/component-name.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-component-name',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './component-name.component.html',
|
||||
styleUrls: ['./component-name.component.scss']
|
||||
})
|
||||
export class ComponentNameComponent {
|
||||
private service = inject(ComponentNameService);
|
||||
|
||||
data = signal<DataType[]>([]);
|
||||
loading = signal(false);
|
||||
error = signal<string | null>(null);
|
||||
|
||||
ngOnInit() {
|
||||
this.loadData();
|
||||
}
|
||||
|
||||
loadData() {
|
||||
this.loading.set(true);
|
||||
this.service.getData().subscribe({
|
||||
next: data => {
|
||||
this.data.set(data);
|
||||
this.loading.set(false);
|
||||
},
|
||||
error: err => {
|
||||
this.error.set(err.message);
|
||||
this.loading.set(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleAction(id: string) {
|
||||
// Handle user action
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Dumb Component Template
|
||||
|
||||
```typescript
|
||||
import { Component, input, output, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-component-name',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './component-name.component.html',
|
||||
styleUrls: ['./component-name.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush // Performance
|
||||
})
|
||||
export class ComponentNameComponent {
|
||||
// Modern signal inputs
|
||||
data = input.required<DataType>();
|
||||
disabled = input(false);
|
||||
|
||||
// Modern signal outputs
|
||||
action = output<string>();
|
||||
delete = output<string>();
|
||||
|
||||
handleClick() {
|
||||
this.action.emit(this.data().id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Template Structure
|
||||
|
||||
### For List Components
|
||||
```html
|
||||
<div class="component-name-container">
|
||||
@if (loading()) {
|
||||
<app-loading-spinner />
|
||||
} @else if (error()) {
|
||||
<app-error-message [message]="error()" />
|
||||
} @else {
|
||||
@for (item of data(); track item.id) {
|
||||
<div class="item">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
} @empty {
|
||||
<p class="empty-state">No items found</p>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
```
|
||||
|
||||
### For Detail Components
|
||||
```html
|
||||
<div class="component-name-detail">
|
||||
@if (data(); as item) {
|
||||
<h1>{{ item.title }}</h1>
|
||||
<p>{{ item.description }}</p>
|
||||
<div class="actions">
|
||||
<button (click)="handleEdit()">Edit</button>
|
||||
<button (click)="handleDelete()">Delete</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
```
|
||||
|
||||
## Styling Template (SCSS)
|
||||
|
||||
```scss
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.component-name-container {
|
||||
padding: 1rem;
|
||||
|
||||
.item {
|
||||
margin-bottom: 1rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
padding: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive
|
||||
@media (max-width: 768px) {
|
||||
.component-name-container {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Test Template
|
||||
|
||||
```typescript
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentNameComponent } from './component-name.component';
|
||||
|
||||
describe('ComponentNameComponent', () => {
|
||||
let component: ComponentNameComponent;
|
||||
let fixture: ComponentFixture<ComponentNameComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ComponentNameComponent]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ComponentNameComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should display data', () => {
|
||||
component.data.set([{ id: '1', name: 'Test' }]);
|
||||
fixture.detectChanges();
|
||||
|
||||
const compiled = fixture.nativeElement;
|
||||
expect(compiled.querySelector('.item')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices to Include
|
||||
|
||||
1. **Always standalone: true**
|
||||
2. **Separate template and style files** (never inline)
|
||||
3. **OnPush for dumb components**
|
||||
4. **Use signals for local state**
|
||||
5. **TrackBy in @for loops**
|
||||
6. **Proper error handling**
|
||||
7. **Loading states**
|
||||
8. **Empty states**
|
||||
9. **Accessibility attributes**
|
||||
10. **Responsive design**
|
||||
|
||||
## Component Checklist
|
||||
|
||||
Generated component should have:
|
||||
- [ ] Standalone: true
|
||||
- [ ] Proper imports
|
||||
- [ ] Separate template file
|
||||
- [ ] Separate styles file
|
||||
- [ ] Signal-based state (if smart)
|
||||
- [ ] Input/Output (if dumb)
|
||||
- [ ] OnPush (if dumb)
|
||||
- [ ] TrackBy functions
|
||||
- [ ] Error handling
|
||||
- [ ] Loading states
|
||||
- [ ] Tests (if requested)
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# In Claude Code
|
||||
/angular-development:create-component
|
||||
|
||||
# Natural language
|
||||
"Create a smart component called product-list that displays products"
|
||||
"Generate a dumb component for user-card with name and email inputs"
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Smart components go in `features/<feature>/components/`
|
||||
- Dumb components go in `shared/components/`
|
||||
- Always ask about data structure before generating
|
||||
- Include proper TypeScript types
|
||||
- Add comments for complex logic
|
||||
427
commands/create-service.md
Normal file
427
commands/create-service.md
Normal file
@@ -0,0 +1,427 @@
|
||||
---
|
||||
name: create-service
|
||||
description: Generate Angular service with HTTP methods, error handling, caching, and proper dependency injection
|
||||
---
|
||||
|
||||
Generate a production-ready Angular service with HTTP communication, state management, and error handling.
|
||||
|
||||
## Service Types
|
||||
|
||||
Ask the user which type:
|
||||
|
||||
1. **Data Service** - HTTP API communication
|
||||
2. **State Service** - Global state management
|
||||
3. **Utility Service** - Helper functions and utilities
|
||||
4. **Facade Service** - Simplifies complex subsystems
|
||||
|
||||
## Information Needed
|
||||
|
||||
1. **Service name** - kebab-case (e.g., `user-service`)
|
||||
2. **Service type** - Data, State, Utility, or Facade
|
||||
3. **API endpoint** - Base URL for data services
|
||||
4. **Data model** - What type of data does it handle?
|
||||
5. **Operations needed** - CRUD? Search? Filter?
|
||||
6. **Caching?** - Should responses be cached?
|
||||
|
||||
## Service Structure
|
||||
|
||||
### Data Service Template
|
||||
|
||||
```typescript
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { catchError, retry, shareReplay } from 'rxjs/operators';
|
||||
import { environment } from '@environments/environment';
|
||||
|
||||
export interface DataModel {
|
||||
id: string;
|
||||
// Add properties
|
||||
}
|
||||
|
||||
export interface QueryParams {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root' // Singleton service
|
||||
})
|
||||
export class DataService {
|
||||
private http = inject(HttpClient);
|
||||
private baseUrl = `${environment.apiUrl}/data`;
|
||||
|
||||
// GET all with pagination
|
||||
getAll(params?: QueryParams): Observable<DataModel[]> {
|
||||
let httpParams = new HttpParams();
|
||||
|
||||
if (params) {
|
||||
Object.keys(params).forEach(key => {
|
||||
if (params[key as keyof QueryParams] !== undefined) {
|
||||
httpParams = httpParams.set(key, params[key as keyof QueryParams]!.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return this.http.get<DataModel[]>(this.baseUrl, { params: httpParams }).pipe(
|
||||
retry(2),
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
|
||||
// GET by ID with caching
|
||||
getById(id: string): Observable<DataModel> {
|
||||
return this.http.get<DataModel>(`${this.baseUrl}/${id}`).pipe(
|
||||
shareReplay(1), // Cache for multiple subscribers
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
|
||||
// POST - Create
|
||||
create(data: Omit<DataModel, 'id'>): Observable<DataModel> {
|
||||
return this.http.post<DataModel>(this.baseUrl, data).pipe(
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
|
||||
// PUT - Update
|
||||
update(id: string, data: Partial<DataModel>): Observable<DataModel> {
|
||||
return this.http.put<DataModel>(`${this.baseUrl}/${id}`, data).pipe(
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
|
||||
// PATCH - Partial update
|
||||
patch(id: string, data: Partial<DataModel>): Observable<DataModel> {
|
||||
return this.http.patch<DataModel>(`${this.baseUrl}/${id}`, data).pipe(
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
|
||||
// DELETE
|
||||
delete(id: string): Observable<void> {
|
||||
return this.http.delete<void>(`${this.baseUrl}/${id}`).pipe(
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
|
||||
// Search
|
||||
search(query: string): Observable<DataModel[]> {
|
||||
return this.http.get<DataModel[]>(`${this.baseUrl}/search`, {
|
||||
params: { q: query }
|
||||
}).pipe(
|
||||
catchError(this.handleError)
|
||||
);
|
||||
}
|
||||
|
||||
// Error handling
|
||||
private handleError(error: any): Observable<never> {
|
||||
console.error('Service error:', error);
|
||||
|
||||
let errorMessage = 'An error occurred';
|
||||
|
||||
if (error.error instanceof ErrorEvent) {
|
||||
// Client-side error
|
||||
errorMessage = `Error: ${error.error.message}`;
|
||||
} else {
|
||||
// Server-side error
|
||||
errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`;
|
||||
}
|
||||
|
||||
return throwError(() => new Error(errorMessage));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### State Service Template
|
||||
|
||||
```typescript
|
||||
import { Injectable, signal, computed } from '@angular/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
|
||||
export interface AppState {
|
||||
loading: boolean;
|
||||
data: DataModel[];
|
||||
selectedId: string | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class StateService {
|
||||
// Using Signals (Modern approach)
|
||||
private dataSignal = signal<DataModel[]>([]);
|
||||
private loadingSignal = signal(false);
|
||||
private errorSignal = signal<string | null>(null);
|
||||
|
||||
// Public readonly signals
|
||||
readonly data = this.dataSignal.asReadonly();
|
||||
readonly loading = this.loadingSignal.asReadonly();
|
||||
readonly error = this.errorSignal.asReadonly();
|
||||
|
||||
// Computed signals
|
||||
readonly itemCount = computed(() => this.data().length);
|
||||
readonly hasData = computed(() => this.data().length > 0);
|
||||
|
||||
// OR using BehaviorSubject (Traditional approach)
|
||||
private stateSubject = new BehaviorSubject<AppState>({
|
||||
loading: false,
|
||||
data: [],
|
||||
selectedId: null,
|
||||
error: null
|
||||
});
|
||||
|
||||
state$ = this.stateSubject.asObservable();
|
||||
|
||||
// Setters
|
||||
setData(data: DataModel[]) {
|
||||
this.dataSignal.set(data);
|
||||
}
|
||||
|
||||
addItem(item: DataModel) {
|
||||
this.dataSignal.update(current => [...current, item]);
|
||||
}
|
||||
|
||||
updateItem(id: string, updates: Partial<DataModel>) {
|
||||
this.dataSignal.update(current =>
|
||||
current.map(item => item.id === id ? { ...item, ...updates } : item)
|
||||
);
|
||||
}
|
||||
|
||||
removeItem(id: string) {
|
||||
this.dataSignal.update(current => current.filter(item => item.id !== id));
|
||||
}
|
||||
|
||||
setLoading(loading: boolean) {
|
||||
this.loadingSignal.set(loading);
|
||||
}
|
||||
|
||||
setError(error: string | null) {
|
||||
this.errorSignal.set(error);
|
||||
}
|
||||
|
||||
clearState() {
|
||||
this.dataSignal.set([]);
|
||||
this.errorSignal.set(null);
|
||||
this.loadingSignal.set(false);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Facade Service Template
|
||||
|
||||
```typescript
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { Observable, forkJoin, combineLatest } from 'rxjs';
|
||||
import { map, switchMap, tap } from 'rxjs/operators';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class FacadeService {
|
||||
private dataService = inject(DataService);
|
||||
private stateService = inject(StateService);
|
||||
private cacheService = inject(CacheService);
|
||||
|
||||
// Simplified API for components
|
||||
readonly data$ = this.stateService.data;
|
||||
readonly loading$ = this.stateService.loading;
|
||||
|
||||
// Complex operation simplified
|
||||
loadData(): Observable<DataModel[]> {
|
||||
this.stateService.setLoading(true);
|
||||
|
||||
return this.dataService.getAll().pipe(
|
||||
tap(data => {
|
||||
this.stateService.setData(data);
|
||||
this.cacheService.set('data', data);
|
||||
this.stateService.setLoading(false);
|
||||
}),
|
||||
catchError(error => {
|
||||
this.stateService.setError(error.message);
|
||||
this.stateService.setLoading(false);
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Orchestrate multiple services
|
||||
initialize(): Observable<any> {
|
||||
return forkJoin({
|
||||
config: this.dataService.getConfig(),
|
||||
user: this.dataService.getUser(),
|
||||
permissions: this.dataService.getPermissions()
|
||||
}).pipe(
|
||||
tap(({ config, user, permissions }) => {
|
||||
this.stateService.setConfig(config);
|
||||
this.stateService.setUser(user);
|
||||
this.stateService.setPermissions(permissions);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Utility Service Template
|
||||
|
||||
```typescript
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class UtilityService {
|
||||
// Date utilities
|
||||
formatDate(date: Date | string): string {
|
||||
return new Date(date).toLocaleDateString('en-US');
|
||||
}
|
||||
|
||||
// String utilities
|
||||
capitalize(str: string): string {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
slugify(str: string): string {
|
||||
return str
|
||||
.toLowerCase()
|
||||
.replace(/[^\w\s-]/g, '')
|
||||
.replace(/[\s_-]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
}
|
||||
|
||||
// Array utilities
|
||||
groupBy<T>(array: T[], key: keyof T): Record<string, T[]> {
|
||||
return array.reduce((result, item) => {
|
||||
const groupKey = String(item[key]);
|
||||
if (!result[groupKey]) {
|
||||
result[groupKey] = [];
|
||||
}
|
||||
result[groupKey].push(item);
|
||||
return result;
|
||||
}, {} as Record<string, T[]>);
|
||||
}
|
||||
|
||||
// Number utilities
|
||||
formatCurrency(amount: number, currency = 'USD'): string {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency
|
||||
}).format(amount);
|
||||
}
|
||||
|
||||
// Validation utilities
|
||||
isValidEmail(email: string): boolean {
|
||||
const regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||
return regex.test(email);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Service with Caching
|
||||
|
||||
```typescript
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { Observable, of, timer } from 'rxjs';
|
||||
import { tap, switchMap, shareReplay } from 'rxjs/operators';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CachedDataService {
|
||||
private http = inject(HttpClient);
|
||||
private cache = new Map<string, { data: any; timestamp: number }>();
|
||||
private TTL = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
getData(key: string, forceRefresh = false): Observable<DataModel[]> {
|
||||
const cached = this.cache.get(key);
|
||||
|
||||
// Return cached if valid
|
||||
if (!forceRefresh && cached && Date.now() - cached.timestamp < this.TTL) {
|
||||
return of(cached.data);
|
||||
}
|
||||
|
||||
// Fetch fresh data
|
||||
return this.http.get<DataModel[]>(`/api/${key}`).pipe(
|
||||
tap(data => {
|
||||
this.cache.set(key, {
|
||||
data,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}),
|
||||
shareReplay(1)
|
||||
);
|
||||
}
|
||||
|
||||
invalidateCache(key?: string) {
|
||||
if (key) {
|
||||
this.cache.delete(key);
|
||||
} else {
|
||||
this.cache.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Test Template
|
||||
|
||||
```typescript
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||
import { DataService } from './data.service';
|
||||
|
||||
describe('DataService', () => {
|
||||
let service: DataService;
|
||||
let httpMock: HttpTestingController;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule],
|
||||
providers: [DataService]
|
||||
});
|
||||
|
||||
service = TestBed.inject(DataService);
|
||||
httpMock = TestBed.inject(HttpTestingController);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
httpMock.verify();
|
||||
});
|
||||
|
||||
it('should fetch data', () => {
|
||||
const mockData = [{ id: '1', name: 'Test' }];
|
||||
|
||||
service.getAll().subscribe(data => {
|
||||
expect(data).toEqual(mockData);
|
||||
});
|
||||
|
||||
const req = httpMock.expectOne(`${service['baseUrl']}`);
|
||||
expect(req.request.method).toBe('GET');
|
||||
req.flush(mockData);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use `providedIn: 'root'`** for singleton services
|
||||
2. **Inject dependencies with `inject()`** function
|
||||
3. **Handle errors properly** with catchError
|
||||
4. **Add retry logic** for network requests
|
||||
5. **Cache responses** when appropriate
|
||||
6. **Use interfaces** for data models
|
||||
7. **Add JSDoc comments** for complex methods
|
||||
8. **Write tests** for all public methods
|
||||
9. **Use environment variables** for URLs
|
||||
10. **Use HttpParams** for query parameters
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
/angular-development:create-service
|
||||
|
||||
# Natural language
|
||||
"Create a data service for products with CRUD operations"
|
||||
"Generate a state service for shopping cart"
|
||||
```
|
||||
Reference in New Issue
Block a user