diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts index 8ed454f336..049da807ee 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts @@ -1174,7 +1174,10 @@ type TcbDirectiveInput = { function tcbGetDirectiveInputs( el: TmplAstElement | TmplAstTemplate, dir: TypeCheckableDirectiveMeta, tcb: Context, scope: Scope): TcbDirectiveInput[] { - const directiveInputs: TcbDirectiveInput[] = []; + // Only the first binding to a property is written. + // TODO(alxhub): produce an error for duplicate bindings to the same property, independently of + // this logic. + const directiveInputs = new Map(); // `dir.inputs` is an object map of field names on the directive class to property names. // This is backwards from what's needed to match bindings - a map of properties to field names // is desired. Invert `dir.inputs` into `propMatch` to create this map. @@ -1185,13 +1188,6 @@ function tcbGetDirectiveInputs( propMatch.set(inputs[key] as string, key); }); - // To determine which of directive's inputs are unset, we keep track of the set of field names - // that have not been seen yet. A field is removed from this set once a binding to it is found. - // Note: it's actually important here that `propMatch.values()` isn't used, as there can be - // multiple fields which share the same property name and only one of them will be listed as a - // value in `propMatch`. - const unsetFields = new Set(Object.keys(inputs)); - el.inputs.forEach(processAttribute); el.attributes.forEach(processAttribute); if (el instanceof TmplAstTemplate) { @@ -1199,11 +1195,16 @@ function tcbGetDirectiveInputs( } // Add unset directive inputs for each of the remaining unset fields. - for (const field of unsetFields) { - directiveInputs.push({type: 'unset', field}); + // Note: it's actually important here that `propMatch.values()` isn't used, as there can be + // multiple fields which share the same property name and only one of them will be listed as a + // value in `propMatch`. + for (const field of Object.keys(inputs)) { + if (!directiveInputs.has(field)) { + directiveInputs.set(field, {type: 'unset', field}); + } } - return directiveInputs; + return Array.from(directiveInputs.values()); /** * Add a binding expression to the map for each input/template attribute of the directive that has @@ -1226,8 +1227,10 @@ function tcbGetDirectiveInputs( } const field = propMatch.get(attr.name) !; - // Remove the field from the set of unseen fields, now that it's been assigned to. - unsetFields.delete(field); + // Skip the attribute if a previous binding also wrote to it. + if (directiveInputs.has(field)) { + return; + } let expr: ts.Expression; if (attr instanceof TmplAstBoundAttribute) { @@ -1238,7 +1241,7 @@ function tcbGetDirectiveInputs( expr = ts.createStringLiteral(attr.value); } - directiveInputs.push({ + directiveInputs.set(field, { type: 'binding', field: field, expression: expr, diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts index 6e35fb68f7..50ec363108 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts @@ -51,6 +51,19 @@ describe('type check blocks', () => { expect(tcb(TEMPLATE, DIRECTIVES)).toContain('"inputA": ("value")'); }); + it('should handle multiple bindings to the same property', () => { + const TEMPLATE = `
`; + const DIRECTIVES: TestDeclaration[] = [{ + type: 'directive', + name: 'DirA', + selector: '[dir-a]', + inputs: {inputA: 'inputA'}, + }]; + const block = tcb(TEMPLATE, DIRECTIVES); + expect(block).toContain('"inputA": (1)'); + expect(block).not.toContain('"inputA": (2)'); + }); + it('should handle empty bindings', () => { const TEMPLATE = `
`; const DIRECTIVES: TestDeclaration[] = [{