Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
c21229c
fix: resolve Angular build break caused by @a2ui/web_core protocol up…
gspencergoog Mar 20, 2026
cb4aa55
refactor(angular): implement native Angular signals and unify propert…
gspencergoog Mar 20, 2026
a2759f7
Merge branch 'main' into angular_updates
gspencergoog Mar 20, 2026
6a8ee17
fix(angular): make signal wrapper properties configurable for data bi…
gspencergoog Mar 20, 2026
b2292f5
chore(angular): log warning if signal name property definition fails
gspencergoog Mar 20, 2026
0b425ff
refactor: implement engine-agnostic reactive provider in web_core v0.9
gspencergoog Mar 20, 2026
8d77f51
Merge branch 'main' into angular_updates
gspencergoog Mar 20, 2026
3791120
fix(angular): resolve reactive update regression in v0.9
gspencergoog Mar 20, 2026
a906072
refactor(angular): update BoundProperty.value to return T instead of …
gspencergoog Mar 20, 2026
8f24020
fix(demo): restore explicit Zod schemas for Card and CustomSlider
gspencergoog Mar 20, 2026
59cdd76
test(angular): restore specific dispatchAction assertion in button spec
gspencergoog Mar 20, 2026
52cdc5e
Add missing license
gspencergoog Mar 20, 2026
8b8f8ed
test(angular): centralize testing boilerplate across basic catalog sp…
gspencergoog Mar 20, 2026
8f9da0a
refactor(angular): align BoundProperty type and implementation
gspencergoog Mar 20, 2026
a20ab01
refactor(angular): align BoundProperty implementation with type and a…
gspencergoog Mar 20, 2026
47a03a5
test(angular): refine createBoundProperty mock by removing update method
gspencergoog Mar 20, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import { Injectable } from '@angular/core';
import { A2uiRendererService } from '@a2ui/angular/v0_9';

import { A2uiClientAction, A2uiMessage } from '@a2ui/web_core/v0_9';
import { A2uiClientAction, A2uiMessage, CreateSurfaceMessage } from '@a2ui/web_core/v0_9';
import { ActionDispatcher } from './action-dispatcher.service';

