Skip to content

Commit 87460e3

Browse files
committed
fix(angular): resolve capitalization binding and DataContext integration
- update DataContext instantiation in button.component.ts to pass surface - fix function aggregation in DemoCatalog (array-based) - use web_core effect in utils.ts to ensure single source of truth - add unit test for capitalization function binding - exclude tests in tsconfig.app.json to fix compilation scopes - remove temporary verification script
1 parent cf3e6ef commit 87460e3

12 files changed

Lines changed: 119 additions & 72 deletions

File tree

renderers/angular/demo-app/src/app/demo-catalog.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,13 @@ export class DemoCatalog extends MinimalCatalog {
5454
const components = Array.from(this.components.values());
5555
components.push(customSliderApi, cardApi);
5656

57-
const functions = {
57+
const functions = [
5858
...BASIC_FUNCTIONS,
59-
...Object.fromEntries(this.functions?.entries() || []),
60-
};
59+
...Array.from(this.functions?.values() || []),
60+
];
61+
6162
(this as any).components = new Map(components.map((c) => [c.name, c]));
62-
(this as any).functions = new Map(Object.entries(functions));
63+
(this as any).functions = new Map(functions.map((f) => [f.name, f]));
6364
(this as any).id = 'https://a2ui.org/specification/v0_9/catalogs/minimal/minimal_catalog.json';
6465
}
6566
}

renderers/angular/demo-app/src/app/examples-bundle.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ export const EXAMPLES = [
295295
{
296296
id: 'root',
297297
component: 'Column',
298-
children: ['input_field', 'result_label', 'result_text'],
298+
children: ['input_field', 'result_label', 'result_text', 'submit_button'],
299299
justify: 'start',
300300
align: 'stretch',
301301
},
@@ -328,6 +328,32 @@ export const EXAMPLES = [
328328
},
329329
variant: 'h2',
330330
},
331+
{
332+
id: 'submit_button',
333+
component: 'Button',
334+
child: 'submit_label',
335+
variant: 'primary',
336+
action: {
337+
event: {
338+
name: 'capitalized_submit',
339+
context: {
340+
value: {
341+
call: 'capitalize',
342+
args: {
343+
value: {
344+
path: '/inputValue',
345+
},
346+
},
347+
},
348+
},
349+
},
350+
},
351+
},
352+
{
353+
id: 'submit_label',
354+
component: 'Text',
355+
text: 'Submit',
356+
},
331357
],
332358
},
333359
},

renderers/angular/demo-app/tsconfig.app.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@
1010
"rootDir": "../"
1111
},
1212
"include": ["src/**/*.ts", "../src/lib/v0_9/**/*.ts"],
13-
"exclude": ["src/**/*.spec.ts"]
13+
"exclude": ["src/**/*.spec.ts", "../src/**/*.spec.ts"]
1414
}
1515

