feat(forms): experimental prototype signal forms support for select[multiple]#68350
feat(forms): experimental prototype signal forms support for select[multiple]#68350sonukapoor wants to merge 5 commits intoangular:mainfrom
Conversation
Add an initial Signal Forms implementation for <select multiple>. This prototype (for angular#67563) adds a dedicated runtime path for multi-select controls and updates Signal Forms type checking so <select multiple [formField]> is treated as string[] instead of string. The current scope is intentionally small: - support native <select multiple> - synchronize model -> DOM and DOM -> model - resync correctly when options change - preserve DOM option order in the emitted value Not included in this prototype: - compareWith - object-valued options / ngValue semantics - broader selection abstractions Also adds focused coverage for: - Signal Forms type-checking of <select multiple> - runtime synchronization for multi-select controls
| function getSelectMultipleControlValue(select: HTMLSelectElement): string[] { | ||
| const selected: string[] = []; | ||
| const selectedOptions = select.selectedOptions; | ||
|
|
||
| if (selectedOptions !== undefined) { | ||
| for (let i = 0; i < selectedOptions.length; i++) { | ||
| selected.push(selectedOptions[i].value); | ||
| } | ||
| return selected; | ||
| } | ||
|
|
||
| for (let i = 0; i < select.options.length; i++) { | ||
| const option = select.options[i]; | ||
| if (option.selected) { | ||
| selected.push(option.value); | ||
| } | ||
| } | ||
|
|
||
| return selected; | ||
| } |
There was a problem hiding this comment.
It's not obvious why we need to cover both cases. Can you add a comment about that
There was a problem hiding this comment.
Added a comment to the fallback branch explaining why it exists. selectedOptions isn't available in all environments (older jsdom in particular), so we iterate options and check the selected flag as a fallback.
There was a problem hiding this comment.
older jsdom in particular
Angular doesn't use JSDOM but Domino. But it looks like this argument still hold. I couldn't find any trace of selectedOptions on https://github.com/angular/domino.
There was a problem hiding this comment.
Good correction, thanks. It's Domino, not jsdom. I updated the comment to reflect that.
There was a problem hiding this comment.
I second guessing this. If we have that 2nd part that checks for selected. Do we actually need selectedOptions ?
There was a problem hiding this comment.
The options/selected fallback is functionally equivalent and works in all environments, so selectedOptions was only ever a minor performance optimization. For typical multi-selects the difference is negligible. Dropped the selectedOptions path entirely and simplified to a single loop.
- Remove $any() cast from the test — the TCB already enforces string[] for select[multiple], so no workaround is needed - Extract the CONTROL_BINDING_NAMES update loop into a shared applyControlStateBindings() helper in bindings.ts, used by both the native and select-multiple control paths - Throw a RuntimeError (code 1921) in setSelectMultipleControlValue when a non-array value is passed; the guard is unconditional so production never silently corrupts the selection state - Move the selectedOptions fallback comment to the fallback branch where it actually explains the divergence
… comment Angular uses Domino for server-side rendering, not jsdom. The fallback comment now correctly names Domino as the environment where `selectedOptions` is unavailable.
b4ab903 to
950ab18
Compare
…e path Removed the selectedOptions fast path and use only the options/selected fallback, which works correctly in all environments including Domino. The performance difference is negligible for typical multi-selects and the simpler single-path implementation is easier to follow.
| return true; | ||
| } | ||
|
|
||
| return node.inputs.some( |
There was a problem hiding this comment.
Won't this return true for any property binding to multiple? What if it's been set to false?
<select [multiple]="false" ... >There was a problem hiding this comment.
I think dynamic [type] bindings raise a similar problem and it looks like we address those above by falling back to all value types. We may need to do something similar here, where the type because string | string[].
There was a problem hiding this comment.
You're right. I replaced hasMultipleSelect with getSelectMultipleMode that returns 'static' | 'dynamic' | 'none'. The static attribute case still maps to string[], but any property binding now maps to string | string[] since we can't know the value at compile time.
There was a problem hiding this comment.
I have also addressed, alongside the first comment, getSelectMultipleMode now returns 'dynamic' for any property binding, and the caller maps that to string | string[]. Same approach as the hasDynamicType handling for [type] bindings on <input>.
| export function applyControlStateBindings( | ||
| bindings: {[key: string]: unknown}, | ||
| state: ReadonlyFieldState<unknown>, | ||
| onUpdate: (name: ControlBindingKey, value: unknown) => void, |
There was a problem hiding this comment.
It looks like the onUpdate callback is identical for both cases. Can we just inline that here and avoid the closure allocation?
There was a problem hiding this comment.
Agreed. The callback was identical in both callers since input and select are both just parent.nativeFormElement. Removed the onUpdate parameter entirely. applyControlStateBindings now takes host and parent directly and performs the two operations inline. A ControlStateBindingTarget structural interface avoids pulling FormField into bindings.ts (which would create a cycle). Both call sites are now just applyControlStateBindings(bindings, state, host, parent).
| } else if ( | ||
| this.elementIsNativeFormElement && | ||
| this.nativeFormElement.tagName === 'SELECT' && | ||
| (this.nativeFormElement as HTMLSelectElement).multiple |
There was a problem hiding this comment.
Same concern raised with the type check code above. If the element has a [multiple] property binding, it may not be set yet at this point. Do we want to delegate all <select> elements through a selectControlCreate(), which dynamically handles multiple? Alternatively we can prohibit a dynamic multiple binding if we prefer.
There was a problem hiding this comment.
Went with the prohibit option. The check happens at the start of the first update cycle (after Angular has applied property bindings), so it catches the case where [multiple] was set dynamically before ɵngControlCreate had a chance to see the correct value. If select.multiple doesn't match what was assumed at creation time, a DYNAMIC_SELECT_MULTIPLE_BINDING error is thrown in dev mode pointing to the static attribute as the fix. A unified dynamic handler could be a follow-up if there's demand for it.
…] prototype - Fix hasMultipleSelect to distinguish static vs dynamic [multiple] bindings. A property binding [multiple]="..." can evaluate to false at runtime, so the type checker now returns string|string[] for dynamic bindings rather than always assuming string[]. - Remove the onUpdate callback from applyControlStateBindings. Both callers had identical callbacks, so the operations are now inlined directly using host/parent parameters. A ControlStateBindingTarget structural interface avoids introducing a circular import. - Add a dev-mode guard that throws DYNAMIC_SELECT_MULTIPLE_BINDING if the select.multiple state doesn't match what was observed at ɵngControlCreate time. Dynamic [multiple] bindings are not supported; the static attribute should be used instead.
Add an initial Signal Forms implementation for
<select multiple>.This is an experimental draft implementation of Signal Forms support for
<select multiple>, and I’m mainly looking for feedback on the direction before taking it further.I discussed this issue with @AndrewKushnir, who suggested putting together a draft PR so the team could comment on the approach in code.
The current scope is intentionally small:
<select multiple>Not included in this prototype:
Also adds focused coverage for:
<select multiple>PR Checklist
Please check if your PR fulfills the following requirements:
PR Type
What kind of change does this PR introduce?
What is the current behavior?
Issue Number: #67563
Does this PR introduce a breaking change?