/**
Expand Down
4 changes: 2 additions & 2 deletions renderers/angular/a2ui_explorer/src/app/card.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ import { BoundProperty } from '@a2ui/angular/v0_9';
style="border: 1px solid #ccc; padding: 16px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 16px; background-color: white;"
>
<a2ui-v09-component-host
*ngIf="props['child']?.value()"
[componentId]="props['child'].value()"
*ngIf="props['child']?.()"
[componentId]="props['child']()!"
[surfaceId]="surfaceId"
>
</a2ui-v09-component-host>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ import { BoundProperty } from '@a2ui/angular/v0_9';
imports: [CommonModule],
template: `
<div class="custom-slider-container">
<label>{{ props['label']?.value() || 'Value' }}: {{ props['value']?.value() }}</label>
<label>{{ props['label']?.() || 'Value' }}: {{ props['value']?.() }}</label>
<input
type="range"
[min]="props['min']?.value() || 0"
[max]="props['max']?.value() || 100"
[value]="props['value']?.value() || 0"
[min]="props['min']?.() || 0"
[max]="props['max']?.() || 100"
[value]="props['value']?.() || 0"
(input)="handleInput($event)"
/>
</div>
Expand All @@ -57,6 +57,6 @@ export class CustomSliderComponent {

handleInput(event: Event) {
const val = Number((event.target as HTMLInputElement).value);
this.props['value']?.onUpdate(val);
this.props['value']?.set(val);
}
}
15 changes: 7 additions & 8 deletions renderers/angular/a2ui_explorer/src/app/demo-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ import { createFunctionImplementation, FunctionImplementation } from '@a2ui/web_
})
export class DemoCatalog extends BasicCatalogBase {
constructor() {
const cardApi: AngularComponentImplementation = {
name: 'Card',
schema: z.object({
child: z.string().optional(),
}) as any,
component: CardComponent,
};
const customSliderApi: AngularComponentImplementation = {
name: 'CustomSlider',
schema: z.object({
Expand All @@ -41,14 +48,6 @@ export class DemoCatalog extends BasicCatalogBase {
component: CustomSliderComponent,
};

const cardApi: AngularComponentImplementation = {
name: 'Card',
schema: z.object({
child: z.string().optional(),
}) as any,
component: CardComponent,
};

const capitalizeImplementation: FunctionImplementation = createFunctionImplementation(
{
name: 'capitalize',
Expand Down
5 changes: 3 additions & 2 deletions renderers/angular/a2ui_explorer/src/app/demo.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ import { CommonModule } from '@angular/common';
import { A2uiRendererService, A2UI_RENDERER_CONFIG } from '@a2ui/angular/v0_9';
import { AgentStubService } from './agent-stub.service';
import { SurfaceComponent } from '@a2ui/angular/v0_9';
import { AngularCatalog } from '@a2ui/angular/v0_9';
import { AngularCatalog, AngularComponentImplementation } from '@a2ui/angular/v0_9';
import { createFunctionImplementation, FunctionImplementation } from '@a2ui/web_core/v0_9';
import { DemoCatalog } from './demo-catalog';
import { A2uiClientAction, CreateSurfaceMessage } from '@a2ui/web_core/v0_9';
import { A2uiClientAction, A2uiMessage, CreateSurfaceMessage } from '@a2ui/web_core/v0_9';
import { EXAMPLES } from './examples-bundle';
import { Example } from './types';
import { ActionDispatcher } from './action-dispatcher.service';
Expand Down
12 changes: 2 additions & 10 deletions renderers/angular/src/v0_8/components/modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,15 @@ import { Types } from '../types';
template: `
<div class="a2ui-modal-entry-point" (click)="openModal()">
@if (entryPointChild()) {
<ng-container
a2ui-renderer
[surfaceId]="surfaceId()!"
[component]="entryPointChild()!"
/>
<ng-container a2ui-renderer [surfaceId]="surfaceId()!" [component]="entryPointChild()!" />
}
</div>

@if (isOpen()) {
<div [class]="theme.components.Modal.backdrop" (click)="closeModal()">
<div [class]="theme.components.Modal.element" (click)="$event.stopPropagation()">
@if (contentChild()) {
<ng-container
a2ui-renderer
[surfaceId]="surfaceId()!"
[component]="contentChild()!"
/>
<ng-container a2ui-renderer [surfaceId]="surfaceId()!" [component]="contentChild()!" />
}
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,6 @@ export class AudioPlayerComponent {
componentId = input<string>();
dataContextPath = input<string>();

description = computed(() => this.props()['description']?.value());
url = computed(() => this.props()['url']?.value());
description = computed(() => this.props()['description']?.());
url = computed(() => this.props()['url']?.());
}
59 changes: 14 additions & 45 deletions renderers/angular/src/v0_9/catalog/basic/button.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { ButtonComponent } from './button.component';
import { A2uiRendererService } from '../../core/a2ui-renderer.service';
import { ComponentBinder } from '../../core/component-binder.service';
import { By } from '@angular/platform-browser';
import { BoundProperty } from '../../core/types';
import { createBoundProperty, StubComponent, createMockA2uiRendererService, createMockComponentBinder } from '../../testing';

describe('ButtonComponent', () => {
let component: ButtonComponent;
Expand All @@ -35,40 +37,14 @@ describe('ButtonComponent', () => {
['child1', { id: 'child1', type: 'Text', properties: { text: 'Child Content' } }],
]),
catalog: {
id: 'test-catalog',
components: new Map([
[
'Text',
{
component: (() => {
@Component({
standalone: true,
selector: 'dummy-text',
template: 'Dummy Text',
})
class DummyText {
props = input<any>();
surfaceId = input<string>();
componentId = input<string>();
dataContextPath = input<string>();
}
return DummyText;
})(),
},
],
]),
id: 'mock-catalog',
components: new Map([['Text', { type: 'Text', component: StubComponent }]]),
},
};

mockSurfaceGroup = {
getSurface: jasmine.createSpy('getSurface').and.returnValue(mockSurface),
};

mockRendererService = {
surfaceGroup: mockSurfaceGroup,
};

const mockBinder = jasmine.createSpyObj('ComponentBinder', ['bind']);
mockRendererService = createMockA2uiRendererService(mockSurface);
mockSurfaceGroup = mockRendererService.surfaceGroup;
const mockBinder = createMockComponentBinder();
mockBinder.bind.and.returnValue({ text: { value: () => 'bound text' } });

await TestBed.configureTestingModule({
Expand All @@ -84,14 +60,11 @@ describe('ButtonComponent', () => {
fixture.componentRef.setInput('surfaceId', 'surf1');
fixture.componentRef.setInput('componentId', 'comp1');
fixture.componentRef.setInput('props', {
variant: { value: signal('primary'), raw: 'primary', onUpdate: () => {} },
child: { value: signal('child1'), raw: 'child1', onUpdate: () => {} },
action: {
value: signal({ type: 'test-action', data: {} }),
raw: { type: 'test-action', data: {} },
onUpdate: () => {},
},
variant: createBoundProperty('primary'),
child: createBoundProperty('child1'),
action: createBoundProperty({ type: 'test-action', data: {} }),
});
fixture.componentRef.setInput('componentId', 'test-btn');
});

it('should create', () => {
Expand All @@ -108,11 +81,7 @@ describe('ButtonComponent', () => {
it('should set button type to button for non-primary variant', () => {
fixture.componentRef.setInput('props', {
...component.props(),
variant: {
value: signal('secondary'),
raw: 'secondary',
onUpdate: () => {},
},
variant: createBoundProperty('secondary'),
});
fixture.detectChanges();
const button = fixture.debugElement.query(By.css('button'));
Expand All @@ -131,7 +100,7 @@ describe('ButtonComponent', () => {
button.triggerEventHandler('click', null);

expect(mockSurfaceGroup.getSurface).toHaveBeenCalledWith('surf1');
expect(mockSurface.dispatchAction).toHaveBeenCalledWith(jasmine.any(Object), 'comp1');
expect(mockSurface.dispatchAction).toHaveBeenCalledWith(jasmine.any(Object), 'test-btn');
});

it('should show child component host if child prop is present', () => {
Expand All @@ -144,7 +113,7 @@ describe('ButtonComponent', () => {
it('should not show child component host if child prop is absent', () => {
fixture.componentRef.setInput('props', {
...component.props(),
child: { value: signal(null), raw: null, onUpdate: () => {} },
child: createBoundProperty(null),
});
fixture.detectChanges();
const host = fixture.debugElement.query(By.css('a2ui-v09-component-host'));
Expand Down
6 changes: 3 additions & 3 deletions renderers/angular/src/v0_9/catalog/basic/button.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,9 @@ export class ButtonComponent {

private rendererService = inject(A2uiRendererService);

variant = computed(() => this.props()['variant']?.value() ?? 'default');
child = computed(() => this.props()['child']?.value());
action = computed(() => this.props()['action']?.value());
variant = computed(() => this.props()['variant']?.() ?? 'default');
child = computed(() => this.props()['child']?.());
action = computed(() => this.props()['action']?.());

handleClick() {
const action = this.action();
Expand Down
4 changes: 2 additions & 2 deletions renderers/angular/src/v0_9/catalog/basic/card.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ export class CardComponent {
*/
props = input<Record<string, BoundProperty>>({});
surfaceId = input.required<string>();
componentId = input<string>();
componentId = input.required<string>();
dataContextPath = input<string>('/');

