Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 103 additions & 29 deletions frontend/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,11 @@ yarn install # Install dependencies
## 🏗 Architecture Overview

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

Expand All @@ -66,21 +67,47 @@ export class ExampleComponent implements OnInit {
}
```

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

```typescript
@Injectable({ providedIn: 'root' })
export class DataService {
private dataSubject = new BehaviorSubject<any>('');
public cast = this.dataSubject.asObservable();

updateData(newData: any) {
this.dataSubject.next(newData);
private _http = inject(HttpClient);

// Reactive parameter for data fetching
private _activeId = signal<string | null>(null);

// Use rxResource for reactive data fetching
private _dataResource = rxResource({
params: () => this._activeId(),
stream: ({ params: id }) => {
if (!id) return EMPTY;
return this._http.get<Data[]>(`/api/data/${id}`);
},
});

// Expose as readonly signals
public readonly data = computed(() => this._dataResource.value() ?? []);
public readonly loading = computed(() => this._dataResource.isLoading());

setActiveId(id: string): void {
this._activeId.set(id);
}

refresh(): void {
this._dataResource.reload();
}
}
```

**Legacy pattern** (BehaviorSubject - avoid in new code):
```typescript
// Old pattern - do not use for new code
private dataSubject = new BehaviorSubject<any>('');
public cast = this.dataSubject.asObservable();
```

#### Multi-Environment Support
The app supports multiple deployment environments:
- `environment.ts` - Development
Expand Down Expand Up @@ -161,7 +188,7 @@ import { BaseRowFieldComponent } from '../base-row-field/base-row-field.componen
import { DataService } from 'src/app/services/data.service';
```

### Component Structure
### Component Structure (Signal-Based)
```typescript
@Component({
selector: 'app-widget-name',
Expand All @@ -170,27 +197,41 @@ import { DataService } from 'src/app/services/data.service';
imports: [CommonModule, MatModule, ...], // All required imports
})
export class WidgetNameComponent implements OnInit {
// Input/Output properties first
@Input() inputProperty: string;
@Output() outputEvent = new EventEmitter<any>();

// Public properties
public publicProperty: string;

// Private properties
private _privateProperty: string;

// Dependency injection
private _dataService = inject(DataService);

// Signals for component state (required for new code)
protected loading = signal(false);
protected items = signal<Item[]>([]);
protected searchQuery = signal('');

// Computed signals for derived state
protected filteredItems = computed(() => {
const items = this.items();
const query = this.searchQuery().toLowerCase();
return query ? items.filter(i => i.name.toLowerCase().includes(query)) : items;
});

// Effects for side effects
constructor() {
effect(() => {
const query = this.searchQuery();
// React to signal changes
});
}

// Lifecycle hooks
ngOnInit(): void {
// Initialization logic
}

// Public methods
public handleClick(): void {
handleClick(): void {
this.loading.set(true);
// Event handling
}
// Private methods

// Private methods at the end
private _helperMethod(): void {
// Internal logic
}
Expand All @@ -206,23 +247,47 @@ export class WidgetNameComponent implements OnInit {

### Test Structure
```typescript
// Define testable type for accessing protected signals (avoid `as any`)
type ComponentNameTestable = ComponentName & {
loading: Signal<boolean>;
items: WritableSignal<Item[]>;
searchQuery: WritableSignal<string>;
};

describe('ComponentName', () => {
let component: ComponentName;
let fixture: ComponentFixture<ComponentName>;

let mockDataService: Partial<DataService>;

beforeEach(async () => {
// Use Partial<ServiceType> instead of `any` for mocks
mockDataService = {
data: signal([]).asReadonly(),
loading: signal(false).asReadonly(),
setActiveId: vi.fn(),
};

await TestBed.configureTestingModule({
imports: [ComponentName, MaterialModules, ...],
providers: [provideHttpClient(), MockServices, ...]
providers: [
provideHttpClient(),
{ provide: DataService, useValue: mockDataService },
]
}).compileComponents();

fixture = TestBed.createComponent(ComponentName);
component = fixture.componentInstance;
});

it('should create', () => {
expect(component).toBeTruthy();
});

it('should access protected signals with proper typing', () => {
const testable = component as ComponentNameTestable;
testable.searchQuery.set('test');
expect(testable.loading()).toBe(false);
});
});
```

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

## 🚨 Important Notes

### Signals Requirement
**All new code must use Angular signals** for state management:
- Use `signal()` for component state instead of plain properties
- Use `computed()` for derived state
- Use `effect()` for side effects
- Use `rxResource()` in services for data fetching
- Avoid BehaviorSubject in new code
- Never use `as any` in tests - use `Partial<ServiceType>` and testable type aliases

### Migration Recommendations
- **TSLint → ESLint**: Current linting uses deprecated TSLint
- **Material Design 3**: Consider upgrading from M2 to M3 APIs
Expand Down
2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"amplitude-js": "^8.21.9",
"angular-password-strength-meter": "npm:@eresearchqut/angular-password-strength-meter@^13.0.7",
"angulartics2": "^14.1.0",
"chart.js": "^4.5.1",
"color-string": "^2.0.1",
"convert": "^5.12.0",
"date-fns": "^4.1.0",
Expand All @@ -50,6 +51,7 @@
"mermaid": "^11.12.1",
"monaco-editor": "0.55.1",
"ng-dynamic-component": "^10.7.0",
"ng2-charts": "^8.0.0",
"ngx-cookie-service": "^19.0.0",
"ngx-markdown": "^19.1.1",
"ngx-stripe": "^19.0.0",
Expand Down
Loading
Loading