refactor(vendor): align a2ui renderer typings

main
Peter Steinberger 2026-01-14 01:11:12 +00:00
parent acfa762617
commit b80abf8dd1
3 changed files with 0 additions and 140 deletions

View File

@ -1,25 +1,20 @@
/* /*
Copyright 2025 Google LLC Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0 https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
export { export {
type ClientToServerMessage as A2UIClientEventMessage, type ClientToServerMessage as A2UIClientEventMessage,
type ClientCapabilitiesDynamic, type ClientCapabilitiesDynamic,
} from "./client-event.js"; } from "./client-event.js";
export { type Action } from "./components.js"; export { type Action } from "./components.js";
import { import {
AudioPlayer, AudioPlayer,
Button, Button,
@ -35,12 +30,10 @@ import {
Video, Video,
} from "./components"; } from "./components";
import { StringValue } from "./primitives"; import { StringValue } from "./primitives";
export type MessageProcessor = { export type MessageProcessor = {
getSurfaces(): ReadonlyMap<string, Surface>; getSurfaces(): ReadonlyMap<string, Surface>;
clearSurfaces(): void; clearSurfaces(): void;
processMessages(messages: ServerToClientMessage[]): void; processMessages(messages: ServerToClientMessage[]): void;
/** /**
* Retrieves the data for a given component node and a relative path string. * Retrieves the data for a given component node and a relative path string.
* This correctly handles the special `.` path, which refers to the node's * This correctly handles the special `.` path, which refers to the node's
@ -51,17 +44,14 @@ export type MessageProcessor = {
relativePath: string, relativePath: string,
surfaceId: string surfaceId: string
): DataValue | null; ): DataValue | null;
setData( setData(
node: AnyComponentNode | null, node: AnyComponentNode | null,
relativePath: string, relativePath: string,
value: DataValue, value: DataValue,
surfaceId: string surfaceId: string
): void; ): void;
resolvePath(path: string, dataContextPath?: string): string; resolvePath(path: string, dataContextPath?: string): string;
}; };
export type Theme = { export type Theme = {
components: { components: {
AudioPlayer: Record<string, boolean>; AudioPlayer: Record<string, boolean>;
@ -193,7 +183,6 @@ export type Theme = {
Video?: Record<string, string>; Video?: Record<string, string>;
}; };
}; };
/** /**
* Represents a user-initiated action, sent from the client to the server. * Represents a user-initiated action, sent from the client to the server.
*/ */
@ -219,7 +208,6 @@ export interface UserAction {
[k: string]: unknown; [k: string]: unknown;
}; };
} }
/** A recursive type for any valid JSON-like value in the data model. */ /** A recursive type for any valid JSON-like value in the data model. */
export type DataValue = export type DataValue =
| string | string
@ -232,19 +220,16 @@ export type DataValue =
export type DataObject = { [key: string]: DataValue }; export type DataObject = { [key: string]: DataValue };
export type DataMap = Map<string, DataValue>; export type DataMap = Map<string, DataValue>;
export type DataArray = DataValue[]; export type DataArray = DataValue[];
/** A template for creating components from a list in the data model. */ /** A template for creating components from a list in the data model. */
export interface ComponentArrayTemplate { export interface ComponentArrayTemplate {
componentId: string; componentId: string;
dataBinding: string; dataBinding: string;
} }
/** Defines a list of child components, either explicitly or via a template. */ /** Defines a list of child components, either explicitly or via a template. */
export interface ComponentArrayReference { export interface ComponentArrayReference {
explicitList?: string[]; explicitList?: string[];
template?: ComponentArrayTemplate; template?: ComponentArrayTemplate;
} }
/** Represents the general shape of a component's properties. */ /** Represents the general shape of a component's properties. */
export type ComponentProperties = { export type ComponentProperties = {
// Allow any property, but define known structural ones for type safety. // Allow any property, but define known structural ones for type safety.
@ -252,31 +237,26 @@ export type ComponentProperties = {
child?: string; child?: string;
[k: string]: unknown; [k: string]: unknown;
}; };
/** A raw component instance from a SurfaceUpdate message. */ /** A raw component instance from a SurfaceUpdate message. */
export interface ComponentInstance { export interface ComponentInstance {
id: string; id: string;
weight?: number; weight?: number;
component?: ComponentProperties; component?: ComponentProperties;
} }
export interface BeginRenderingMessage { export interface BeginRenderingMessage {
surfaceId: string; surfaceId: string;
root: string; root: string;
styles?: Record<string, string>; styles?: Record<string, string>;
} }
export interface SurfaceUpdateMessage { export interface SurfaceUpdateMessage {
surfaceId: string; surfaceId: string;
components: ComponentInstance[]; components: ComponentInstance[];
} }
export interface DataModelUpdate { export interface DataModelUpdate {
surfaceId: string; surfaceId: string;
path?: string; path?: string;
contents: ValueMap[]; contents: ValueMap[];
} }
// ValueMap is a type of DataObject for passing to the data model. // ValueMap is a type of DataObject for passing to the data model.
export type ValueMap = DataObject & { export type ValueMap = DataObject & {
key: string; key: string;
@ -286,18 +266,15 @@ export type ValueMap = DataObject & {
valueBoolean?: boolean; valueBoolean?: boolean;
valueMap?: ValueMap[]; valueMap?: ValueMap[];
}; };
export interface DeleteSurfaceMessage { export interface DeleteSurfaceMessage {
surfaceId: string; surfaceId: string;
} }
export interface ServerToClientMessage { export interface ServerToClientMessage {
beginRendering?: BeginRenderingMessage; beginRendering?: BeginRenderingMessage;
surfaceUpdate?: SurfaceUpdateMessage; surfaceUpdate?: SurfaceUpdateMessage;
dataModelUpdate?: DataModelUpdate; dataModelUpdate?: DataModelUpdate;
deleteSurface?: DeleteSurfaceMessage; deleteSurface?: DeleteSurfaceMessage;
} }
/** /**
* A recursive type for any value that can appear within a resolved component * A recursive type for any value that can appear within a resolved component
* tree. This is the main type that makes the recursive resolution possible. * tree. This is the main type that makes the recursive resolution possible.
@ -310,13 +287,10 @@ export type ResolvedValue =
| AnyComponentNode | AnyComponentNode
| ResolvedMap | ResolvedMap
| ResolvedArray; | ResolvedArray;
/** A generic map where each value has been recursively resolved. */ /** A generic map where each value has been recursively resolved. */
export type ResolvedMap = { [key: string]: ResolvedValue }; export type ResolvedMap = { [key: string]: ResolvedValue };
/** A generic array where each item has been recursively resolved. */ /** A generic array where each item has been recursively resolved. */
export type ResolvedArray = ResolvedValue[]; export type ResolvedArray = ResolvedValue[];
/** /**
* A base interface that all component nodes share. * A base interface that all component nodes share.
*/ */
@ -326,103 +300,83 @@ interface BaseComponentNode {
dataContextPath?: string; dataContextPath?: string;
slotName?: string; slotName?: string;
} }
export interface TextNode extends BaseComponentNode { export interface TextNode extends BaseComponentNode {
type: "Text"; type: "Text";
properties: ResolvedText; properties: ResolvedText;
} }
export interface ImageNode extends BaseComponentNode { export interface ImageNode extends BaseComponentNode {
type: "Image"; type: "Image";
properties: ResolvedImage; properties: ResolvedImage;
} }
export interface IconNode extends BaseComponentNode { export interface IconNode extends BaseComponentNode {
type: "Icon"; type: "Icon";
properties: ResolvedIcon; properties: ResolvedIcon;
} }
export interface VideoNode extends BaseComponentNode { export interface VideoNode extends BaseComponentNode {
type: "Video"; type: "Video";
properties: ResolvedVideo; properties: ResolvedVideo;
} }
export interface AudioPlayerNode extends BaseComponentNode { export interface AudioPlayerNode extends BaseComponentNode {
type: "AudioPlayer"; type: "AudioPlayer";
properties: ResolvedAudioPlayer; properties: ResolvedAudioPlayer;
} }
export interface RowNode extends BaseComponentNode { export interface RowNode extends BaseComponentNode {
type: "Row"; type: "Row";
properties: ResolvedRow; properties: ResolvedRow;
} }
export interface ColumnNode extends BaseComponentNode { export interface ColumnNode extends BaseComponentNode {
type: "Column"; type: "Column";
properties: ResolvedColumn; properties: ResolvedColumn;
} }
export interface ListNode extends BaseComponentNode { export interface ListNode extends BaseComponentNode {
type: "List"; type: "List";
properties: ResolvedList; properties: ResolvedList;
} }
export interface CardNode extends BaseComponentNode { export interface CardNode extends BaseComponentNode {
type: "Card"; type: "Card";
properties: ResolvedCard; properties: ResolvedCard;
} }
export interface TabsNode extends BaseComponentNode { export interface TabsNode extends BaseComponentNode {
type: "Tabs"; type: "Tabs";
properties: ResolvedTabs; properties: ResolvedTabs;
} }
export interface DividerNode extends BaseComponentNode { export interface DividerNode extends BaseComponentNode {
type: "Divider"; type: "Divider";
properties: ResolvedDivider; properties: ResolvedDivider;
} }
export interface ModalNode extends BaseComponentNode { export interface ModalNode extends BaseComponentNode {
type: "Modal"; type: "Modal";
properties: ResolvedModal; properties: ResolvedModal;
} }
export interface ButtonNode extends BaseComponentNode { export interface ButtonNode extends BaseComponentNode {
type: "Button"; type: "Button";
properties: ResolvedButton; properties: ResolvedButton;
} }
export interface CheckboxNode extends BaseComponentNode { export interface CheckboxNode extends BaseComponentNode {
type: "CheckBox"; type: "CheckBox";
properties: ResolvedCheckbox; properties: ResolvedCheckbox;
} }
export interface TextFieldNode extends BaseComponentNode { export interface TextFieldNode extends BaseComponentNode {
type: "TextField"; type: "TextField";
properties: ResolvedTextField; properties: ResolvedTextField;
} }
export interface DateTimeInputNode extends BaseComponentNode { export interface DateTimeInputNode extends BaseComponentNode {
type: "DateTimeInput"; type: "DateTimeInput";
properties: ResolvedDateTimeInput; properties: ResolvedDateTimeInput;
} }
export interface MultipleChoiceNode extends BaseComponentNode { export interface MultipleChoiceNode extends BaseComponentNode {
type: "MultipleChoice"; type: "MultipleChoice";
properties: ResolvedMultipleChoice; properties: ResolvedMultipleChoice;
} }
export interface SliderNode extends BaseComponentNode { export interface SliderNode extends BaseComponentNode {
type: "Slider"; type: "Slider";
properties: ResolvedSlider; properties: ResolvedSlider;
} }
export interface CustomNode extends BaseComponentNode { export interface CustomNode extends BaseComponentNode {
type: string; type: string;
// For custom nodes, properties are just a map of string keys to any resolved value. // For custom nodes, properties are just a map of string keys to any resolved value.
properties: CustomNodeProperties; properties: CustomNodeProperties;
} }
/** /**
* The complete discriminated union of all possible resolved component nodes. * The complete discriminated union of all possible resolved component nodes.
* A renderer would use this type for any given node in the component tree. * A renderer would use this type for any given node in the component tree.
@ -447,7 +401,6 @@ export type AnyComponentNode =
| MultipleChoiceNode | MultipleChoiceNode
| SliderNode | SliderNode
| CustomNode; | CustomNode;
// These components do not contain other components can reuse their // These components do not contain other components can reuse their
// original interfaces. // original interfaces.
export type ResolvedText = Text; export type ResolvedText = Text;
@ -461,7 +414,6 @@ export type ResolvedTextField = TextField;
export type ResolvedDateTimeInput = DateTimeInput; export type ResolvedDateTimeInput = DateTimeInput;
export type ResolvedMultipleChoice = MultipleChoice; export type ResolvedMultipleChoice = MultipleChoice;
export type ResolvedSlider = Slider; export type ResolvedSlider = Slider;
export interface ResolvedRow { export interface ResolvedRow {
children: AnyComponentNode[]; children: AnyComponentNode[];
distribution?: distribution?:
@ -473,7 +425,6 @@ export interface ResolvedRow {
| "spaceEvenly"; | "spaceEvenly";
alignment?: "start" | "center" | "end" | "stretch"; alignment?: "start" | "center" | "end" | "stretch";
} }
export interface ResolvedColumn { export interface ResolvedColumn {
children: AnyComponentNode[]; children: AnyComponentNode[];
distribution?: distribution?:
@ -485,43 +436,34 @@ export interface ResolvedColumn {
| "spaceEvenly"; | "spaceEvenly";
alignment?: "start" | "center" | "end" | "stretch"; alignment?: "start" | "center" | "end" | "stretch";
} }
export interface ResolvedButton { export interface ResolvedButton {
child: AnyComponentNode; child: AnyComponentNode;
action: Button["action"]; action: Button["action"];
} }
export interface ResolvedList { export interface ResolvedList {
children: AnyComponentNode[]; children: AnyComponentNode[];
direction?: "vertical" | "horizontal"; direction?: "vertical" | "horizontal";
alignment?: "start" | "center" | "end" | "stretch"; alignment?: "start" | "center" | "end" | "stretch";
} }
export interface ResolvedCard { export interface ResolvedCard {
child: AnyComponentNode; child: AnyComponentNode;
children: AnyComponentNode[]; children: AnyComponentNode[];
} }
export interface ResolvedTabItem { export interface ResolvedTabItem {
title: StringValue; title: StringValue;
child: AnyComponentNode; child: AnyComponentNode;
} }
export interface ResolvedTabs { export interface ResolvedTabs {
tabItems: ResolvedTabItem[]; tabItems: ResolvedTabItem[];
} }
export interface ResolvedModal { export interface ResolvedModal {
entryPointChild: AnyComponentNode; entryPointChild: AnyComponentNode;
contentChild: AnyComponentNode; contentChild: AnyComponentNode;
} }
export interface CustomNodeProperties { export interface CustomNodeProperties {
[k: string]: ResolvedValue; [k: string]: ResolvedValue;
} }
export type SurfaceID = string; export type SurfaceID = string;
/** The complete state of a single UI surface. */ /** The complete state of a single UI surface. */
export interface Surface { export interface Surface {
rootComponentId: string | null; rootComponentId: string | null;

View File

@ -1,19 +1,15 @@
/* /*
Copyright 2025 Google LLC Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0 https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { SignalWatcher } from "@lit-labs/signals"; import { SignalWatcher } from "@lit-labs/signals";
import { consume } from "@lit/context"; import { consume } from "@lit/context";
import { import {
@ -34,48 +30,36 @@ import { Theme, AnyComponentNode, SurfaceID } from "../types/types.js";
import { themeContext } from "./context/theme.js"; import { themeContext } from "./context/theme.js";
import { structuralStyles } from "./styles.js"; import { structuralStyles } from "./styles.js";
import { componentRegistry } from "./component-registry.js"; import { componentRegistry } from "./component-registry.js";
type NodeOfType<T extends AnyComponentNode["type"]> = Extract< type NodeOfType<T extends AnyComponentNode["type"]> = Extract<
AnyComponentNode, AnyComponentNode,
{ type: T } { type: T }
>; >;
// This is the base class all the components will inherit // This is the base class all the components will inherit
@customElement("a2ui-root") @customElement("a2ui-root")
export class Root extends (SignalWatcher(LitElement) as typeof LitElement) { export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
@property() @property()
accessor surfaceId: SurfaceID | null = null; accessor surfaceId: SurfaceID | null = null;
@property() @property()
accessor component: AnyComponentNode | null = null; accessor component: AnyComponentNode | null = null;
@consume({ context: themeContext }) @consume({ context: themeContext })
accessor theme!: Theme; accessor theme!: Theme;
@property({ attribute: false }) @property({ attribute: false })
accessor childComponents: AnyComponentNode[] | null = null; accessor childComponents: AnyComponentNode[] | null = null;
@property({ attribute: false }) @property({ attribute: false })
accessor processor: A2uiMessageProcessor | null = null; accessor processor: A2uiMessageProcessor | null = null;
@property() @property()
accessor dataContextPath: string = ""; accessor dataContextPath: string = "";
@property() @property()
accessor enableCustomElements = false; accessor enableCustomElements = false;
@property() @property()
set weight(weight: string | number) { set weight(weight: string | number) {
this.#weight = weight; this.#weight = weight;
this.style.setProperty("--weight", `${weight}`); this.style.setProperty("--weight", `${weight}`);
} }
get weight() { get weight() {
return this.#weight; return this.#weight;
} }
#weight: string | number = 1; #weight: string | number = 1;
static styles = [ static styles = [
structuralStyles, structuralStyles,
css` css`
@ -87,44 +71,36 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
} }
`, `,
]; ];
/** /**
* Holds the cleanup function for our effect. * Holds the cleanup function for our effect.
* We need this to stop the effect when the component is disconnected. * We need this to stop the effect when the component is disconnected.
*/ */
#lightDomEffectDisposer: null | (() => void) = null; #lightDomEffectDisposer: null | (() => void) = null;
protected willUpdate(changedProperties: PropertyValues<this>): void { protected willUpdate(changedProperties: PropertyValues<this>): void {
if (changedProperties.has("childComponents")) { if (changedProperties.has("childComponents")) {
if (this.#lightDomEffectDisposer) { if (this.#lightDomEffectDisposer) {
this.#lightDomEffectDisposer(); this.#lightDomEffectDisposer();
} }
// This effect watches the A2UI Children signal and updates the Light DOM. // This effect watches the A2UI Children signal and updates the Light DOM.
this.#lightDomEffectDisposer = effect(() => { this.#lightDomEffectDisposer = effect(() => {
// 1. Read the signal to create the subscription. // 1. Read the signal to create the subscription.
const allChildren = this.childComponents ?? null; const allChildren = this.childComponents ?? null;
// 2. Generate the template for the children. // 2. Generate the template for the children.
const lightDomTemplate = this.renderComponentTree(allChildren); const lightDomTemplate = this.renderComponentTree(allChildren);
// 3. Imperatively render that template into the component itself. // 3. Imperatively render that template into the component itself.
render(lightDomTemplate, this, { host: this }); render(lightDomTemplate, this, { host: this });
}); });
} }
} }
/** /**
* Clean up the effect when the component is removed from the DOM. * Clean up the effect when the component is removed from the DOM.
*/ */
disconnectedCallback(): void { disconnectedCallback(): void {
super.disconnectedCallback(); super.disconnectedCallback();
if (this.#lightDomEffectDisposer) { if (this.#lightDomEffectDisposer) {
this.#lightDomEffectDisposer(); this.#lightDomEffectDisposer();
} }
} }
/** /**
* Turns the SignalMap into a renderable TemplateResult for Lit. * Turns the SignalMap into a renderable TemplateResult for Lit.
*/ */
@ -134,18 +110,15 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
if (!components) { if (!components) {
return nothing; return nothing;
} }
if (!Array.isArray(components)) { if (!Array.isArray(components)) {
return nothing; return nothing;
} }
return html` ${map(components, (component) => { return html` ${map(components, (component) => {
// 1. Check if there is a registered custom component or override. // 1. Check if there is a registered custom component or override.
if (this.enableCustomElements) { if (this.enableCustomElements) {
const registeredCtor = componentRegistry.get(component.type); const registeredCtor = componentRegistry.get(component.type);
// We also check customElements.get for non-registered but defined elements // We also check customElements.get for non-registered but defined elements
const elCtor = registeredCtor || customElements.get(component.type); const elCtor = registeredCtor || customElements.get(component.type);
if (elCtor) { if (elCtor) {
const node = component as AnyComponentNode; const node = component as AnyComponentNode;
const el = new elCtor() as Root; const el = new elCtor() as Root;
@ -158,7 +131,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
el.processor = this.processor; el.processor = this.processor;
el.surfaceId = this.surfaceId; el.surfaceId = this.surfaceId;
el.dataContextPath = node.dataContextPath ?? "/"; el.dataContextPath = node.dataContextPath ?? "/";
for (const [prop, val] of Object.entries(component.properties)) { for (const [prop, val] of Object.entries(component.properties)) {
// @ts-expect-error We're off the books. // @ts-expect-error We're off the books.
el[prop] = val; el[prop] = val;
@ -166,7 +138,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
return html`${el}`; return html`${el}`;
} }
} }
// 2. Fallback to standard components. // 2. Fallback to standard components.
switch (component.type) { switch (component.type) {
case "List": { case "List": {
@ -184,7 +155,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
.enableCustomElements=${this.enableCustomElements} .enableCustomElements=${this.enableCustomElements}
></a2ui-list>`; ></a2ui-list>`;
} }
case "Card": { case "Card": {
const node = component as NodeOfType<"Card">; const node = component as NodeOfType<"Card">;
let childComponents: AnyComponentNode[] | null = let childComponents: AnyComponentNode[] | null =
@ -192,7 +162,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
if (!childComponents && node.properties.child) { if (!childComponents && node.properties.child) {
childComponents = [node.properties.child]; childComponents = [node.properties.child];
} }
return html`<a2ui-card return html`<a2ui-card
id=${node.id} id=${node.id}
slot=${node.slotName ? node.slotName : nothing} slot=${node.slotName ? node.slotName : nothing}
@ -205,7 +174,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
.enableCustomElements=${this.enableCustomElements} .enableCustomElements=${this.enableCustomElements}
></a2ui-card>`; ></a2ui-card>`;
} }
case "Column": { case "Column": {
const node = component as NodeOfType<"Column">; const node = component as NodeOfType<"Column">;
return html`<a2ui-column return html`<a2ui-column
@ -222,7 +190,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
.enableCustomElements=${this.enableCustomElements} .enableCustomElements=${this.enableCustomElements}
></a2ui-column>`; ></a2ui-column>`;
} }
case "Row": { case "Row": {
const node = component as NodeOfType<"Row">; const node = component as NodeOfType<"Row">;
return html`<a2ui-row return html`<a2ui-row
@ -239,7 +206,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
.enableCustomElements=${this.enableCustomElements} .enableCustomElements=${this.enableCustomElements}
></a2ui-row>`; ></a2ui-row>`;
} }
case "Image": { case "Image": {
const node = component as NodeOfType<"Image">; const node = component as NodeOfType<"Image">;
return html`<a2ui-image return html`<a2ui-image
@ -256,7 +222,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
.enableCustomElements=${this.enableCustomElements} .enableCustomElements=${this.enableCustomElements}
></a2ui-image>`; ></a2ui-image>`;
} }
case "Icon": { case "Icon": {
const node = component as NodeOfType<"Icon">; const node = component as NodeOfType<"Icon">;
return html`<a2ui-icon return html`<a2ui-icon
@ -271,7 +236,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
.enableCustomElements=${this.enableCustomElements} .enableCustomElements=${this.enableCustomElements}
></a2ui-icon>`; ></a2ui-icon>`;
} }
case "AudioPlayer": { case "AudioPlayer": {
const node = component as NodeOfType<"AudioPlayer">; const node = component as NodeOfType<"AudioPlayer">;
return html`<a2ui-audioplayer return html`<a2ui-audioplayer
@ -286,7 +250,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
.enableCustomElements=${this.enableCustomElements} .enableCustomElements=${this.enableCustomElements}
></a2ui-audioplayer>`; ></a2ui-audioplayer>`;
} }
case "Button": { case "Button": {
const node = component as NodeOfType<"Button">; const node = component as NodeOfType<"Button">;
return html`<a2ui-button return html`<a2ui-button
@ -302,7 +265,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
.enableCustomElements=${this.enableCustomElements} .enableCustomElements=${this.enableCustomElements}
></a2ui-button>`; ></a2ui-button>`;
} }
case "Text": { case "Text": {
const node = component as NodeOfType<"Text">; const node = component as NodeOfType<"Text">;
return html`<a2ui-text return html`<a2ui-text
@ -319,7 +281,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
.enableCustomElements=${this.enableCustomElements} .enableCustomElements=${this.enableCustomElements}
></a2ui-text>`; ></a2ui-text>`;
} }
case "CheckBox": { case "CheckBox": {
const node = component as NodeOfType<"CheckBox">; const node = component as NodeOfType<"CheckBox">;
return html`<a2ui-checkbox return html`<a2ui-checkbox
@ -335,7 +296,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
.enableCustomElements=${this.enableCustomElements} .enableCustomElements=${this.enableCustomElements}
></a2ui-checkbox>`; ></a2ui-checkbox>`;
} }
case "DateTimeInput": { case "DateTimeInput": {
const node = component as NodeOfType<"DateTimeInput">; const node = component as NodeOfType<"DateTimeInput">;
return html`<a2ui-datetimeinput return html`<a2ui-datetimeinput
@ -353,7 +313,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
.enableCustomElements=${this.enableCustomElements} .enableCustomElements=${this.enableCustomElements}
></a2ui-datetimeinput>`; ></a2ui-datetimeinput>`;
} }
case "Divider": { case "Divider": {
// TODO: thickness, axis and color. // TODO: thickness, axis and color.
const node = component as NodeOfType<"Divider">; const node = component as NodeOfType<"Divider">;
@ -371,7 +330,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
.enableCustomElements=${this.enableCustomElements} .enableCustomElements=${this.enableCustomElements}
></a2ui-divider>`; ></a2ui-divider>`;
} }
case "MultipleChoice": { case "MultipleChoice": {
// TODO: maxAllowedSelections and selections. // TODO: maxAllowedSelections and selections.
const node = component as NodeOfType<"MultipleChoice">; const node = component as NodeOfType<"MultipleChoice">;
@ -389,7 +347,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
.enableCustomElements=${this.enableCustomElements} .enableCustomElements=${this.enableCustomElements}
></a2ui-multiplechoice>`; ></a2ui-multiplechoice>`;
} }
case "Slider": { case "Slider": {
const node = component as NodeOfType<"Slider">; const node = component as NodeOfType<"Slider">;
return html`<a2ui-slider return html`<a2ui-slider
@ -406,7 +363,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
.enableCustomElements=${this.enableCustomElements} .enableCustomElements=${this.enableCustomElements}
></a2ui-slider>`; ></a2ui-slider>`;
} }
case "TextField": { case "TextField": {
// TODO: type and validationRegexp. // TODO: type and validationRegexp.
const node = component as NodeOfType<"TextField">; const node = component as NodeOfType<"TextField">;
@ -425,7 +381,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
.enableCustomElements=${this.enableCustomElements} .enableCustomElements=${this.enableCustomElements}
></a2ui-textfield>`; ></a2ui-textfield>`;
} }
case "Video": { case "Video": {
const node = component as NodeOfType<"Video">; const node = component as NodeOfType<"Video">;
return html`<a2ui-video return html`<a2ui-video
@ -440,7 +395,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
.enableCustomElements=${this.enableCustomElements} .enableCustomElements=${this.enableCustomElements}
></a2ui-video>`; ></a2ui-video>`;
} }
case "Tabs": { case "Tabs": {
const node = component as NodeOfType<"Tabs">; const node = component as NodeOfType<"Tabs">;
const titles: StringValue[] = []; const titles: StringValue[] = [];
@ -451,7 +405,6 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
childComponents.push(item.child); childComponents.push(item.child);
} }
} }
return html`<a2ui-tabs return html`<a2ui-tabs
id=${node.id} id=${node.id}
slot=${node.slotName ? node.slotName : nothing} slot=${node.slotName ? node.slotName : nothing}
@ -465,16 +418,13 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
.enableCustomElements=${this.enableCustomElements} .enableCustomElements=${this.enableCustomElements}
></a2ui-tabs>`; ></a2ui-tabs>`;
} }
case "Modal": { case "Modal": {
const node = component as NodeOfType<"Modal">; const node = component as NodeOfType<"Modal">;
const childComponents: AnyComponentNode[] = [ const childComponents: AnyComponentNode[] = [
node.properties.entryPointChild, node.properties.entryPointChild,
node.properties.contentChild, node.properties.contentChild,
]; ];
node.properties.entryPointChild.slotName = "entry"; node.properties.entryPointChild.slotName = "entry";
return html`<a2ui-modal return html`<a2ui-modal
id=${node.id} id=${node.id}
slot=${node.slotName ? node.slotName : nothing} slot=${node.slotName ? node.slotName : nothing}
@ -487,27 +437,22 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
.enableCustomElements=${this.enableCustomElements} .enableCustomElements=${this.enableCustomElements}
></a2ui-modal>`; ></a2ui-modal>`;
} }
default: { default: {
return this.renderCustomComponent(component); return this.renderCustomComponent(component);
} }
} }
})}`; })}`;
} }
private renderCustomComponent(component: AnyComponentNode) { private renderCustomComponent(component: AnyComponentNode) {
if (!this.enableCustomElements) { if (!this.enableCustomElements) {
return; return;
} }
const node = component as AnyComponentNode; const node = component as AnyComponentNode;
const registeredCtor = componentRegistry.get(component.type); const registeredCtor = componentRegistry.get(component.type);
const elCtor = registeredCtor || customElements.get(component.type); const elCtor = registeredCtor || customElements.get(component.type);
if (!elCtor) { if (!elCtor) {
return html`Unknown element ${component.type}`; return html`Unknown element ${component.type}`;
} }
const el = new elCtor() as Root; const el = new elCtor() as Root;
el.id = node.id; el.id = node.id;
if (node.slotName) { if (node.slotName) {
@ -518,14 +463,12 @@ export class Root extends (SignalWatcher(LitElement) as typeof LitElement) {
el.processor = this.processor; el.processor = this.processor;
el.surfaceId = this.surfaceId; el.surfaceId = this.surfaceId;
el.dataContextPath = node.dataContextPath ?? "/"; el.dataContextPath = node.dataContextPath ?? "/";
for (const [prop, val] of Object.entries(component.properties)) { for (const [prop, val] of Object.entries(component.properties)) {
// @ts-expect-error We're off the books. // @ts-expect-error We're off the books.
el[prop] = val; el[prop] = val;
} }
return html`${el}`; return html`${el}`;
} }
render(): TemplateResult | typeof nothing { render(): TemplateResult | typeof nothing {
return html`<slot></slot>`; return html`<slot></slot>`;
} }

View File

@ -1,22 +1,17 @@
/* /*
Copyright 2025 Google LLC Copyright 2025 Google LLC
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0 https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { SurfaceUpdateSchemaMatcher } from "./surface_update_schema_matcher"; import { SurfaceUpdateSchemaMatcher } from "./surface_update_schema_matcher";
import { SchemaMatcher } from "./schema_matcher"; import { SchemaMatcher } from "./schema_matcher";
export function validateSchema( export function validateSchema(
data: any, data: any,
schemaName: string, schemaName: string,
@ -36,7 +31,6 @@ export function validateSchema(
"A2UI Protocol message must have one of: surfaceUpdate, dataModelUpdate, beginRendering, deleteSurface.", "A2UI Protocol message must have one of: surfaceUpdate, dataModelUpdate, beginRendering, deleteSurface.",
); );
} }
if (matchers) { if (matchers) {
for (const matcher of matchers) { for (const matcher of matchers) {
const result = matcher.validate(data); const result = matcher.validate(data);
@ -45,10 +39,8 @@ export function validateSchema(
} }
} }
} }
return errors; return errors;
} }
function validateDeleteSurface(data: any, errors: string[]) { function validateDeleteSurface(data: any, errors: string[]) {
if (data.surfaceId === undefined) { if (data.surfaceId === undefined) {
errors.push("DeleteSurface must have a 'surfaceId' property."); errors.push("DeleteSurface must have a 'surfaceId' property.");
@ -60,7 +52,6 @@ function validateDeleteSurface(data: any, errors: string[]) {
} }
} }
} }
function validateSurfaceUpdate(data: any, errors: string[]) { function validateSurfaceUpdate(data: any, errors: string[]) {
if (data.surfaceId === undefined) { if (data.surfaceId === undefined) {
errors.push("SurfaceUpdate must have a 'surfaceId' property."); errors.push("SurfaceUpdate must have a 'surfaceId' property.");
@ -69,7 +60,6 @@ function validateSurfaceUpdate(data: any, errors: string[]) {
errors.push("SurfaceUpdate must have a 'components' array."); errors.push("SurfaceUpdate must have a 'components' array.");
return; return;
} }
const componentIds = new Set<string>(); const componentIds = new Set<string>();
for (const c of data.components) { for (const c of data.components) {
if (c.id) { if (c.id) {
@ -79,29 +69,24 @@ function validateSurfaceUpdate(data: any, errors: string[]) {
componentIds.add(c.id); componentIds.add(c.id);
} }
} }
for (const component of data.components) { for (const component of data.components) {
validateComponent(component, componentIds, errors); validateComponent(component, componentIds, errors);
} }
} }
function validateDataModelUpdate(data: any, errors: string[]) { function validateDataModelUpdate(data: any, errors: string[]) {
if (data.surfaceId === undefined) { if (data.surfaceId === undefined) {
errors.push("DataModelUpdate must have a 'surfaceId' property."); errors.push("DataModelUpdate must have a 'surfaceId' property.");
} }
const allowedTopLevel = ["surfaceId", "path", "contents"]; const allowedTopLevel = ["surfaceId", "path", "contents"];
for (const key in data) { for (const key in data) {
if (!allowedTopLevel.includes(key)) { if (!allowedTopLevel.includes(key)) {
errors.push(`DataModelUpdate has unexpected property: ${key}`); errors.push(`DataModelUpdate has unexpected property: ${key}`);
} }
} }
if (!Array.isArray(data.contents)) { if (!Array.isArray(data.contents)) {
errors.push("DataModelUpdate must have a 'contents' array."); errors.push("DataModelUpdate must have a 'contents' array.");
return; return;
} }
const validateValueProperty = ( const validateValueProperty = (
item: any, item: any,
itemErrors: string[], itemErrors: string[],
@ -127,7 +112,6 @@ function validateDataModelUpdate(data: any, errors: string[]) {
); );
return; return;
} }
if (foundValueProp === "valueMap") { if (foundValueProp === "valueMap") {
if (!Array.isArray(item.valueMap)) { if (!Array.isArray(item.valueMap)) {
itemErrors.push(`${prefix} 'valueMap' must be an array.`); itemErrors.push(`${prefix} 'valueMap' must be an array.`);
@ -162,7 +146,6 @@ function validateDataModelUpdate(data: any, errors: string[]) {
}); });
} }
}; };
data.contents.forEach((item: any, index: number) => { data.contents.forEach((item: any, index: number) => {
if (!item.key) { if (!item.key) {
errors.push( errors.push(
@ -190,7 +173,6 @@ function validateDataModelUpdate(data: any, errors: string[]) {
} }
}); });
} }
function validateBeginRendering(data: any, errors: string[]) { function validateBeginRendering(data: any, errors: string[]) {
if (data.surfaceId === undefined) { if (data.surfaceId === undefined) {
errors.push("BeginRendering message must have a 'surfaceId' property."); errors.push("BeginRendering message must have a 'surfaceId' property.");
@ -199,7 +181,6 @@ function validateBeginRendering(data: any, errors: string[]) {
errors.push("BeginRendering message must have a 'root' property."); errors.push("BeginRendering message must have a 'root' property.");
} }
} }
function validateBoundValue( function validateBoundValue(
prop: any, prop: any,
propName: string, propName: string,
@ -232,7 +213,6 @@ function validateBoundValue(
); );
} }
} }
function validateComponent( function validateComponent(
component: any, component: any,
allIds: Set<string>, allIds: Set<string>,
@ -246,7 +226,6 @@ function validateComponent(
errors.push(`Component '${component.id}' is missing 'component'.`); errors.push(`Component '${component.id}' is missing 'component'.`);
return; return;
} }
const componentTypes = Object.keys(component.component); const componentTypes = Object.keys(component.component);
if (componentTypes.length !== 1) { if (componentTypes.length !== 1) {
errors.push( errors.push(
@ -254,10 +233,8 @@ function validateComponent(
); );
return; return;
} }
const componentType = componentTypes[0]; const componentType = componentTypes[0];
const properties = component.component[componentType]; const properties = component.component[componentType];
const checkRequired = (props: string[]) => { const checkRequired = (props: string[]) => {
for (const prop of props) { for (const prop of props) {
if (properties[prop] === undefined) { if (properties[prop] === undefined) {
@ -267,7 +244,6 @@ function validateComponent(
} }
} }
}; };
const checkRefs = (ids: (string | undefined)[]) => { const checkRefs = (ids: (string | undefined)[]) => {
for (const id of ids) { for (const id of ids) {
if (id && !allIds.has(id)) { if (id && !allIds.has(id)) {
@ -277,7 +253,6 @@ function validateComponent(
} }
} }
}; };
switch (componentType) { switch (componentType) {
case "Heading": case "Heading":
checkRequired(["text"]); checkRequired(["text"]);