child = computed(() => this.props()['child']?.value());
child = computed(() => this.props()['child']?.());
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,16 @@ export class CheckBoxComponent {
*/
props = input<Record<string, BoundProperty>>({});
surfaceId = input.required<string>();
componentId = input<string>();
componentId = input.required<string>();
dataContextPath = input<string>('/');

private rendererService = inject(A2uiRendererService);

value = computed(() => this.props()['value']?.value() === true);
label = computed(() => this.props()['label']?.value());
value = computed(() => this.props()['value']?.() === true);
label = computed(() => this.props()['label']?.());

handleChange(event: Event) {
const checked = (event.target as HTMLInputElement).checked;
this.props()['value']?.onUpdate(checked);
this.props()['value']?.set(checked);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -122,12 +122,10 @@ export class ChoicePickerComponent {

private rendererService = inject(A2uiRendererService);

displayStyle = computed(() => this.props()['displayStyle']?.value());
choices = computed(
() => this.props()['choices']?.value() || this.props()['options']?.value() || [],
);
variant = computed(() => this.props()['variant']?.value());
selectedValue = computed(() => this.props()['value']?.value());
displayStyle = computed(() => this.props()['displayStyle']?.());
choices = computed(() => this.props()['choices']?.() || this.props()['options']?.() || []);
variant = computed(() => this.props()['variant']?.());
selectedValue = computed(() => this.props()['value']?.());

isMultiple(): boolean {
return this.variant() === 'multipleSelection';
Expand Down Expand Up @@ -160,10 +158,10 @@ export class ChoicePickerComponent {
} else {
next = next.filter((v: any) => v !== value);
}
this.props()['value']?.onUpdate(next);
this.props()['value']?.set(next);
} else {
if (active) {
this.props()['value']?.onUpdate(value);
this.props()['value']?.set(value);
}
}
}
Expand Down
48 changes: 12 additions & 36 deletions renderers/angular/src/v0_9/catalog/basic/column.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,12 @@
*/

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Component, input, signal } from '@angular/core';
import { ColumnComponent } from './column.component';
import { A2uiRendererService } from '../../core/a2ui-renderer.service';
import { ComponentBinder } from '../../core/component-binder.service';
import { By } from '@angular/platform-browser';
import { createBoundProperty, StubComponent, createMockA2uiRendererService, createMockComponentBinder } from '../../testing';

@Component({
standalone: true,
selector: 'dummy-child',
template: 'Dummy Child',
})
class DummyChild {
props = input<any>();
surfaceId = input<string>();
componentId = input<string>();
dataContextPath = input<string>();
}

describe('ColumnComponent', () => {
let component: ColumnComponent;
Expand All @@ -50,19 +39,14 @@ describe('ColumnComponent', () => {
]),
catalog: {
id: 'test-catalog',
components: new Map([['Child', { component: DummyChild }]]),
components: new Map([['Child', { component: StubComponent }]]),
},
};

mockSurfaceGroup = {
getSurface: jasmine.createSpy('getSurface').and.returnValue(mockSurface),
};

mockRendererService = {
surfaceGroup: mockSurfaceGroup,
};
mockRendererService = createMockA2uiRendererService(mockSurface);
mockSurfaceGroup = mockRendererService.surfaceGroup;

mockBinder = jasmine.createSpyObj('ComponentBinder', ['bind']);
mockBinder = createMockComponentBinder();
mockBinder.bind.and.returnValue({ text: { value: () => 'bound' } });

await TestBed.configureTestingModule({
Expand All @@ -77,13 +61,9 @@ describe('ColumnComponent', () => {
component = fixture.componentInstance;
fixture.componentRef.setInput('surfaceId', 'surf1');
fixture.componentRef.setInput('props', {
justify: { value: signal('start'), raw: 'start', onUpdate: () => {} },
align: { value: signal('stretch'), raw: 'stretch', onUpdate: () => {} },
children: {
value: signal(['child1', 'child2']),
raw: ['child1', 'child2'],
onUpdate: () => {},
},
justify: createBoundProperty('start'),
align: createBoundProperty('stretch'),
children: createBoundProperty(['child1', 'child2']),
});
});

Expand Down Expand Up @@ -111,14 +91,10 @@ describe('ColumnComponent', () => {
it('should render repeating children', () => {
fixture.componentRef.setInput('props', {
...component.props(),
children: {
value: signal([{}, {}]),
raw: {
componentId: 'template1',
path: 'items',
},
onUpdate: () => {},
},
children: createBoundProperty([{}, {}], {
componentId: 'template1',
path: 'items',
}),
});
fixture.detectChanges();

Expand Down
Loading
Loading