diff --git a/packages/core/src/render3/instructions/shared.ts b/packages/core/src/render3/instructions/shared.ts index 39455e437e..828188fbd9 100644 --- a/packages/core/src/render3/instructions/shared.ts +++ b/packages/core/src/render3/instructions/shared.ts @@ -30,7 +30,7 @@ import {SanitizerFn} from '../interfaces/sanitization'; import {isComponentDef, isComponentHost, isContentQueryHost, isLContainer, isRootView} from '../interfaces/type_checks'; import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_COMPONENT_VIEW, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, InitPhaseState, INJECTOR, LView, LViewFlags, NEXT, PARENT, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, T_HOST, TData, TVIEW, TView, TViewType} from '../interfaces/view'; import {assertNodeOfPossibleTypes} from '../node_assert'; -import {isNodeMatchingSelectorList} from '../node_selector_matcher'; +import {isInlineTemplate, isNodeMatchingSelectorList} from '../node_selector_matcher'; import {enterView, getBindingsEnabled, getCheckNoChangesMode, getIsParent, getPreviousOrParentTNode, getSelectedIndex, getTView, leaveView, setBindingIndex, setBindingRootForHostBindings, setCheckNoChangesMode, setCurrentQueryIndex, setPreviousOrParentTNode, setSelectedIndex} from '../state'; import {NO_CHANGE} from '../tokens'; import {isAnimationProp, mergeHostAttrs} from '../util/attrs_utils'; @@ -900,8 +900,14 @@ function initializeInputAndOutputAliases(tView: TView, tNode: TNode): void { for (let i = start; i < end; i++) { const directiveDef = defs[i] as DirectiveDef; const directiveInputs = directiveDef.inputs; - inputsFromAttrs.push( - tNodeAttrs !== null ? generateInitialInputs(directiveInputs, tNodeAttrs) : null); + // Do not use unbound attributes as inputs to structural directives, since structural + // directive inputs can only be set using microsyntax (e.g. `
`). + // TODO(FW-1930): microsyntax expressions may also contain unbound/static attributes, which + // should be set for inline templates. + const initialInputs = (tNodeAttrs !== null && !isInlineTemplate(tNode)) ? + generateInitialInputs(directiveInputs, tNodeAttrs) : + null; + inputsFromAttrs.push(initialInputs); inputsStore = generatePropertyAliases(directiveInputs, i, inputsStore); outputsStore = generatePropertyAliases(directiveDef.outputs, i, outputsStore); } diff --git a/packages/core/src/render3/node_selector_matcher.ts b/packages/core/src/render3/node_selector_matcher.ts index 78e7aa78bb..1cf74b409d 100644 --- a/packages/core/src/render3/node_selector_matcher.ts +++ b/packages/core/src/render3/node_selector_matcher.ts @@ -56,6 +56,15 @@ function isCssClassMatching( return false; } +/** + * Checks whether the `tNode` represents an inline template (e.g. `*ngFor`). + * + * @param tNode current TNode + */ +export function isInlineTemplate(tNode: TNode): boolean { + return tNode.type === TNodeType.Container && tNode.tagName !== NG_TEMPLATE_SELECTOR; +} + /** * Function that checks whether a given tNode matches tag-based selector and has a valid type. * @@ -134,11 +143,9 @@ export function isNodeMatchingSelector( continue; } - const isInlineTemplate = - tNode.type == TNodeType.Container && tNode.tagName !== NG_TEMPLATE_SELECTOR; const attrName = (mode & SelectorFlags.CLASS) ? 'class' : current; const attrIndexInNode = - findAttrIndexInNode(attrName, nodeAttrs, isInlineTemplate, isProjectionMode); + findAttrIndexInNode(attrName, nodeAttrs, isInlineTemplate(tNode), isProjectionMode); if (attrIndexInNode === -1) { if (isPositive(mode)) return false; diff --git a/packages/core/test/acceptance/directive_spec.ts b/packages/core/test/acceptance/directive_spec.ts index f8174f08a3..21a6d87850 100644 --- a/packages/core/test/acceptance/directive_spec.ts +++ b/packages/core/test/acceptance/directive_spec.ts @@ -420,6 +420,93 @@ describe('directives', () => { expect(dirInstance!.dir).toBe('Hello'); }); + + it('should not set structural directive inputs from static element attrs', () => { + const dirInstances: StructuralDir[] = []; + + @Directive({selector: '[dir]'}) + class StructuralDir { + constructor() { + dirInstances.push(this); + } + @Input() dirOf!: number[]; + @Input() dirUnboundInput: any; + } + + @Component({ + template: ` + +
Some content
+ + + +
Some content
+
+ `, + }) + class App { + items: number[] = [1, 2, 3]; + } + + TestBed.configureTestingModule({ + declarations: [App, StructuralDir], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const [regularDir, desugaredDir] = dirInstances; + + // When directive is used as a structural one, the `dirUnboundInput` should not be treated as + // an input. + expect(regularDir.dirUnboundInput).toBe(undefined); + + // In de-sugared version the `dirUnboundInput` acts as a regular input, so it should be set + // to an empty string. + expect(desugaredDir.dirUnboundInput).toBe(''); + }); + + it('should not set structural directive inputs from element bindings', () => { + const dirInstances: StructuralDir[] = []; + + @Directive({selector: '[dir]'}) + class StructuralDir { + constructor() { + dirInstances.push(this); + } + @Input() dirOf!: number[]; + @Input() title: any; + } + + @Component({ + template: ` + +
Some content
+ + + +
Some content
+
+ `, + }) + class App { + items: number[] = [1, 2, 3]; + title: string = 'element title'; + } + + TestBed.configureTestingModule({ + declarations: [App, StructuralDir], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const [regularDir, desugaredDir] = dirInstances; + + // When directive is used as a structural one, the `title` should not be treated as an input. + expect(regularDir.title).toBe(undefined); + + // In de-sugared version the `title` acts as a regular input, so it should be set. + expect(desugaredDir.title).toBe('element title'); + }); }); describe('outputs', () => { diff --git a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json index 7470bbe21e..d3b1605e18 100644 --- a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json +++ b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json @@ -461,6 +461,9 @@ { "name": "isFactory" }, + { + "name": "isInlineTemplate" + }, { "name": "isLContainer" }, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index b68bdac5a8..b0a7253a54 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -860,6 +860,9 @@ { "name": "isInHostBindings" }, + { + "name": "isInlineTemplate" + }, { "name": "isJsObject" },