Skip to content

Commit ab415f3

Browse files
crisbetoAndrewKushnir
authored andcommitted
fix(core): control not recognized when input has directive injecting ViewContainerRef (#64368)
When a directive injects a `ViewContainerRef`, the runtime inserts a container that was throwing off the logic that recognizes native controls. These changes switch to check if the node is a native control through the `TNode`. This also makes it a bit less prone to breaking during SSR. Fixes #64362. PR Close #64368
1 parent 356dc4c commit ab415f3

File tree

2 files changed

+75
-25
lines changed

2 files changed

+75
-25
lines changed

packages/core/src/render3/instructions/control.ts

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ import {bindingUpdated} from '../bindings';
1010
import {ɵCONTROL, ɵControl} from '../interfaces/control';
1111
import {ComponentDef} from '../interfaces/definition';
1212
import {InputFlags} from '../interfaces/input_flags';
13-
import {TNode, TNodeFlags} from '../interfaces/node';
13+
import {TElementNode, TNode, TNodeFlags, TNodeType} from '../interfaces/node';
1414
import {Renderer} from '../interfaces/renderer';
1515
import {SanitizerFn} from '../interfaces/sanitization';
1616
import {isComponentHost} from '../interfaces/type_checks';
1717
import {LView, RENDERER, TView} from '../interfaces/view';
1818
import {getCurrentTNode, getLView, getSelectedTNode, getTView, nextBindingIndex} from '../state';
19+
import {isNameOnlyAttributeMarker} from '../util/attrs_utils';
1920
import {getNativeByTNode} from '../util/view_utils';
2021
import {listenToOutput} from '../view/directive_outputs';
2122
import {listenToDomEvent, wrapListener} from '../view/listeners';
@@ -131,12 +132,11 @@ function getControlDirectiveFirstCreatePass<T>(
131132
}
132133
}
133134

134-
const nativeElement = lView[tNode.index];
135-
if (isNativeControl(nativeElement)) {
136-
if (isNumericInput(nativeElement)) {
135+
if (isNativeControl(tNode)) {
136+
if (isNumericInput(tNode)) {
137137
tNode.flags |= TNodeFlags.isNativeNumericControl;
138138
}
139-
if (isTextControl(nativeElement)) {
139+
if (isTextControl(tNode)) {
140140
tNode.flags |= TNodeFlags.isNativeTextControl;
141141
}
142142
return control;
@@ -259,12 +259,12 @@ interface HTMLTextAreaElementNarrowed extends HTMLTextAreaElement {
259259
*/
260260
type NativeControlElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElementNarrowed;
261261

262-
function isNativeControl(element: unknown): element is NativeControlElement {
263-
return (
264-
element instanceof HTMLInputElement ||
265-
element instanceof HTMLSelectElement ||
266-
element instanceof HTMLTextAreaElement
267-
);
262+
function isNativeControl(tNode: TNode): tNode is TElementNode {
263+
if (tNode.type !== TNodeType.Element) {
264+
return false;
265+
}
266+
const tagName = tNode.value;
267+
return tagName === 'input' || tagName === 'textarea' || tagName === 'select';
268268
}
269269

270270
/**
@@ -402,17 +402,33 @@ function isDateOrNull(value: unknown): value is Date | null {
402402
}
403403

404404
/** Returns whether `control` has a numeric input type. */
405-
function isNumericInput(control: NativeControlElement) {
406-
switch (control.type) {
407-
case 'date':
408-
case 'datetime-local':
409-
case 'month':
410-
case 'number':
411-
case 'range':
412-
case 'time':
413-
case 'week':
414-
return true;
405+
function isNumericInput(tNode: TElementNode): boolean {
406+
if (!tNode.attrs || tNode.value !== 'input') {
407+
return false;
415408
}
409+
410+
for (let i = 0; i < tNode.attrs.length; i += 2) {
411+
const name = tNode.attrs[i];
412+
413+
if (isNameOnlyAttributeMarker(name)) {
414+
break;
415+
}
416+
417+
if (name === 'type') {
418+
const value = tNode.attrs[i + 1];
419+
420+
return (
421+
value === 'date' ||
422+
value === 'datetime-local' ||
423+
value === 'month' ||
424+
value === 'number' ||
425+
value === 'range' ||
426+
value === 'time' ||
427+
value === 'week'
428+
);
429+
}
430+
}
431+
416432
return false;
417433
}
418434

@@ -422,10 +438,8 @@ function isNumericInput(control: NativeControlElement) {
422438
* This is not the same as an input with `type="text"`, but rather any input that accepts
423439
* text-based input which includes numeric types.
424440
*/
425-
function isTextControl(
426-
control: NativeControlElement,
427-
): control is Exclude<NativeControlElement, HTMLSelectElement> {
428-
return !(control instanceof HTMLSelectElement);
441+
function isTextControl(tNode: TElementNode): boolean {
442+
return tNode.value !== 'select';
429443
}
430444

431445
/**

packages/forms/signals/test/web/field_directive.spec.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88

99
import {
1010
Component,
11+
Directive,
1112
ElementRef,
13+
inject,
1214
Injector,
1315
input,
1416
inputBinding,
@@ -17,6 +19,7 @@ import {
1719
signal,
1820
viewChild,
1921
viewChildren,
22+
ViewContainerRef,
2023
} from '@angular/core';
2124
import {TestBed} from '@angular/core/testing';
2225
import {
@@ -1079,6 +1082,39 @@ describe('field directive', () => {
10791082
).toEqual(['manual disabled', 'schema disabled']);
10801083
});
10811084

1085+
it('should bind to native control that has directive injecting ViewContainerRef', () => {
1086+
@Directive({selector: 'input'})
1087+
class InputDirective {
1088+
vcr = inject(ViewContainerRef);
1089+
}
1090+
1091+
@Component({
1092+
imports: [Field, InputDirective],
1093+
template: `<input [field]="f">`,
1094+
})
1095+
class TestCmp {
1096+
f = form(signal('test'));
1097+
}
1098+
1099+
const fix = act(() => TestBed.createComponent(TestCmp));
1100+
const input = fix.nativeElement.firstChild as HTMLInputElement;
1101+
const cmp = fix.componentInstance as TestCmp;
1102+
1103+
// Initial state
1104+
expect(input.value).toBe('test');
1105+
1106+
// Model -> View
1107+
act(() => cmp.f().value.set('testing'));
1108+
expect(input.value).toBe('testing');
1109+
1110+
// View -> Model
1111+
act(() => {
1112+
input.value = 'typing';
1113+
input.dispatchEvent(new Event('input'));
1114+
});
1115+
expect(cmp.f().value()).toBe('typing');
1116+
});
1117+
10821118
describe('should work with different input types', () => {
10831119
it('should sync string field with number type input', () => {
10841120
@Component({

0 commit comments

Comments
 (0)