renderers/angular/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"scripts": {
66
"build": "ng build && node postprocess-build.mjs",
77
"demo": "ng serve demo-app",
8-
"regression-test": "node verify_capitalization.js"
8+
"test": "ng test lib --watch=false"
99
},
1010
"dependencies": {
1111
"@a2ui/web_core": "file:../web_core",

renderers/angular/src/lib/v0_9/catalog/minimal/button.component.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,8 @@ export class ButtonComponent {
8181
const surface = this.rendererService.surfaceGroup?.getSurface(this.surfaceId);
8282
if (surface) {
8383
const dataContext = new DataContext(
84-
surface.dataModel,
84+
surface,
8585
this.dataContextPath,
86-
surface.catalog.invoker,
8786
);
8887
const resolvedAction = dataContext.resolveAction(action);
8988
surface.dispatchAction(resolvedAction);

renderers/angular/src/lib/v0_9/core/a2ui-renderer.service.spec.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ describe('A2uiRendererService', () => {
2626
mockCatalog = {
2727
components: new Map(),
2828
functions: new Map(),
29+
get invoker() {
30+
return (name: string, args: any, ctx: any, ab?: any) => {
31+
const fn = mockCatalog.functions.get(name);
32+
if (fn) return fn(args, ctx, ab);
33+
console.warn(`Function "${name}" not found in catalog`);
34+
return undefined;
35+
};
36+
}
2937
};
3038

3139
TestBed.configureTestingModule({
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { TestBed } from '@angular/core/testing';
18+
import { DataModel, SurfaceModel, DataContext } from '@a2ui/web_core/v0_9';
19+
import { MinimalCatalog } from '../catalog/minimal/minimal-catalog';
20+
import { toAngularSignal } from './utils';
21+
import { DestroyRef } from '@angular/core';
22+
23+
describe('Capitalize Function Binding', () => {
24+
let mockDestroyRef: jasmine.SpyObj<DestroyRef>;
25+
26+
beforeEach(() => {
27+
mockDestroyRef = jasmine.createSpyObj('DestroyRef', ['onDestroy']);
28+
mockDestroyRef.onDestroy.and.returnValue(() => {});
29+
});
30+
31+
it('should update output correctly when bound input updates using function call binding', () => {
32+
const catalog = new MinimalCatalog();
33+
34+
// Create Surface Model and DataContext
35+
const surface = new SurfaceModel('surface_1', catalog);
36+
const dataModel = surface.dataModel;
37+
const context = new DataContext(surface, '/');
38+
39+
const callValue = {
40+
call: 'capitalize',
41+
args: {
42+
value: {
43+
path: '/inputValue'
44+
}
45+
},
46+
returnType: 'string'
47+
};
48+
49+
// 1. Resolve Signal
50+
const resSig = context.resolveSignal<string>(callValue as any);
51+
52+
// 2. Convert to Angular Signal
53+
const angSig = toAngularSignal(resSig, mockDestroyRef);
54+
55+
// 3. Initial state
56+
expect(angSig()).toBe('');
57+
58+
// 4. Update data model Simulation typing
59+
dataModel.set('/inputValue', 'regression test');
60+
61+
// 5. Verify reactive updates
62+
expect(angSig()).toBe('Regression test');
63+
64+
// 6. Update again to confirm reactive stream remains healthy
65+
dataModel.set('/inputValue', 'another test');
66+
expect(angSig()).toBe('Another test');
67+
});
68+
});

renderers/angular/src/lib/v0_9/core/component-host.component.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ describe('ComponentHostComponent', () => {
4848
componentsModel: new Map([
4949
['comp1', { id: 'comp1', type: 'TestType', properties: { text: 'Hello' } }],
5050
]),
51+
catalog: { invoker: jasmine.createSpy('invoker').and.returnValue('result') },
5152
};
5253

5354
mockSurfaceGroup = {
@@ -64,7 +65,7 @@ describe('ComponentHostComponent', () => {
6465
};
6566

6667
mockBinder = jasmine.createSpyObj('ComponentBinder', ['bind']);
67-
mockBinder.bind.and.returnValue({ text: 'bound-hello' });
68+
mockBinder.bind.and.returnValue({ text: { value: () => 'bound-hello', onUpdate: () => {} } as any });
6869

6970
await TestBed.configureTestingModule({
7071
imports: [ComponentHostComponent],
@@ -92,7 +93,7 @@ describe('ComponentHostComponent', () => {
9293
// @ts-ignore - Accessing protected property
9394
expect(component.componentType).toBe(TestChildComponent);
9495
// @ts-ignore - Accessing protected property
95-
expect(component.props).toEqual({ text: 'bound-hello' });
96+
expect(component.props).toEqual({ text: jasmine.objectContaining({ value: jasmine.any(Function) }) as any });
9697

9798
expect(mockSurfaceGroup.getSurface).toHaveBeenCalledWith('surf1');
9899
expect(mockBinder.bind).toHaveBeenCalled();

renderers/angular/src/lib/v0_9/core/utils.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616

1717
import { DestroyRef, Signal, signal as angularSignal } from '@angular/core';
18-
import { Signal as PreactSignal, effect } from '@preact/signals-core';
18+
import { Signal as PreactSignal, effect } from '@a2ui/web_core/v0_9';
1919

2020
/**
2121
* Bridges a Preact Signal to an Angular Signal.
@@ -27,16 +27,14 @@ import { Signal as PreactSignal, effect } from '@preact/signals-core';
2727
*/
2828
import { NgZone } from '@angular/core';
2929

30-
const preactEffect = (window as any).webCoreSignals?.effect || effect;
31-
3230
export function toAngularSignal<T>(
3331
preactSignal: PreactSignal<T>,
3432
destroyRef: DestroyRef,
3533
ngZone?: NgZone,
3634
): Signal<T> {
3735
const s = angularSignal(preactSignal.peek());
3836

39-
const dispose = preactEffect(() => {
37+
const dispose = effect(() => {
4038
if (ngZone) {
4139
ngZone.run(() => s.set(preactSignal.value));
4240
} else {

renderers/angular/verify_capitalization.js

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

0 commit comments

Comments
 (0)