From a4ef1d2078d94c3a5174539e6da0b1b0fa018bfd Mon Sep 17 00:00:00 2001
From: wbz <1664546556@qq.com>
Date: Fri, 9 Jan 2026 20:57:55 +0800
Subject: [PATCH 1/3] fix(renderers): align MultipleChoice selections with spec
---
renderers/angular/src/lib/catalog/default.ts | 3 +-
.../src/lib/catalog/multiple-choice.ts | 84 ++++++++++--
renderers/lit/src/0.8/ui/multiple-choice.ts | 122 ++++++++++++++----
renderers/lit/src/0.8/ui/root.ts | 1 -
.../app/features/library/library.component.ts | 4 +-
5 files changed, 172 insertions(+), 42 deletions(-)
diff --git a/renderers/angular/src/lib/catalog/default.ts b/renderers/angular/src/lib/catalog/default.ts
index 10c1146f..fd6d0d51 100644
--- a/renderers/angular/src/lib/catalog/default.ts
+++ b/renderers/angular/src/lib/catalog/default.ts
@@ -116,7 +116,8 @@ export const DEFAULT_CATALOG: Catalog = {
const properties = (node as Types.MultipleChoiceNode).properties;
return [
inputBinding('options', () => properties.options || []),
- inputBinding('value', () => properties.selections),
+ inputBinding('selections', () => properties.selections),
+ inputBinding('maxAllowedSelections', () => properties.maxAllowedSelections ?? null),
inputBinding('description', () => 'Select an item'), // TODO: this should be defined in the properties
];
},
diff --git a/renderers/angular/src/lib/catalog/multiple-choice.ts b/renderers/angular/src/lib/catalog/multiple-choice.ts
index 538eb5bb..cc5932df 100644
--- a/renderers/angular/src/lib/catalog/multiple-choice.ts
+++ b/renderers/angular/src/lib/catalog/multiple-choice.ts
@@ -16,7 +16,7 @@
import { Component, computed, input } from '@angular/core';
import { DynamicComponent } from '../rendering/dynamic-component';
-import { Primitives } from '@a2ui/lit/0.8';
+import { Primitives, Types } from '@a2ui/lit/0.8';
@Component({
selector: 'a2ui-multiple-choice',
@@ -29,12 +29,17 @@ import { Primitives } from '@a2ui/lit/0.8';
@@ -55,23 +60,78 @@ import { Primitives } from '@a2ui/lit/0.8';
})
export class MultipleChoice extends DynamicComponent {
readonly options = input.required<{ label: Primitives.StringValue; value: string }[]>();
- readonly value = input.required();
+ readonly selections =
+ input.required();
+ readonly maxAllowedSelections = input(null);
readonly description = input.required();
protected readonly selectId = super.getUniqueId('a2ui-multiple-choice');
- protected selectValue = computed(() => super.resolvePrimitive(this.value()));
+ protected selectedValues = computed(() => this.resolveSelections());
+ protected isMultiple = computed(() => {
+ const maxSelections = this.maxAllowedSelections();
+ const selections = this.selectedValues();
+ return typeof maxSelections === 'number' ? maxSelections > 1 : selections.length > 1;
+ });
protected handleChange(event: Event) {
- const path = this.value()?.path;
+ const selections = this.selections();
+ const path = selections?.path;
- if (!(event.target instanceof HTMLSelectElement) || !event.target.value || !path) {
+ if (!(event.target instanceof HTMLSelectElement) || !path) {
return;
}
- this.processor.setData(
- this.component(),
- this.processor.resolvePath(path, this.component().dataContextPath),
- event.target.value,
- );
+ const isMultiple = this.isMultiple();
+ const selectedValues = isMultiple
+ ? Array.from(event.target.selectedOptions).map((option) => option.value)
+ : [event.target.value];
+ const maxSelections = this.maxAllowedSelections();
+ const nextSelections =
+ typeof maxSelections === 'number' && selectedValues.length > maxSelections
+ ? selectedValues.slice(0, maxSelections)
+ : selectedValues;
+
+ if (isMultiple && nextSelections.length !== selectedValues.length) {
+ const allowed = new Set(nextSelections);
+ for (const option of Array.from(event.target.options)) {
+ option.selected = allowed.has(option.value);
+ }
+ }
+
+ this.processor.setData(this.component(), path, nextSelections, this.surfaceId());
+ }
+
+ private resolveSelections() {
+ const selections = this.selections();
+
+ if (!selections || typeof selections !== 'object') {
+ return [];
+ }
+
+ if ('literalArray' in selections) {
+ return Array.isArray(selections.literalArray) ? selections.literalArray : [];
+ }
+
+ if ('literalString' in selections) {
+ return selections.literalString ? [selections.literalString] : [];
+ }
+
+ if ('literal' in selections) {
+ return selections.literal != null ? [selections.literal] : [];
+ }
+
+ if (selections.path) {
+ const resolved = this.processor.getData(
+ this.component(),
+ selections.path,
+ this.surfaceId() ?? undefined,
+ );
+ if (Array.isArray(resolved)) {
+ return resolved.filter((value): value is string => typeof value === 'string');
+ }
+ return typeof resolved === 'string' ? [resolved] : [];
+ }
+
+ return [];
}
}
diff --git a/renderers/lit/src/0.8/ui/multiple-choice.ts b/renderers/lit/src/0.8/ui/multiple-choice.ts
index a4fba38e..d9ad7404 100644
--- a/renderers/lit/src/0.8/ui/multiple-choice.ts
+++ b/renderers/lit/src/0.8/ui/multiple-choice.ts
@@ -14,7 +14,7 @@
limitations under the License.
*/
-import { html, css, PropertyValues, nothing } from "lit";
+import { html, css, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
import { Root } from "./root.js";
import { StringValue } from "../types/primitives.js";
@@ -23,6 +23,7 @@ import { classMap } from "lit/directives/class-map.js";
import { styleMap } from "lit/directives/style-map.js";
import { structuralStyles } from "./styles.js";
import { extractStringValue } from "./utils/utils.js";
+import { ResolvedMultipleChoice } from "../types/types";
@customElement("a2ui-multiplechoice")
export class MultipleChoice extends Root {
@@ -33,7 +34,11 @@ export class MultipleChoice extends Root {
accessor options: { label: StringValue; value: string }[] = [];
@property()
- accessor selections: StringValue | string[] = [];
+ accessor selections: ResolvedMultipleChoice["selections"] | StringValue | null =
+ null;
+
+ @property({ type: Number })
+ accessor maxAllowedSelections: number | null = null;
static styles = [
structuralStyles,
@@ -59,7 +64,6 @@ export class MultipleChoice extends Root {
];
#setBoundValue(value: string[]) {
- console.log(value);
if (!this.selections || !this.processor) {
return;
}
@@ -78,54 +82,115 @@ export class MultipleChoice extends Root {
);
}
- protected willUpdate(changedProperties: PropertyValues): void {
- const shouldUpdate = changedProperties.has("options");
- if (!shouldUpdate) {
- return;
+ #resolveSelections(): string[] {
+ if (!this.selections || typeof this.selections !== "object") {
+ return [];
}
- if (!this.processor || !this.component || Array.isArray(this.selections)) {
- return;
+ if ("literalArray" in this.selections) {
+ return Array.isArray(this.selections.literalArray)
+ ? this.selections.literalArray
+ : [];
}
- this.selections;
+ if ("literalString" in this.selections) {
+ return this.selections.literalString
+ ? [this.selections.literalString]
+ : [];
+ }
- const selectionValue = this.processor.getData(
- this.component,
- this.selections.path!,
- this.surfaceId ?? A2uiMessageProcessor.DEFAULT_SURFACE_ID
- );
+ if ("literal" in this.selections) {
+ return this.selections.literal !== undefined
+ ? [this.selections.literal]
+ : [];
+ }
+
+ if ("path" in this.selections && this.selections.path) {
+ if (!this.processor || !this.component) {
+ return [];
+ }
+
+ const selectionValue = this.processor.getData(
+ this.component,
+ this.selections.path,
+ this.surfaceId ?? A2uiMessageProcessor.DEFAULT_SURFACE_ID
+ );
+
+ if (Array.isArray(selectionValue)) {
+ return selectionValue.filter(
+ (value): value is string => typeof value === "string"
+ );
+ }
+
+ return typeof selectionValue === "string" ? [selectionValue] : [];
+ }
+
+ return [];
+ }
+
+ #getMaxSelections(): number | null {
+ if (typeof this.maxAllowedSelections !== "number") {
+ return null;
+ }
+
+ return this.maxAllowedSelections > 0 ? this.maxAllowedSelections : 0;
+ }
- if (!Array.isArray(selectionValue)) {
+ #isMultiple(selectedValues: string[]): boolean {
+ const maxSelections = this.#getMaxSelections();
+ return maxSelections ? maxSelections > 1 : selectedValues.length > 1;
+ }
+
+ #handleChange(event: Event, allowMultiple: boolean) {
+ if (!(event.target instanceof HTMLSelectElement)) {
return;
}
- this.#setBoundValue(selectionValue as string[]);
+ const selectedValues = allowMultiple
+ ? Array.from(event.target.selectedOptions).map((option) => option.value)
+ : [event.target.value];
+ const maxSelections = this.#getMaxSelections();
+ const nextSelections =
+ maxSelections !== null && selectedValues.length > maxSelections
+ ? selectedValues.slice(0, maxSelections)
+ : selectedValues;
+
+ if (
+ allowMultiple &&
+ nextSelections.length !== selectedValues.length &&
+ event.target.options
+ ) {
+ const allowed = new Set(nextSelections);
+ for (const option of Array.from(event.target.options)) {
+ option.selected = allowed.has(option.value);
+ }
+ }
+
+ this.#setBoundValue(nextSelections);
}
render() {
+ const selectedValues = this.#resolveSelections();
+ const allowMultiple = this.#isMultiple(selectedValues);
+ const selectedSet = new Set(selectedValues);
+
return html`
`;
diff --git a/renderers/lit/src/0.8/ui/root.ts b/renderers/lit/src/0.8/ui/root.ts
index 93ba9cb6..29843f4b 100644
--- a/renderers/lit/src/0.8/ui/root.ts
+++ b/renderers/lit/src/0.8/ui/root.ts
@@ -372,7 +372,6 @@ export class Root extends SignalWatcher(LitElement) {
}
case "MultipleChoice": {
- // TODO: maxAllowedSelections and selections.
const node = component as NodeOfType<"MultipleChoice">;
return html`
Date: Fri, 9 Jan 2026 21:25:31 +0800
Subject: [PATCH 2/3] chore: trigger CLA recheck
From 193873a5e0f661688b300f0c2911b4fb1d49d4c6 Mon Sep 17 00:00:00 2001
From: wbz <1664546556@qq.com>
Date: Sun, 11 Jan 2026 23:15:24 +0800
Subject: [PATCH 3/3] fix(datetime-input): correct month formatting
---
renderers/angular/src/lib/catalog/datetime-input.ts | 2 +-
renderers/lit/src/0.8/ui/datetime-input.ts | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/renderers/angular/src/lib/catalog/datetime-input.ts b/renderers/angular/src/lib/catalog/datetime-input.ts
index e17ca806..88bfc7f2 100644
--- a/renderers/angular/src/lib/catalog/datetime-input.ts
+++ b/renderers/angular/src/lib/catalog/datetime-input.ts
@@ -94,7 +94,7 @@ export class DatetimeInput extends DynamicComponent {
}
const year = this.padNumber(date.getFullYear());
- const month = this.padNumber(date.getMonth());
+ const month = this.padNumber(date.getMonth() + 1);
const day = this.padNumber(date.getDate());
const hours = this.padNumber(date.getHours());
const minutes = this.padNumber(date.getMinutes());
diff --git a/renderers/lit/src/0.8/ui/datetime-input.ts b/renderers/lit/src/0.8/ui/datetime-input.ts
index c77acd8d..3ed2f211 100644
--- a/renderers/lit/src/0.8/ui/datetime-input.ts
+++ b/renderers/lit/src/0.8/ui/datetime-input.ts
@@ -134,7 +134,7 @@ export class DateTimeInput extends Root {
}
const year = this.#padNumber(date.getFullYear());
- const month = this.#padNumber(date.getMonth());
+ const month = this.#padNumber(date.getMonth() + 1);
const day = this.#padNumber(date.getDate());
const hours = this.#padNumber(date.getHours());
const minutes = this.#padNumber(date.getMinutes());