Skip to content

Commit 93d2c37

Browse files
committed
Refactor: Improve type safety in Angular v0.9 Core by removing 'any' in favor of explicit typings for component props and children
1 parent a6109b0 commit 93d2c37

12 files changed

Lines changed: 72 additions & 84 deletions

renderers/angular/a2ui_explorer/src/app/card.component.ts

Lines changed: 0 additions & 47 deletions
This file was deleted.

renderers/angular/src/v0_9/catalog/basic/column.component.spec.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,12 @@ describe('ColumnComponent', () => {
8181
justify: { value: signal('start'), raw: 'start', onUpdate: () => {} },
8282
align: { value: signal('stretch'), raw: 'stretch', onUpdate: () => {} },
8383
children: {
84-
value: signal(['child1', 'child2']),
84+
value: signal({
85+
children: [
86+
{ id: 'child1', basePath: '/' },
87+
{ id: 'child2', basePath: '/' },
88+
],
89+
}),
8590
raw: ['child1', 'child2'],
8691
onUpdate: () => {},
8792
},
@@ -143,7 +148,14 @@ describe('ColumnComponent', () => {
143148
fixture.componentRef.setInput('props', {
144149
...component.props(),
145150
children: {
146-
value: signal([{}, {}]),
151+
value: signal({
152+
templateId: 'template1',
153+
path: 'items',
154+
children: [
155+
{ id: 'child1', basePath: '/items/0' },
156+
{ id: 'child2', basePath: '/items/1' },
157+
],
158+
}),
147159
raw: {
148160
componentId: 'template1',
149161
path: 'items',
@@ -192,7 +204,9 @@ describe('ColumnComponent', () => {
192204
it('should handle missing justify and align properties', () => {
193205
fixture.componentRef.setInput('props', {
194206
children: {
195-
value: signal(['child1']),
207+
value: signal({
208+
children: [{ id: 'child1', basePath: '/' }],
209+
}),
196210
raw: ['child1'],
197211
onUpdate: () => {},
198212
},

renderers/angular/src/v0_9/catalog/basic/column.component.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,17 +73,14 @@ export class ColumnComponent extends BasicCatalogComponent<typeof ColumnApi> {
7373
return val ? ALIGN_MAP[val] || val : undefined;
7474
});
7575

76-
protected readonly children = computed(() => {
77-
const raw = this.props()['children']?.value() || [];
78-
return Array.isArray(raw) ? raw : [];
79-
});
76+
protected readonly children = computed(() => this.props()['children']?.value()?.children || []);
8077

8178
protected readonly isRepeating = computed(() => {
82-
return !!this.props()['children']?.raw?.componentId;
79+
return !!this.props()['children']?.value()?.templateId;
8380
});
8481

8582
protected readonly templateId = computed(() => {
86-
return this.props()['children']?.raw?.componentId;
83+
return this.props()['children']?.value()?.templateId;
8784
});
8885

8986
protected readonly normalizedChildren = computed(() => {
@@ -97,6 +94,7 @@ export class ColumnComponent extends BasicCatalogComponent<typeof ColumnApi> {
9794
});
9895

9996
protected getNormalizedPath(index: number) {
100-
return getNormalizedPath(this.props()['children']?.raw?.path, this.dataContextPath(), index);
97+
return getNormalizedPath(this.props()['children']?.value()?.path, this.dataContextPath(), index);
10198
}
10299
}
100+

renderers/angular/src/v0_9/catalog/basic/complex-components.spec.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,6 @@ describe('Complex Components', () => {
435435
});
436436

437437
it('should handle missing children property', () => {
438-
fixture.componentRef.setInput('props', {});
439438
fixture.detectChanges();
440439
expect(component.children()).toEqual([]);
441440
});
@@ -497,7 +496,6 @@ describe('Complex Components', () => {
497496
});
498497

499498
it('should handle missing tabs property', () => {
500-
fixture.componentRef.setInput('props', {});
501499
fixture.detectChanges();
502500
expect(component.tabs()).toEqual([]);
503501
expect(fixture.nativeElement.querySelectorAll('.a2ui-tab-button').length).toBe(0);
@@ -600,7 +598,6 @@ describe('Complex Components', () => {
600598
});
601599

602600
it('should handle missing trigger or content', () => {
603-
fixture.componentRef.setInput('props', {});
604601
fixture.detectChanges();
605602
expect(component.trigger()).toBeUndefined();
606603
expect(component.content()).toBeUndefined();

renderers/angular/src/v0_9/catalog/basic/row.component.spec.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,12 @@ describe('RowComponent', () => {
8181
justify: { value: signal('center'), raw: 'center', onUpdate: () => {} },
8282
align: { value: signal('baseline'), raw: 'baseline', onUpdate: () => {} },
8383
children: {
84-
value: signal(['child1', 'child2']),
84+
value: signal({
85+
children: [
86+
{ id: 'child1', basePath: '/' },
87+
{ id: 'child2', basePath: '/' },
88+
],
89+
}),
8590
raw: ['child1', 'child2'],
8691
onUpdate: () => {},
8792
},
@@ -112,7 +117,14 @@ describe('RowComponent', () => {
112117
fixture.componentRef.setInput('props', {
113118
...component.props(),
114119
children: {
115-
value: signal([{}, {}]), // two items
120+
value: signal({
121+
templateId: 'template1',
122+
path: 'items',
123+
children: [
124+
{ id: 'child1', basePath: '/items/0' },
125+
{ id: 'child2', basePath: '/items/1' },
126+
],
127+
}),
116128
raw: {
117129
componentId: 'template1',
118130
path: 'items',
@@ -161,7 +173,9 @@ describe('RowComponent', () => {
161173
it('should handle missing justify and align properties', () => {
162174
fixture.componentRef.setInput('props', {
163175
children: {
164-
value: signal(['child1']),
176+
value: signal({
177+
children: [{ id: 'child1', basePath: '/' }],
178+
}),
165179
raw: ['child1'],
166180
onUpdate: () => {},
167181
},

renderers/angular/src/v0_9/catalog/basic/row.component.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,17 +71,14 @@ export class RowComponent extends BasicCatalogComponent<typeof RowApi> {
7171
return val ? ALIGN_MAP[val] || val : undefined;
7272
});
7373

74-
protected readonly children = computed(() => {
75-
const raw = this.props()['children']?.value() || [];
76-
return Array.isArray(raw) ? raw : [];
77-
});
74+
protected readonly children = computed(() => this.props()['children']?.value().children || []);
7875

7976
protected readonly isRepeating = computed(() => {
80-
return !!this.props()['children']?.raw?.componentId;
77+
return !!this.props()['children']?.value().templateId;
8178
});
8279

8380
protected readonly templateId = computed(() => {
84-
return this.props()['children']?.raw?.componentId;
81+
return this.props()['children']?.value().templateId;
8582
});
8683

8784
protected readonly normalizedChildren = computed(() => {
@@ -95,6 +92,6 @@ export class RowComponent extends BasicCatalogComponent<typeof RowApi> {
9592
});
9693

9794
protected getNormalizedPath(index: number) {
98-
return getNormalizedPath(this.props()['children']?.raw?.path, this.dataContextPath(), index);
95+
return getNormalizedPath(this.props()['children']?.value().path, this.dataContextPath(), index);
9996
}
10097
}

renderers/angular/src/v0_9/core/catalog_component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export abstract class CatalogComponent<Api extends ComponentApi> {
3030
/**
3131
* Reactive properties resolved from the A2UI ComponentModel.
3232
*/
33-
readonly props = input<ComponentApiToProps<Api>>({} as any);
33+
readonly props = input<ComponentApiToProps<Api>>({} as ComponentApiToProps<Api>);
3434
readonly surfaceId = input.required<string>();
3535
readonly componentId = input.required<string>();
3636
readonly dataContextPath = input<string>('/');

renderers/angular/src/v0_9/core/component-binder.service.spec.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { TestBed } from '@angular/core/testing';
1818
import { DestroyRef } from '@angular/core';
1919
import { signal as preactSignal } from '@preact/signals-core';
2020
import { ComponentContext } from '@a2ui/web_core/v0_9';
21-
import { ComponentBinder } from './component-binder.service';
21+
import { ComponentBinder, Children } from './component-binder.service';
2222

2323
describe('ComponentBinder', () => {
2424
let binder: ComponentBinder;
@@ -166,10 +166,12 @@ describe('ComponentBinder', () => {
166166
const bound = binder.bind(mockContext);
167167

168168
expect(bound['children']).toBeDefined();
169-
const children = bound['children'].value();
170-
expect(Array.isArray(children)).toBe(true);
171-
expect(children.length).toBe(2);
172-
expect(children[0]).toEqual({ id: 'item-comp', basePath: '/list/data/0' });
173-
expect(children[1]).toEqual({ id: 'item-comp', basePath: '/list/data/1' });
169+
const boundChildren = bound['children'].value() as Children;
170+
expect(boundChildren.templateId).toBe('item-comp');
171+
expect(boundChildren.path).toBe('/list/data');
172+
expect(Array.isArray(boundChildren.children)).toBe(true);
173+
expect(boundChildren.children.length).toBe(2);
174+
expect(boundChildren.children[0]).toEqual({ id: 'item-comp', basePath: '/list/data/0' });
175+
expect(boundChildren.children[1]).toEqual({ id: 'item-comp', basePath: '/list/data/1' });
174176
});
175177
});

renderers/angular/src/v0_9/core/component-binder.service.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ export interface Child {
2525
basePath: string;
2626
}
2727

28+
/** A collection of child components. */
29+
export interface Children {
30+
children: Child[];
31+
/** Optional component ID to be used as a template when instantiating the children. */
32+
templateId?: string;
33+
/** Optional path to the list of children. */
34+
path?: string;
35+
}
36+
2837
/**
2938
* Binds A2UI ComponentModel properties to reactive Angular Signals.
3039
*
@@ -84,15 +93,18 @@ export class ComponentBinder {
8493
});
8594
} else if (key === 'children') {
8695
const originalSig = preactSig;
96+
const templateId: string | undefined = value.componentId;
97+
const path: string | undefined = value.path;
8798
preactSig = computed(() => {
8899
const val = originalSig.value;
89100
const arr = Array.isArray(val) ? val : [];
90-
return arr.map(item => {
101+
const children: Child[] = arr.map(item => {
91102
if (typeof item === 'object' && item !== null && 'id' in item) {
92103
return item;
93104
}
94105
return { id: item, basePath: context.dataContext.path };
95106
});
107+
return { templateId, children, path };
96108
});
97109
}
98110

renderers/angular/src/v0_9/core/types.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import { Signal } from '@angular/core';
1818
import { z } from 'zod';
1919
import { ComponentApi, DataBindingSchema, FunctionCallSchema } from '@a2ui/web_core/v0_9';
20-
import { Child } from './component-binder.service';
20+
import { Child, Children } from './component-binder.service';
2121

2222
/**
2323
* Represents a component property bound to an Angular Signal and update logic.
@@ -28,7 +28,7 @@ import { Child } from './component-binder.service';
2828
*
2929
* @template T The type of the property value.
3030
*/
31-
export interface BoundProperty<T = any> {
31+
export interface BoundProperty<T = unknown> {
3232
/**
3333
* The reactive Angular Signal containing the current resolved value.
3434
*
@@ -42,7 +42,7 @@ export interface BoundProperty<T = any> {
4242
*
4343
* This may be a literal value or a data binding path object.
4444
*/
45-
readonly raw: any;
45+
readonly raw: unknown;
4646

4747
/**
4848
* Callback to update the value in the A2UI DataContext.
@@ -59,7 +59,7 @@ type FunctionCallType = z.infer<typeof FunctionCallSchema>;
5959
type DynamicSchemaValueToRaw<Input> = Exclude<Input, DataBindingType | FunctionCallType>;
6060

6161
type InferredInterfaceToProps<InferredSchema> = {
62-
[K in keyof InferredSchema]: K extends 'children' | 'child' | 'trigger' | 'content'
62+
[K in keyof InferredSchema]: K extends 'children' ? BoundProperty<Children> : K extends 'child' | 'trigger' | 'content'
6363
? BoundProperty<Child>
6464
: BoundProperty<DynamicSchemaValueToRaw<InferredSchema[K]>>
6565
}
@@ -70,7 +70,7 @@ interface CheckProps {
7070
}
7171

7272
/** The binder can add some properties to the Props object. This util adds them to the type. */
73-
export type ExtendedProps<ComponentProps extends { [key: string]: any }> =
73+
export type ExtendedProps<ComponentProps extends { [key: string]: unknown }> =
7474
'checks' extends keyof ComponentProps ? Omit<ComponentProps, 'checks'> & CheckProps : ComponentProps;
7575

7676
/**

0 commit comments

Comments
 (0)