Skip to content

Commit 65c9cb3

Browse files
guguclaude
andcommitted
docs: update CLAUDE.md to require signals for new code
- Add Angular Signals as core technology - Update service example to show rxResource pattern - Update component structure to show signal-based state - Update test structure to show proper typing (no `as any`) - Add signals requirement section in important notes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 2b02171 commit 65c9cb3

1 file changed

Lines changed: 103 additions & 29 deletions

File tree

frontend/CLAUDE.md

Lines changed: 103 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,11 @@ yarn install # Install dependencies
4242
## 🏗 Architecture Overview
4343

4444
### Core Technologies
45-
- **Angular 19** with standalone components architecture
45+
- **Angular 20** with standalone components architecture
4646
- **TypeScript 5.6** targeting ES2022
47-
- **Angular Material 19** for UI components
48-
- **RxJS 7.4** for reactive programming
47+
- **Angular Signals** for reactive state management
48+
- **Angular Material 19** for UI components
49+
- **RxJS 7.4** for reactive programming (HTTP calls, complex streams)
4950
- **SCSS** with Material Design theming
5051
- **Jasmine/Karma** for testing
5152

@@ -66,21 +67,47 @@ export class ExampleComponent implements OnInit {
6667
}
6768
```
6869

69-
#### Service-Based State Management
70-
No NgRx - uses BehaviorSubject pattern for state management:
70+
#### Signal-Based State Management (Required for New Code)
71+
All new code must use Angular signals for state management. Use `rxResource` for data fetching:
7172

7273
```typescript
7374
@Injectable({ providedIn: 'root' })
7475
export class DataService {
75-
private dataSubject = new BehaviorSubject<any>('');
76-
public cast = this.dataSubject.asObservable();
77-
78-
updateData(newData: any) {
79-
this.dataSubject.next(newData);
76+
private _http = inject(HttpClient);
77+
78+
// Reactive parameter for data fetching
79+
private _activeId = signal<string | null>(null);
80+
81+
// Use rxResource for reactive data fetching
82+
private _dataResource = rxResource({
83+
params: () => this._activeId(),
84+
stream: ({ params: id }) => {
85+
if (!id) return EMPTY;
86+
return this._http.get<Data[]>(`/api/data/${id}`);
87+
},
88+
});
89+
90+
// Expose as readonly signals
91+
public readonly data = computed(() => this._dataResource.value() ?? []);
92+
public readonly loading = computed(() => this._dataResource.isLoading());
93+
94+
setActiveId(id: string): void {
95+
this._activeId.set(id);
96+
}
97+
98+
refresh(): void {
99+
this._dataResource.reload();
80100
}
81101
}
82102
```
83103

104+
**Legacy pattern** (BehaviorSubject - avoid in new code):
105+
```typescript
106+
// Old pattern - do not use for new code
107+
private dataSubject = new BehaviorSubject<any>('');
108+
public cast = this.dataSubject.asObservable();
109+
```
110+
84111
#### Multi-Environment Support
85112
The app supports multiple deployment environments:
86113
- `environment.ts` - Development
@@ -161,7 +188,7 @@ import { BaseRowFieldComponent } from '../base-row-field/base-row-field.componen
161188
import { DataService } from 'src/app/services/data.service';
162189
```
163190

164-
### Component Structure
191+
### Component Structure (Signal-Based)
165192
```typescript
166193
@Component({
167194
selector: 'app-widget-name',
@@ -170,27 +197,41 @@ import { DataService } from 'src/app/services/data.service';
170197
imports: [CommonModule, MatModule, ...], // All required imports
171198
})
172199
export class WidgetNameComponent implements OnInit {
173-
// Input/Output properties first
174-
@Input() inputProperty: string;
175-
@Output() outputEvent = new EventEmitter<any>();
176-
177-
// Public properties
178-
public publicProperty: string;
179-
180-
// Private properties
181-
private _privateProperty: string;
182-
200+
// Dependency injection
201+
private _dataService = inject(DataService);
202+
203+
// Signals for component state (required for new code)
204+
protected loading = signal(false);
205+
protected items = signal<Item[]>([]);
206+
protected searchQuery = signal('');
207+
208+
// Computed signals for derived state
209+
protected filteredItems = computed(() => {
210+
const items = this.items();
211+
const query = this.searchQuery().toLowerCase();
212+
return query ? items.filter(i => i.name.toLowerCase().includes(query)) : items;
213+
});
214+
215+
// Effects for side effects
216+
constructor() {
217+
effect(() => {
218+
const query = this.searchQuery();
219+
// React to signal changes
220+
});
221+
}
222+
183223
// Lifecycle hooks
184224
ngOnInit(): void {
185225
// Initialization logic
186226
}
187-
227+
188228
// Public methods
189-
public handleClick(): void {
229+
handleClick(): void {
230+
this.loading.set(true);
190231
// Event handling
191232
}
192-
193-
// Private methods
233+
234+
// Private methods at the end
194235
private _helperMethod(): void {
195236
// Internal logic
196237
}
@@ -206,23 +247,47 @@ export class WidgetNameComponent implements OnInit {
206247

207248
### Test Structure
208249
```typescript
250+
// Define testable type for accessing protected signals (avoid `as any`)
251+
type ComponentNameTestable = ComponentName & {
252+
loading: Signal<boolean>;
253+
items: WritableSignal<Item[]>;
254+
searchQuery: WritableSignal<string>;
255+
};
256+
209257
describe('ComponentName', () => {
210258
let component: ComponentName;
211259
let fixture: ComponentFixture<ComponentName>;
212-
260+
let mockDataService: Partial<DataService>;
261+
213262
beforeEach(async () => {
263+
// Use Partial<ServiceType> instead of `any` for mocks
264+
mockDataService = {
265+
data: signal([]).asReadonly(),
266+
loading: signal(false).asReadonly(),
267+
setActiveId: vi.fn(),
268+
};
269+
214270
await TestBed.configureTestingModule({
215271
imports: [ComponentName, MaterialModules, ...],
216-
providers: [provideHttpClient(), MockServices, ...]
272+
providers: [
273+
provideHttpClient(),
274+
{ provide: DataService, useValue: mockDataService },
275+
]
217276
}).compileComponents();
218-
277+
219278
fixture = TestBed.createComponent(ComponentName);
220279
component = fixture.componentInstance;
221280
});
222-
281+
223282
it('should create', () => {
224283
expect(component).toBeTruthy();
225284
});
285+
286+
it('should access protected signals with proper typing', () => {
287+
const testable = component as ComponentNameTestable;
288+
testable.searchQuery.set('test');
289+
expect(testable.loading()).toBe(false);
290+
});
226291
});
227292
```
228293

@@ -283,6 +348,15 @@ Custom launcher `ChromeHeadlessCustom` is configured for CI with flags `--no-san
283348

284349
## 🚨 Important Notes
285350

351+
### Signals Requirement
352+
**All new code must use Angular signals** for state management:
353+
- Use `signal()` for component state instead of plain properties
354+
- Use `computed()` for derived state
355+
- Use `effect()` for side effects
356+
- Use `rxResource()` in services for data fetching
357+
- Avoid BehaviorSubject in new code
358+
- Never use `as any` in tests - use `Partial<ServiceType>` and testable type aliases
359+
286360
### Migration Recommendations
287361
- **TSLint → ESLint**: Current linting uses deprecated TSLint
288362
- **Material Design 3**: Consider upgrading from M2 to M3 APIs

0 commit comments

Comments
 (0)