Skip to content

Commit 2d85ae5

Browse files
mmalerbakirjs
authored andcommitted
feat(forms): add [formField] directive
This will replace the `[field]` directive, since `[field]` is a very generic name for signal forms to commandeer refactor(forms): hook up `formField` directive in compiler Hooks up the `formField` direcive to get the same treatment as the `field` directive in the compiler. apply updated formatting
1 parent 53d3ae0 commit 2d85ae5

File tree

21 files changed

+3114
-46
lines changed

21 files changed

+3114
-46
lines changed

goldens/public-api/forms/signals/index.api.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ export interface FieldState<TValue, TKey extends string | number = string | numb
170170
// (undocumented)
171171
readonly errors: Signal<ValidationError.WithField[]>;
172172
readonly errorSummary: Signal<ValidationError.WithField[]>;
173-
readonly fieldBindings: Signal<readonly Field<unknown>[]>;
173+
readonly formFieldBindings: Signal<readonly (Field<unknown> | FormField<unknown>)[]>;
174174
readonly hidden: Signal<boolean>;
175175
readonly invalid: Signal<boolean>;
176176
readonly keyInParent: Signal<TKey>;
@@ -196,12 +196,37 @@ export function form<TModel>(model: WritableSignal<TModel>, schemaOrOptions: Sch
196196
// @public
197197
export function form<TModel>(model: WritableSignal<TModel>, schema: SchemaOrSchemaFn<TModel>, options: FormOptions): FieldTree<TModel>;
198198

199+
// @public
200+
export const FORM_FIELD: InjectionToken<FormField<unknown>>;
201+
199202
// @public
200203
export interface FormCheckboxControl extends FormUiControl {
201204
readonly checked: ModelSignal<boolean>;
202205
readonly value?: undefined;
203206
}
204207

208+
// @public
209+
export class FormField<T> {
210+
// (undocumented)
211+
readonlyCONTROL]: {
212+
readonly create: typeof ɵɵcontrolCreate;
213+
readonly update: typeof ɵcontrolUpdate;
214+
};
215+
// (undocumented)
216+
readonly element: HTMLElement;
217+
// (undocumented)
218+
readonly formField: i0.InputSignal<FieldTree<T>>;
219+
protected getOrCreateNgControl(): InteropNgControl;
220+
// (undocumented)
221+
readonly injector: Injector;
222+
// (undocumented)
223+
readonly state: i0.Signal<[T] extends [_angular_forms.AbstractControl<any, any, any>] ? CompatFieldState<T, string | number> : FieldState<T, string | number>>;
224+
// (undocumented)
225+
static ɵdir: i0.ɵɵDirectiveDeclaration<FormField<any>, "[formField]", never, { "formField": { "alias": "formField"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;
226+
// (undocumented)
227+
static ɵfac: i0.ɵɵFactoryDeclaration<FormField<any>, never>;
228+
}
229+
205230
// @public
206231
export interface FormOptions {
207232
adapter?: FieldAdapter;
@@ -520,7 +545,7 @@ export type SchemaPathTree<TModel, TPathKind extends PathKind = PathKind.Root> =
520545
// @public
521546
export interface SignalFormsConfig {
522547
classes?: {
523-
[className: string]: (state: Field<unknown>) => boolean;
548+
[className: string]: (state: Field<unknown> | FormField<unknown>) => boolean;
524549
};
525550
}
526551

packages/compiler-cli/src/ngtsc/typecheck/src/ops/signal_forms.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ import {
2222
TmplAstTemplate,
2323
} from '@angular/compiler';
2424
import ts from 'typescript';
25-
import {TcbOp} from './base';
26-
import type {Context} from './context';
27-
import type {Scope} from './scope';
28-
import {addParseSpanInfo} from '../diagnostics';
29-
import {tsDeclareVariable} from '../ts_util';
3025
import {TypeCheckableDirectiveMeta} from '../../api';
31-
import {tcbExpression} from './expression';
3226
import {markIgnoreDiagnostics} from '../comments';
27+
import {addParseSpanInfo} from '../diagnostics';
28+
import {tsDeclareVariable} from '../ts_util';
29+
import {TcbOp} from './base';
3330
import {TcbBoundAttribute} from './bindings';
31+
import type {Context} from './context';
32+
import {tcbExpression} from './expression';
33+
import type {Scope} from './scope';
3434

3535
/** Possible types of custom form control directives. */
3636
export type CustomFormControlType = 'value' | 'checkbox';
@@ -103,7 +103,11 @@ export class TcbNativeFieldOp extends TcbOp {
103103
override execute(): null {
104104
const inputs = this.node instanceof TmplAstHostElement ? this.node.bindings : this.node.inputs;
105105
const fieldBinding =
106-
inputs.find((input) => input.type === BindingType.Property && input.name === 'field') ?? null;
106+
inputs.find(
107+
(input) =>
108+
input.type === BindingType.Property &&
109+
(input.name === 'field' || input.name === 'formField'),
110+
) ?? null;
107111

108112
// This should only happen if there's something like `<input field="static"/>`
109113
// which will be caught by the input type checking of the `Field` directive.
@@ -227,7 +231,8 @@ export function expandBoundAttributesForField(
227231
customFormControlType: CustomFormControlType | null,
228232
): TcbBoundAttribute[] | null {
229233
const fieldBinding = node.inputs.find(
230-
(input) => input.type === BindingType.Property && input.name === 'field',
234+
(input) =>
235+
input.type === BindingType.Property && (input.name === 'field' || input.name === 'formField'),
231236
);
232237

233238
if (!fieldBinding) {
@@ -281,7 +286,7 @@ export function expandBoundAttributesForField(
281286
}
282287

283288
export function isFieldDirective(meta: TypeCheckableDirectiveMeta): boolean {
284-
if (meta.name !== 'Field') {
289+
if (meta.name !== 'Field' && meta.name !== 'FormField') {
285290
return false;
286291
}
287292

packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/control_bindings/control_bindings.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export class MyComponent {
1919
i0.ɵɵadvance();
2020
i0.ɵɵattribute("field", ctx.value);
2121
i0.ɵɵadvance(2);
22-
i0.ɵɵcontrol(ctx.value);
22+
i0.ɵɵcontrol(ctx.value, "field");
2323
}
2424
},
2525
dependencies: [Field],

packages/compiler/src/render3/view/compiler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88

99
import {ConstantPool} from '../../constant_pool';
1010
import * as core from '../../core';
11+
import {CssSelector} from '../../directive_matching';
1112
import * as o from '../../output/output_ast';
1213
import {ParseError, ParseSourceSpan} from '../../parse_util';
13-
import {CssSelector} from '../../directive_matching';
1414
import {ShadowCss} from '../../shadow_css';
1515
import {CompilationJobKind, TemplateCompilationMode} from '../../template/pipeline/src/compilation';
1616
import {emitHostBindingFunction, emitTemplateFn, transform} from '../../template/pipeline/src/emit';

packages/compiler/src/template/pipeline/ir/src/ops/update.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1067,7 +1067,8 @@ export function createStoreLetOp(
10671067
/**
10681068
* A specialized {@link PropertyOp} that may bind a form field to a control.
10691069
*/
1070-
export interface ControlOp extends Omit<PropertyOp, 'kind' | 'name'> {
1070+
// TODO: Omit name when we remove the `Field` directive.
1071+
export interface ControlOp extends Omit<PropertyOp, 'kind'> {
10711072
kind: OpKind.Control;
10721073
}
10731074

@@ -1076,6 +1077,7 @@ export function createControlOp(op: BindingOp): ControlOp {
10761077
return {
10771078
kind: OpKind.Control,
10781079
target: op.target,
1080+
name: op.name,
10791081
expression: op.expression,
10801082
bindingKind: op.bindingKind,
10811083
securityContext: op.securityContext,

packages/compiler/src/template/pipeline/src/ingest.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,9 @@ function ingestElement(unit: ViewCompilationUnit, element: t.Element): void {
318318

319319
// We want to ensure that the controlCreateOp is after the ops that create the element
320320
const fieldInput = element.inputs.find(
321-
(input) => input.name === 'field' && input.type === e.BindingType.Property,
321+
(input) =>
322+
(input.name === 'field' || input.name === 'formField') &&
323+
input.type === e.BindingType.Property,
322324
);
323325
if (fieldInput) {
324326
// If the input name is 'field', this could be a form control binding which requires a

packages/compiler/src/template/pipeline/src/instruction.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,7 @@ export function property(
596596
}
597597

598598
export function control(
599+
name: string,
599600
expression: o.Expression | ir.Interpolation,
600601
sanitizer: o.Expression | null,
601602
sourceSpan: ParseSourceSpan,
@@ -606,6 +607,7 @@ export function control(
606607
} else {
607608
args.push(expression);
608609
}
610+
args.push(o.literal(name));
609611
if (sanitizer !== null) {
610612
args.push(sanitizer);
611613
}

packages/compiler/src/template/pipeline/src/phases/attribute_extraction.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export function extractAttributes(job: CompilationJob): void {
6262
op.target,
6363
ir.BindingKind.Property,
6464
null,
65-
'field',
65+
op.name,
6666
/* expression */ null,
6767
/* i18nContext */ null,
6868
/* i18nMessage */ null,

packages/compiler/src/template/pipeline/src/phases/binding_specialization.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ export function specializeBindings(job: CompilationJob): void {
127127
op.sourceSpan,
128128
),
129129
);
130-
} else if (op.name === 'field') {
130+
} else if (op.name === 'field' || op.name === 'formField') {
131131
ir.OpList.replace<ir.UpdateOp>(op, ir.createControlOp(op));
132132
} else {
133133
ir.OpList.replace<ir.UpdateOp>(

packages/compiler/src/template/pipeline/src/phases/reify.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -743,7 +743,7 @@ function reifyProperty(op: ir.PropertyOp): ir.UpdateOp {
743743
}
744744

745745
function reifyControl(op: ir.ControlOp): ir.UpdateOp {
746-
return ng.control(op.expression, op.sanitizer, op.sourceSpan);
746+
return ng.control(op.name, op.expression, op.sanitizer, op.sourceSpan);
747747
}
748748

749749
function reifyIrExpression(expr: o.Expression): o.Expression {

0 commit comments

Comments
 (0)