mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(forms): Ensure the control instruction comes after the other bindings
Prior to this change, binding to radio value was sensitive to the order in which `value` & `formField` where binding in the template. The compiler change makes that order non-important. fixes #66402
This commit is contained in:
committed by
Jessica Janiuk
parent
7fd076bdd3
commit
65fa5b5439
+71
@@ -48,3 +48,74 @@ export declare class MyComponent {
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<MyComponent, "ng-component", never, {}, {}, never, never, true, never>;
|
||||
}
|
||||
|
||||
/****************************************************************************************************
|
||||
* PARTIAL FILE: radio_bindings.js
|
||||
****************************************************************************************************/
|
||||
import { Component, Directive, input } from '@angular/core';
|
||||
import * as i0 from "@angular/core";
|
||||
export class FormField {
|
||||
formField = input(...(ngDevMode ? [undefined, { debugName: "formField" }] : []));
|
||||
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: FormField, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
||||
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "0.0.0-PLACEHOLDER", type: FormField, isStandalone: true, selector: "[formField]", inputs: { formField: { classPropertyName: "formField", publicName: "formField", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
|
||||
}
|
||||
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: FormField, decorators: [{
|
||||
type: Directive,
|
||||
args: [{ selector: '[formField]' }]
|
||||
}], propDecorators: { formField: [{ type: i0.Input, args: [{ isSignal: true, alias: "formField", required: false }] }] } });
|
||||
// Notice that we check that the binding order doesn't matter
|
||||
export class MyComponent {
|
||||
value = 'foo';
|
||||
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
||||
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, isStandalone: true, selector: "ng-component", ngImport: i0, template: `
|
||||
<input
|
||||
type="radio"
|
||||
[formField]="value"
|
||||
[value]="'foo'"
|
||||
id="radio"
|
||||
/>
|
||||
|
||||
<input
|
||||
type="radio"
|
||||
[value]="'foo'"
|
||||
[formField]="value"
|
||||
id="radio"
|
||||
/>
|
||||
`, isInline: true, dependencies: [{ kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"] }] });
|
||||
}
|
||||
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, decorators: [{
|
||||
type: Component,
|
||||
args: [{
|
||||
template: `
|
||||
<input
|
||||
type="radio"
|
||||
[formField]="value"
|
||||
[value]="'foo'"
|
||||
id="radio"
|
||||
/>
|
||||
|
||||
<input
|
||||
type="radio"
|
||||
[value]="'foo'"
|
||||
[formField]="value"
|
||||
id="radio"
|
||||
/>
|
||||
`,
|
||||
imports: [FormField],
|
||||
}]
|
||||
}] });
|
||||
|
||||
/****************************************************************************************************
|
||||
* PARTIAL FILE: radio_bindings.d.ts
|
||||
****************************************************************************************************/
|
||||
import * as i0 from "@angular/core";
|
||||
export declare class FormField {
|
||||
readonly formField: import("@angular/core").InputSignal<string | undefined>;
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<FormField, never>;
|
||||
static ɵdir: i0.ɵɵDirectiveDeclaration<FormField, "[formField]", never, { "formField": { "alias": "formField"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
|
||||
}
|
||||
export declare class MyComponent {
|
||||
value: string;
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<MyComponent, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<MyComponent, "ng-component", never, {}, {}, never, never, true, never>;
|
||||
}
|
||||
|
||||
|
||||
+9
@@ -9,6 +9,15 @@
|
||||
"files": ["control_bindings.js"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "should generate control instruction for `field` property bindings on radio inputs and execute it after the value",
|
||||
"inputFiles": ["radio_bindings.ts"],
|
||||
"expectations": [
|
||||
{
|
||||
"files": ["radio_bindings.js"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
export class MyComponent {
|
||||
// ...
|
||||
static ɵcmp = /* @__PURE__ */i0.ɵɵdefineComponent({
|
||||
type: MyComponent,
|
||||
selectors: [["ng-component"]],
|
||||
decls: 2,
|
||||
vars: 6,
|
||||
consts: [["type", "radio", "id", "radio", 3, "formField", "value"], ["type", "radio", "id", "radio", 3, "value", "formField"]],
|
||||
template: function MyComponent_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
i0.ɵɵelement(0, "input", 0);
|
||||
i0.ɵɵcontrolCreate();
|
||||
i0.ɵɵelement(1, "input", 1);
|
||||
i0.ɵɵcontrolCreate();
|
||||
}
|
||||
if (rf & 2) {
|
||||
i0.ɵɵproperty("value", "foo");
|
||||
i0.ɵɵcontrol(ctx.value, "formField");
|
||||
i0.ɵɵadvance();
|
||||
i0.ɵɵproperty("value", "foo");
|
||||
i0.ɵɵcontrol(ctx.value, "formField");
|
||||
}
|
||||
},
|
||||
dependencies: [FormField],
|
||||
encapsulation: 2
|
||||
});
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { Component, Directive, input } from '@angular/core';
|
||||
|
||||
@Directive({selector: '[formField]'})
|
||||
export class FormField {
|
||||
readonly formField = input<string>();
|
||||
}
|
||||
|
||||
// Notice that we check that the binding order doesn't matter
|
||||
@Component({
|
||||
template: `
|
||||
<input
|
||||
type="radio"
|
||||
[formField]="value"
|
||||
[value]="'foo'"
|
||||
id="radio"
|
||||
/>
|
||||
|
||||
<input
|
||||
type="radio"
|
||||
[value]="'foo'"
|
||||
[formField]="value"
|
||||
id="radio"
|
||||
/>
|
||||
`,
|
||||
imports: [FormField],
|
||||
})
|
||||
export class MyComponent {
|
||||
value = 'foo';
|
||||
|
||||
}
|
||||
@@ -66,6 +66,7 @@ const UPDATE_ORDERING: Array<Rule<ir.UpdateOp>> = [
|
||||
{test: kindWithInterpolationTest(ir.OpKind.Property, true)},
|
||||
{test: nonInterpolationPropertyKindTest},
|
||||
{test: kindWithInterpolationTest(ir.OpKind.Attribute, false)},
|
||||
{test: kindTest(ir.OpKind.Control)},
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -97,6 +98,7 @@ const handledOpKinds = new Set([
|
||||
ir.OpKind.DomProperty,
|
||||
ir.OpKind.Attribute,
|
||||
ir.OpKind.Animation,
|
||||
ir.OpKind.Control,
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
@@ -2667,6 +2667,30 @@ describe('field directive', () => {
|
||||
expect(cmp.f().value()).toBe('b');
|
||||
});
|
||||
|
||||
it('synchronizes with a radio group with bindings', () => {
|
||||
const {cmp, inputA, inputB, inputC, ABC} = setupRadioWithBindingsGroup();
|
||||
|
||||
// All the inputs should have the same name.
|
||||
expect(inputA.name).toBe('test');
|
||||
expect(inputB.name).toBe('test');
|
||||
expect(inputC.name).toBe('test');
|
||||
|
||||
// Model -> View
|
||||
act(() => cmp.f().value.set(ABC.C));
|
||||
expect(inputA.checked).toBe(false);
|
||||
expect(inputB.checked).toBe(false);
|
||||
expect(inputC.checked).toBe(true);
|
||||
|
||||
// View -> Model
|
||||
act(() => {
|
||||
inputB.click();
|
||||
});
|
||||
expect(inputA.checked).toBe(false);
|
||||
expect(inputB.checked).toBe(true);
|
||||
expect(inputC.checked).toBe(false);
|
||||
expect(cmp.f().value()).toBe(ABC.B);
|
||||
});
|
||||
|
||||
it('synchronizes with a textarea', () => {
|
||||
@Component({
|
||||
imports: [FormField],
|
||||
@@ -4321,6 +4345,39 @@ function setupRadioGroup() {
|
||||
return {cmp, inputA, inputB, inputC};
|
||||
}
|
||||
|
||||
function setupRadioWithBindingsGroup() {
|
||||
enum ABC {
|
||||
A = 'a',
|
||||
B = 'b',
|
||||
C = 'c',
|
||||
}
|
||||
@Component({
|
||||
imports: [FormField],
|
||||
template: `
|
||||
<form>
|
||||
<input type="radio" [formField]="f" [value]="ABC.A" />
|
||||
<input type="radio" [formField]="f" [value]="ABC.B" />
|
||||
<input type="radio" [formField]="f" [value]="ABC.C" />
|
||||
</form>
|
||||
`,
|
||||
})
|
||||
class TestCmp {
|
||||
f = form(signal(ABC.A), {
|
||||
name: 'test',
|
||||
});
|
||||
ABC = ABC;
|
||||
}
|
||||
|
||||
const fix = act(() => TestBed.createComponent(TestCmp));
|
||||
const formEl = (fix.nativeElement as HTMLElement).firstChild as HTMLFormElement;
|
||||
const inputs = Array.from(formEl.children) as HTMLInputElement[];
|
||||
|
||||
const [inputA, inputB, inputC] = inputs;
|
||||
const cmp = fix.componentInstance as TestCmp;
|
||||
|
||||
return {cmp, inputA, inputB, inputC, ABC};
|
||||
}
|
||||
|
||||
function act<T>(fn: () => T): T {
|
||||
try {
|
||||
return fn();
|
||||
|
||||
Reference in New Issue
Block a user