mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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
This commit is contained in:
committed by
Andrew Kushnir
parent
356dc4c952
commit
ab415f3d7f
@@ -10,12 +10,13 @@ import {bindingUpdated} from '../bindings';
|
||||
import {ɵCONTROL, ɵControl} from '../interfaces/control';
|
||||
import {ComponentDef} from '../interfaces/definition';
|
||||
import {InputFlags} from '../interfaces/input_flags';
|
||||
import {TNode, TNodeFlags} from '../interfaces/node';
|
||||
import {TElementNode, TNode, TNodeFlags, TNodeType} from '../interfaces/node';
|
||||
import {Renderer} from '../interfaces/renderer';
|
||||
import {SanitizerFn} from '../interfaces/sanitization';
|
||||
import {isComponentHost} from '../interfaces/type_checks';
|
||||
import {LView, RENDERER, TView} from '../interfaces/view';
|
||||
import {getCurrentTNode, getLView, getSelectedTNode, getTView, nextBindingIndex} from '../state';
|
||||
import {isNameOnlyAttributeMarker} from '../util/attrs_utils';
|
||||
import {getNativeByTNode} from '../util/view_utils';
|
||||
import {listenToOutput} from '../view/directive_outputs';
|
||||
import {listenToDomEvent, wrapListener} from '../view/listeners';
|
||||
@@ -131,12 +132,11 @@ function getControlDirectiveFirstCreatePass<T>(
|
||||
}
|
||||
}
|
||||
|
||||
const nativeElement = lView[tNode.index];
|
||||
if (isNativeControl(nativeElement)) {
|
||||
if (isNumericInput(nativeElement)) {
|
||||
if (isNativeControl(tNode)) {
|
||||
if (isNumericInput(tNode)) {
|
||||
tNode.flags |= TNodeFlags.isNativeNumericControl;
|
||||
}
|
||||
if (isTextControl(nativeElement)) {
|
||||
if (isTextControl(tNode)) {
|
||||
tNode.flags |= TNodeFlags.isNativeTextControl;
|
||||
}
|
||||
return control;
|
||||
@@ -259,12 +259,12 @@ interface HTMLTextAreaElementNarrowed extends HTMLTextAreaElement {
|
||||
*/
|
||||
type NativeControlElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElementNarrowed;
|
||||
|
||||
function isNativeControl(element: unknown): element is NativeControlElement {
|
||||
return (
|
||||
element instanceof HTMLInputElement ||
|
||||
element instanceof HTMLSelectElement ||
|
||||
element instanceof HTMLTextAreaElement
|
||||
);
|
||||
function isNativeControl(tNode: TNode): tNode is TElementNode {
|
||||
if (tNode.type !== TNodeType.Element) {
|
||||
return false;
|
||||
}
|
||||
const tagName = tNode.value;
|
||||
return tagName === 'input' || tagName === 'textarea' || tagName === 'select';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -402,17 +402,33 @@ function isDateOrNull(value: unknown): value is Date | null {
|
||||
}
|
||||
|
||||
/** Returns whether `control` has a numeric input type. */
|
||||
function isNumericInput(control: NativeControlElement) {
|
||||
switch (control.type) {
|
||||
case 'date':
|
||||
case 'datetime-local':
|
||||
case 'month':
|
||||
case 'number':
|
||||
case 'range':
|
||||
case 'time':
|
||||
case 'week':
|
||||
return true;
|
||||
function isNumericInput(tNode: TElementNode): boolean {
|
||||
if (!tNode.attrs || tNode.value !== 'input') {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < tNode.attrs.length; i += 2) {
|
||||
const name = tNode.attrs[i];
|
||||
|
||||
if (isNameOnlyAttributeMarker(name)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (name === 'type') {
|
||||
const value = tNode.attrs[i + 1];
|
||||
|
||||
return (
|
||||
value === 'date' ||
|
||||
value === 'datetime-local' ||
|
||||
value === 'month' ||
|
||||
value === 'number' ||
|
||||
value === 'range' ||
|
||||
value === 'time' ||
|
||||
value === 'week'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -422,10 +438,8 @@ function isNumericInput(control: NativeControlElement) {
|
||||
* This is not the same as an input with `type="text"`, but rather any input that accepts
|
||||
* text-based input which includes numeric types.
|
||||
*/
|
||||
function isTextControl(
|
||||
control: NativeControlElement,
|
||||
): control is Exclude<NativeControlElement, HTMLSelectElement> {
|
||||
return !(control instanceof HTMLSelectElement);
|
||||
function isTextControl(tNode: TElementNode): boolean {
|
||||
return tNode.value !== 'select';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
|
||||
import {
|
||||
Component,
|
||||
Directive,
|
||||
ElementRef,
|
||||
inject,
|
||||
Injector,
|
||||
input,
|
||||
inputBinding,
|
||||
@@ -17,6 +19,7 @@ import {
|
||||
signal,
|
||||
viewChild,
|
||||
viewChildren,
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {
|
||||
@@ -1079,6 +1082,39 @@ describe('field directive', () => {
|
||||
).toEqual(['manual disabled', 'schema disabled']);
|
||||
});
|
||||
|
||||
it('should bind to native control that has directive injecting ViewContainerRef', () => {
|
||||
@Directive({selector: 'input'})
|
||||
class InputDirective {
|
||||
vcr = inject(ViewContainerRef);
|
||||
}
|
||||
|
||||
@Component({
|
||||
imports: [Field, InputDirective],
|
||||
template: `<input [field]="f">`,
|
||||
})
|
||||
class TestCmp {
|
||||
f = form(signal('test'));
|
||||
}
|
||||
|
||||
const fix = act(() => TestBed.createComponent(TestCmp));
|
||||
const input = fix.nativeElement.firstChild as HTMLInputElement;
|
||||
const cmp = fix.componentInstance as TestCmp;
|
||||
|
||||
// Initial state
|
||||
expect(input.value).toBe('test');
|
||||
|
||||
// Model -> View
|
||||
act(() => cmp.f().value.set('testing'));
|
||||
expect(input.value).toBe('testing');
|
||||
|
||||
// View -> Model
|
||||
act(() => {
|
||||
input.value = 'typing';
|
||||
input.dispatchEvent(new Event('input'));
|
||||
});
|
||||
expect(cmp.f().value()).toBe('typing');
|
||||
});
|
||||
|
||||
describe('should work with different input types', () => {
|
||||
it('should sync string field with number type input', () => {
|
||||
@Component({
|
||||
|
||||
Reference in New Issue
Block a user