@@ -10,11 +10,11 @@ import {bindingUpdated} from '../bindings';
1010import { ɵCONTROL , ɵControl } from '../interfaces/control' ;
1111import { ComponentDef } from '../interfaces/definition' ;
1212import { InputFlags } from '../interfaces/input_flags' ;
13- import { TNode } from '../interfaces/node' ;
13+ import { TNode , TNodeFlags } from '../interfaces/node' ;
1414import { Renderer } from '../interfaces/renderer' ;
1515import { SanitizerFn } from '../interfaces/sanitization' ;
1616import { isComponentHost } from '../interfaces/type_checks' ;
17- import { LView , RENDERER } from '../interfaces/view' ;
17+ import { LView , RENDERER , TView } from '../interfaces/view' ;
1818import { getCurrentTNode , getLView , getSelectedTNode , getTView , nextBindingIndex } from '../state' ;
1919import { getNativeByTNode } from '../util/view_utils' ;
2020import { listenToOutput } from '../view/directive_outputs' ;
@@ -33,34 +33,22 @@ import {writeToDirectiveInput} from './write_to_directive_input';
3333 */
3434export function ɵɵcontrolCreate ( ) : void {
3535 const lView = getLView < { } | null > ( ) ;
36+ const tView = getTView ( ) ;
3637 const tNode = getCurrentTNode ( ) ! ;
38+ const control = tView . firstCreatePass
39+ ? getControlDirectiveFirstCreatePass ( tView , tNode , lView )
40+ : getControlDirective ( tNode , lView ) ;
3741
38- // TODO(https://github.com/orgs/angular/projects/60/views/1?pane=issue&itemId=131712274)
39- // * cache the field directive index or instance for reuse.
40- const control = getControlDirective ( tNode , lView ) ;
4142 if ( ! control ) {
4243 return ;
4344 }
4445
45- // TODO(https://github.com/orgs/angular/projects/60/views/1?pane=issue&itemId=131712274):
46- // * cache the custom control component index or instance for reuse.
47- // * cache the control model name for reuse.
48- const customControl = getCustomControlComponent ( tNode ) ;
49- if ( customControl ) {
50- const [ componentIndex , modelName ] = customControl ;
51- listenToCustomControl ( lView , tNode , control , componentIndex , modelName ) ;
52- } else if ( isNativeControl ( lView , tNode ) ) {
53- listenToNativeControl ( lView , tNode , control ) ;
46+ if ( tNode . flags & TNodeFlags . isFormValueControl ) {
47+ listenToCustomControl ( lView , tNode , control , 'value' ) ;
48+ } else if ( tNode . flags & TNodeFlags . isFormCheckboxControl ) {
49+ listenToCustomControl ( lView , tNode , control , 'checked' ) ;
5450 } else {
55- // For example, user wrote <div [control]="f">.
56- // TODO: https://github.com/orgs/angular/projects/60/views/1?pane=issue&itemId=131860276
57- const tagName = tNode . value ;
58- throw new RuntimeError (
59- RuntimeErrorCode . INVALID_CONTROL_HOST ,
60- `'<${ tagName } >' is an invalid control host. The host must be a native form control (such ` +
61- `as <input>', '<select>', or '<textarea>') or a custom form control component with a ` +
62- `'value' or 'checked' model.` ,
63- ) ;
51+ listenToNativeControl ( lView , tNode , control ) ;
6452 }
6553
6654 control . register ( ) ;
@@ -79,49 +67,41 @@ export function ɵɵcontrolCreate(): void {
7967 */
8068export function ɵɵcontrol < T > ( value : T , sanitizer ?: SanitizerFn | null ) : void {
8169 const lView = getLView ( ) ;
82- const bindingIndex = nextBindingIndex ( ) ;
8370 const tNode = getSelectedTNode ( ) ;
71+ const bindingIndex = nextBindingIndex ( ) ;
8472
8573 if ( bindingUpdated ( lView , bindingIndex , value ) ) {
8674 const tView = getTView ( ) ;
8775 setPropertyAndInputs ( tNode , lView , 'field' , value , lView [ RENDERER ] , sanitizer ) ;
8876 ngDevMode && storePropertyBindingMetadata ( tView . data , tNode , 'field' , bindingIndex ) ;
8977 }
9078
91- // TODO: https://github.com/orgs/angular/projects/60/views/1?pane=issue&itemId=131711472
92- // * only run if this is really a control binding determine in the create pass.
9379 const control = getControlDirective ( tNode , lView ) ;
94- if ( ! control ) {
95- return ;
96- }
97-
98- const customControl = getCustomControlComponent ( tNode ) ;
99- if ( customControl ) {
100- const [ componentIndex , modelName ] = customControl ;
101- updateCustomControl ( lView , componentIndex , modelName , control ) ;
102- } else {
103- updateNativeControl ( tNode , lView , control ) ;
80+ if ( control ) {
81+ if ( tNode . flags & TNodeFlags . isFormValueControl ) {
82+ updateCustomControl ( tNode , lView , control , 'value' ) ;
83+ } else if ( tNode . flags & TNodeFlags . isFormCheckboxControl ) {
84+ updateCustomControl ( tNode , lView , control , 'checked' ) ;
85+ } else {
86+ updateNativeControl ( tNode , lView , control ) ;
87+ }
10488 }
10589}
10690
107- /**
108- * Returns the {@link ɵControl} directive on the specified node, if one is present and a `field`
109- * input is bound to it, but not to a component. If a `field` input is bound to a component, we
110- * assume the component will manage the control in its own template and return nothing to indicate
111- * that the directive should not be set up.
112- *
113- * @param tNode The `TNode` of the element to check.
114- * @param lView The `LView` that contains the element.
115- */
116- function getControlDirective < T > ( tNode : TNode , lView : LView ) : ɵControl < T > | undefined {
91+ function getControlDirectiveFirstCreatePass < T > (
92+ tView : TView ,
93+ tNode : TNode ,
94+ lView : LView ,
95+ ) : ɵControl < T > | undefined {
11796 const directiveIndices = tNode . inputs ?. [ 'field' ] ;
11897 if ( ! directiveIndices ) {
119- // There are no matching inputs for the `[control ]` property binding.
98+ // There are no matching inputs for the `[field ]` property binding.
12099 return ;
121100 }
122101
102+ let componentIndex ! : number ;
123103 if ( isComponentHost ( tNode ) ) {
124- const componentIndex = tNode . directiveStart + tNode . componentOffset ;
104+ componentIndex = tNode . directiveStart + tNode . componentOffset ;
125105 if ( directiveIndices . includes ( componentIndex ) ) {
126106 // If component has a `field` input, we assume that it will handle binding the field to the
127107 // appropriate native/custom control in its template, so we do not attempt to bind any inputs
@@ -130,49 +110,74 @@ function getControlDirective<T>(tNode: TNode, lView: LView): ɵControl<T> | unde
130110 }
131111 }
132112
133- // Search for the `Field` directive.
134- for ( let index of directiveIndices ) {
135- const directive = lView [ index ] ;
136- if ( ɵCONTROL in directive ) {
137- return directive ;
113+ // Search for the `ɵControl` directive.
114+ const control = findControlDirective < T > ( lView , directiveIndices ) ;
115+ if ( ! control ) {
116+ // The `ɵControl` directive was not imported by this component.
117+ return ;
118+ }
119+
120+ tNode . flags |= TNodeFlags . isFormControl ;
121+
122+ if ( isComponentHost ( tNode ) ) {
123+ const componentDef = tView . data [ componentIndex ] as ComponentDef < unknown > ;
124+ // TODO: should we check that any additional field state inputs are signal based?
125+ if ( hasModelInput ( componentDef , 'value' ) ) {
126+ tNode . flags |= TNodeFlags . isFormValueControl ;
127+ return control ;
128+ } else if ( hasModelInput ( componentDef , 'checked' ) ) {
129+ tNode . flags |= TNodeFlags . isFormCheckboxControl ;
130+ return control ;
138131 }
139132 }
140133
141- // The `Field` directive was not imported by this component.
142- return ;
143- }
134+ const nativeElement = lView [ tNode . index ] ;
135+ if ( isNativeControl ( nativeElement ) ) {
136+ if ( isNumericInput ( nativeElement ) ) {
137+ tNode . flags |= TNodeFlags . isNativeNumericControl ;
138+ }
139+ if ( isTextControl ( nativeElement ) ) {
140+ tNode . flags |= TNodeFlags . isNativeTextControl ;
141+ }
142+ return control ;
143+ }
144144
145- /**
146- * The name of the property that represents a control's value.
147- */
148- type ControlModelName = 'value' | 'checked' ;
145+ const tagName = tNode . value ;
146+ throw new RuntimeError (
147+ RuntimeErrorCode . INVALID_FIELD_DIRECTIVE_HOST ,
148+ `'<${ tagName } >' is an invalid [field] directive host. The host must be a native form control ` +
149+ `(such as <input>', '<select>', or '<textarea>') or a custom form control component with a ` +
150+ `'value' or 'checked' model.` ,
151+ ) ;
152+ }
149153
150154/**
151- * Returns information about the component on the specified node, if it appears to be a custom form
152- * control.
153- *
154- * A component is considered a custom form control if it has a model input named `value` or
155- * `checked`.
155+ * Returns the {@link ɵControl} directive on the specified node, if one is present and a `field`
156+ * input is bound to it, but not to a component. If a `field` input is bound to a component, we
157+ * assume the component will manage the control in its own template and return nothing to indicate
158+ * that the directive should not be set up.
156159 *
157160 * @param tNode The `TNode` of the element to check.
158- * @returns an array containing the component index and model input name if it's a custom form
159- * control, or undefined.
161+ * @param lView The `LView` that contains the element.
160162 */
161- function getCustomControlComponent ( tNode : TNode ) : [ number , ControlModelName ] | undefined {
162- if ( ! isComponentHost ( tNode ) ) {
163- return ;
164- }
165- const tView = getTView ( ) ;
166- const componentIndex = tNode . directiveStart + tNode . componentOffset ;
167- const componentDef = tView . data [ componentIndex ] as ComponentDef < unknown > ;
168- if ( hasModelInput ( componentDef , 'value' ) ) {
169- return [ componentIndex , 'value' ] ;
170- }
171- if ( hasModelInput ( componentDef , 'checked' ) ) {
172- return [ componentIndex , 'checked' ] ;
163+ function getControlDirective < T > ( tNode : TNode , lView : LView ) : ɵControl < T > | undefined {
164+ return tNode . flags & TNodeFlags . isFormControl
165+ ? findControlDirective ( lView , tNode . inputs ! [ 'field' ] )
166+ : undefined ;
167+ }
168+
169+ function findControlDirective < T > (
170+ lView : LView ,
171+ directiveIndices : number [ ] ,
172+ ) : ɵControl < T > | undefined {
173+ for ( let index of directiveIndices ) {
174+ const directive = lView [ index ] ;
175+ if ( ɵCONTROL in directive ) {
176+ return directive ;
177+ }
173178 }
174- // TODO: https://github.com/orgs/angular/projects/60/views/1?pane=issue&itemId=131861022
175- // * should we check that any additional field state inputs are signal based?
179+
180+ // The `Field` directive was not imported by this component.
176181 return ;
177182}
178183
@@ -205,9 +210,9 @@ function listenToCustomControl(
205210 lView : LView < { } | null > ,
206211 tNode : TNode ,
207212 control : ɵControl < unknown > ,
208- componentIndex : number ,
209- modelName : ControlModelName ,
213+ modelName : string ,
210214) {
215+ const componentIndex = tNode . directiveStart + tNode . componentOffset ;
211216 const outputName = modelName + 'Change' ;
212217 listenToOutput (
213218 tNode ,
@@ -254,8 +259,7 @@ interface HTMLTextAreaElementNarrowed extends HTMLTextAreaElement {
254259 */
255260type NativeControlElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElementNarrowed ;
256261
257- function isNativeControl ( lView : LView < unknown > , tNode : TNode ) : boolean {
258- const element = lView [ tNode . index ] ;
262+ function isNativeControl ( element : unknown ) : element is NativeControlElement {
259263 return (
260264 element instanceof HTMLInputElement ||
261265 element instanceof HTMLSelectElement ||
@@ -314,12 +318,13 @@ function listenToNativeControl(lView: LView<{} | null>, tNode: TNode, control:
314318 * @param control The `ɵControl` directive instance.
315319 */
316320function updateCustomControl (
321+ tNode : TNode ,
317322 lView : LView ,
318- componentIndex : number ,
319- modelName : ControlModelName ,
320323 control : ɵControl < unknown > ,
324+ modelName : string ,
321325) {
322326 const tView = getTView ( ) ;
327+ const componentIndex = tNode . directiveStart + tNode . componentOffset ;
323328 const component = lView [ componentIndex ] ;
324329 const componentDef = tView . data [ componentIndex ] as ComponentDef < { } > ;
325330 const state = control . state ( ) ;
@@ -380,16 +385,12 @@ function updateNativeControl(tNode: TNode, lView: LView, control: ɵControl<unkn
380385 setBooleanAttribute ( renderer , input , 'readonly' , state . readonly ( ) ) ;
381386 setBooleanAttribute ( renderer , input , 'required' , state . required ( ) ) ;
382387
383- // TODO: https://github.com/orgs/angular/projects/60/views/1?pane=issue&itemId=131711472
384- // * cache this in `tNode.flags`.
385- if ( isNumericInput ( input ) ) {
388+ if ( tNode . flags & TNodeFlags . isNativeNumericControl ) {
386389 setOptionalAttribute ( renderer , input , 'max' , state . max ( ) ) ;
387390 setOptionalAttribute ( renderer , input , 'min' , state . min ( ) ) ;
388391 }
389392
390- // TODO: https://github.com/orgs/angular/projects/60/views/1?pane=issue&itemId=131711472
391- // * cache this in `tNode.flags`.
392- if ( isTextInput ( input ) ) {
393+ if ( tNode . flags & TNodeFlags . isNativeTextControl ) {
393394 setOptionalAttribute ( renderer , input , 'maxLength' , state . maxLength ( ) ) ;
394395 setOptionalAttribute ( renderer , input , 'minLength' , state . minLength ( ) ) ;
395396 }
@@ -421,9 +422,9 @@ function isNumericInput(control: NativeControlElement) {
421422 * This is not the same as an input with `type="text"`, but rather any input that accepts
422423 * text-based input which includes numeric types.
423424 */
424- function isTextInput (
425+ function isTextControl (
425426 control : NativeControlElement ,
426- ) : control is HTMLInputElement | HTMLTextAreaElementNarrowed {
427+ ) : control is Exclude < NativeControlElement , HTMLSelectElement > {
427428 return ! ( control instanceof HTMLSelectElement ) ;
428429}
429430
0 commit comments