@@ -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' })
7475export 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
85112The app supports multiple deployment environments:
86113- ` environment.ts ` - Development
@@ -161,7 +188,7 @@ import { BaseRowFieldComponent } from '../base-row-field/base-row-field.componen
161188import { 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})
172199export 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+
209257describe (' 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