diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/template_symbol_builder.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/template_symbol_builder.ts
index bf3df64c20..2cd6d2c746 100644
--- a/packages/compiler-cli/src/ngtsc/typecheck/src/template_symbol_builder.ts
+++ b/packages/compiler-cli/src/ngtsc/typecheck/src/template_symbol_builder.ts
@@ -176,41 +176,67 @@ export class SymbolBuilder {
}
const consumer = this.templateData.boundTarget.getConsumerOfBinding(eventBinding);
- if (consumer === null || consumer instanceof TmplAstTemplate ||
- consumer instanceof TmplAstElement) {
- // Bindings to element or template events produce `addEventListener` which
- // we cannot get the field for.
- return null;
- }
- const outputFieldAccess = TcbDirectiveOutputsOp.decodeOutputCallExpression(node);
- if (outputFieldAccess === null) {
+ if (consumer === null) {
return null;
}
- const tsSymbol =
- this.getTypeChecker().getSymbolAtLocation(outputFieldAccess.argumentExpression);
- if (tsSymbol === undefined) {
- return null;
+ if (consumer instanceof TmplAstTemplate || consumer instanceof TmplAstElement) {
+ if (!ts.isPropertyAccessExpression(node.expression) ||
+ node.expression.name.text !== 'addEventListener') {
+ return null;
+ }
+
+ const addEventListener = node.expression.name;
+ const tsSymbol = this.getTypeChecker().getSymbolAtLocation(addEventListener);
+ const tsType = this.getTypeChecker().getTypeAtLocation(addEventListener);
+ const positionInShimFile = this.getShimPositionForNode(addEventListener);
+ const target = this.getSymbol(consumer);
+
+ if (target === null || tsSymbol === undefined) {
+ return null;
+ }
+
+ return {
+ kind: SymbolKind.Output,
+ bindings: [{
+ kind: SymbolKind.Binding,
+ tsSymbol,
+ tsType,
+ target,
+ shimLocation: {shimPath: this.shimPath, positionInShimFile},
+ }],
+ };
+ } else {
+ const outputFieldAccess = TcbDirectiveOutputsOp.decodeOutputCallExpression(node);
+ if (outputFieldAccess === null) {
+ return null;
+ }
+
+ const tsSymbol =
+ this.getTypeChecker().getSymbolAtLocation(outputFieldAccess.argumentExpression);
+ if (tsSymbol === undefined) {
+ return null;
+ }
+
+
+ const target = this.getDirectiveSymbolForAccessExpression(outputFieldAccess, consumer);
+ if (target === null) {
+ return null;
+ }
+
+ const positionInShimFile = this.getShimPositionForNode(outputFieldAccess);
+ const tsType = this.getTypeChecker().getTypeAtLocation(node);
+ return {
+ kind: SymbolKind.Output,
+ bindings: [{
+ kind: SymbolKind.Binding,
+ tsSymbol,
+ tsType,
+ target,
+ shimLocation: {shimPath: this.shimPath, positionInShimFile},
+ }],
+ };
}
-
-
- const target = this.getDirectiveSymbolForAccessExpression(outputFieldAccess, consumer);
- if (target === null) {
- return null;
- }
-
- const positionInShimFile = this.getShimPositionForNode(outputFieldAccess);
- const tsType = this.getTypeChecker().getTypeAtLocation(node);
- return {
- kind: SymbolKind.Output,
- bindings: [{
- kind: SymbolKind.Binding,
- tsSymbol,
- tsType,
- target,
- shimLocation: {shimPath: this.shimPath, positionInShimFile},
- }],
- };
}
private getSymbolOfInputBinding(binding: TmplAstBoundAttribute|
diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts
index 464282fae3..d39489714a 100644
--- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts
+++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts
@@ -1247,38 +1247,29 @@ runInEachFileSystem(() => {
.toEqual('TestDir');
});
- it('returns empty list when binding does not match any directive output', () => {
- const fileName = absoluteFrom('/main.ts');
- const dirFile = absoluteFrom('/dir.ts');
- const {program, templateTypeChecker} = setup([
- {
- fileName,
- templates: {'Cmp': `
`},
- declarations: [
- {
- name: 'TestDir',
- selector: '[dir]',
- file: dirFile,
- type: 'directive',
- outputs: {outputA: 'outputA'},
- },
- ]
- },
- {
- fileName: dirFile,
- source: `export class TestDir {outputA!: EventEmitter;}`,
- templates: {},
- }
- ]);
- const sf = getSourceFileOrError(program, fileName);
- const cmp = getClass(sf, 'Cmp');
+ it('returns addEventListener binding to native element when no match to any directive output',
+ () => {
+ const fileName = absoluteFrom('/main.ts');
+ const {program, templateTypeChecker} = setup([
+ {
+ fileName,
+ templates: {'Cmp': `
`},
+ },
+ ]);
+ const sf = getSourceFileOrError(program, fileName);
+ const cmp = getClass(sf, 'Cmp');
- const nodes = templateTypeChecker.getTemplate(cmp)!;
+ const nodes = templateTypeChecker.getTemplate(cmp)!;
- const outputABinding = (nodes[0] as TmplAstElement).outputs[0];
- const symbol = templateTypeChecker.getSymbolOfNode(outputABinding, cmp);
- expect(symbol).toBeNull();
- });
+ const outputABinding = (nodes[0] as TmplAstElement).outputs[0];
+ const symbol = templateTypeChecker.getSymbolOfNode(outputABinding, cmp)!;
+ assertOutputBindingSymbol(symbol);
+ expect(program.getTypeChecker().symbolToString(symbol.bindings[0].tsSymbol!))
+ .toEqual('addEventListener');
+
+ const eventSymbol = templateTypeChecker.getSymbolOfNode(outputABinding.handler, cmp)!;
+ assertExpressionSymbol(eventSymbol);
+ });
it('returns empty list when checkTypeOfOutputEvents is false', () => {
const fileName = absoluteFrom('/main.ts');
diff --git a/packages/language-service/ivy/test/legacy/definitions_spec.ts b/packages/language-service/ivy/test/legacy/definitions_spec.ts
index 58bfa993df..67a4953419 100644
--- a/packages/language-service/ivy/test/legacy/definitions_spec.ts
+++ b/packages/language-service/ivy/test/legacy/definitions_spec.ts
@@ -228,6 +228,18 @@ describe('definitions', () => {
expect(directiveDef.textSpan).toEqual('EventSelectorDirective');
expect(directiveDef.contextSpan).toContain('export class EventSelectorDirective');
});
+
+ it('should work for $event from native element', () => {
+ const definitions = getDefinitionsAndAssertBoundSpan({
+ templateOverride: `
`,
+ expectedSpanText: 'click',
+ });
+ expect(definitions!.length).toEqual(1);
+ expect(definitions[0].textSpan).toEqual('addEventListener');
+ expect(definitions[0].contextSpan)
+ .toContain('addEventListener');
+ expect(definitions[0].fileName).toContain('lib.dom.d.ts');
+ });
});
});
diff --git a/packages/language-service/ivy/test/legacy/type_definitions_spec.ts b/packages/language-service/ivy/test/legacy/type_definitions_spec.ts
index 1121460873..7b1c234469 100644
--- a/packages/language-service/ivy/test/legacy/type_definitions_spec.ts
+++ b/packages/language-service/ivy/test/legacy/type_definitions_spec.ts
@@ -32,7 +32,7 @@ describe('type definitions', () => {
describe('elements', () => {
it('should work for native elements', () => {
- const defs = getTypeDefinitionsAndAssertBoundSpan({
+ const defs = getTypeDefinitions({
templateOverride: ``,
});
expect(defs.length).toEqual(2);
@@ -42,7 +42,7 @@ describe('type definitions', () => {
});
it('should return directives which match the element tag', () => {
- const defs = getTypeDefinitionsAndAssertBoundSpan({
+ const defs = getTypeDefinitions({
templateOverride: ``,
});
expect(defs.length).toEqual(3);
@@ -63,7 +63,7 @@ describe('type definitions', () => {
describe('directives', () => {
it('should work for directives', () => {
- const definitions = getTypeDefinitionsAndAssertBoundSpan({
+ const definitions = getTypeDefinitions({
templateOverride: `
`,
});
expect(definitions.length).toEqual(1);
@@ -73,7 +73,7 @@ describe('type definitions', () => {
});
it('should work for components', () => {
- const definitions = getTypeDefinitionsAndAssertBoundSpan({
+ const definitions = getTypeDefinitions({
templateOverride: ``,
});
expect(definitions.length).toEqual(1);
@@ -82,7 +82,7 @@ describe('type definitions', () => {
});
it('should work for structural directives', () => {
- const definitions = getTypeDefinitionsAndAssertBoundSpan({
+ const definitions = getTypeDefinitions({
templateOverride: `
`,
});
expect(definitions.length).toEqual(1);
@@ -94,12 +94,12 @@ describe('type definitions', () => {
});
it('should work for directives with compound selectors', () => {
- let defs = getTypeDefinitionsAndAssertBoundSpan({
+ let defs = getTypeDefinitions({
templateOverride: ` `,
});
expect(defs.length).toEqual(1);
expect(defs[0].contextSpan).toContain('export class CompoundCustomButtonDirective');
- defs = getTypeDefinitionsAndAssertBoundSpan({
+ defs = getTypeDefinitions({
templateOverride: ` `,
});
expect(defs.length).toEqual(1);
@@ -110,7 +110,7 @@ describe('type definitions', () => {
describe('bindings', () => {
describe('inputs', () => {
it('should return something for input providers with non-primitive types', () => {
- const defs = getTypeDefinitionsAndAssertBoundSpan({
+ const defs = getTypeDefinitions({
templateOverride: ` `,
});
expect(defs.length).toEqual(1);
@@ -118,7 +118,7 @@ describe('type definitions', () => {
});
it('should work for structural directive inputs ngForTrackBy', () => {
- const definitions = getTypeDefinitionsAndAssertBoundSpan({
+ const definitions = getTypeDefinitions({
templateOverride: `
`,
});
expect(definitions!.length).toEqual(1);
@@ -129,7 +129,7 @@ describe('type definitions', () => {
});
it('should work for structural directive inputs ngForOf', () => {
- const definitions = getTypeDefinitionsAndAssertBoundSpan({
+ const definitions = getTypeDefinitions({
templateOverride: `
`,
});
// In addition to all the array defs, this will also return the NgForOf def because the
@@ -140,7 +140,7 @@ describe('type definitions', () => {
});
it('should return nothing for two-way binding providers', () => {
- const definitions = getTypeDefinitionsAndAssertBoundSpan({
+ const definitions = getTypeDefinitions({
templateOverride: ` `,
});
// TODO(atscott): This should actually return EventEmitter type but we only match the input
@@ -151,7 +151,7 @@ describe('type definitions', () => {
describe('outputs', () => {
it('should work for event providers', () => {
- const definitions = getTypeDefinitionsAndAssertBoundSpan({
+ const definitions = getTypeDefinitions({
templateOverride: ` `,
});
expect(definitions!.length).toEqual(2);
@@ -167,7 +167,7 @@ describe('type definitions', () => {
});
it('should return the directive when the event is part of the selector', () => {
- const definitions = getTypeDefinitionsAndAssertBoundSpan({
+ const definitions = getTypeDefinitions({
templateOverride: `
`,
});
expect(definitions!.length).toEqual(3);
@@ -176,12 +176,24 @@ describe('type definitions', () => {
const directiveDef = definitions[2];
expect(directiveDef.contextSpan).toContain('export class EventSelectorDirective');
});
+
+ it('should work for native event outputs', () => {
+ const definitions = getTypeDefinitions({
+ templateOverride: `
`,
+ });
+ expect(definitions!.length).toEqual(1);
+ expect(definitions[0].textSpan).toEqual('addEventListener');
+ expect(definitions[0].contextSpan)
+ .toEqual(
+ 'addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;');
+ expect(definitions[0].fileName).toContain('lib.dom.d.ts');
+ });
});
});
describe('references', () => {
it('should work for element references', () => {
- const defs = getTypeDefinitionsAndAssertBoundSpan({
+ const defs = getTypeDefinitions({
templateOverride: `
{{char¦t}}`,
});
expect(defs.length).toEqual(2);
@@ -190,7 +202,7 @@ describe('type definitions', () => {
});
it('should work for directive references', () => {
- const defs = getTypeDefinitionsAndAssertBoundSpan({
+ const defs = getTypeDefinitions({
templateOverride: `
`,
});
expect(defs.length).toEqual(1);
@@ -201,7 +213,7 @@ describe('type definitions', () => {
describe('variables', () => {
it('should work for array members', () => {
- const definitions = getTypeDefinitionsAndAssertBoundSpan({
+ const definitions = getTypeDefinitions({
templateOverride: `{{her¦o}}
`,
});
expect(definitions!.length).toEqual(1);
@@ -214,7 +226,7 @@ describe('type definitions', () => {
describe('pipes', () => {
it('should work for pipes', () => {
const templateOverride = `The hero's birthday is {{birthday | da¦te: "MM/dd/yy"}}
`;
- const definitions = getTypeDefinitionsAndAssertBoundSpan({
+ const definitions = getTypeDefinitions({
templateOverride,
});
expect(definitions!.length).toEqual(1);
@@ -227,7 +239,7 @@ describe('type definitions', () => {
describe('expressions', () => {
it('should return nothing for primitives', () => {
- const definitions = getTypeDefinitionsAndAssertBoundSpan({
+ const definitions = getTypeDefinitions({
templateOverride: `{{ tit¦le }}
`,
});
expect(definitions!.length).toEqual(0);
@@ -236,7 +248,7 @@ describe('type definitions', () => {
// TODO(atscott): Investigate why this returns nothing in the test environment. This actually
// works in the extension.
xit('should work for functions on primitives', () => {
- const definitions = getTypeDefinitionsAndAssertBoundSpan({
+ const definitions = getTypeDefinitions({
templateOverride: `{{ title.toLower¦case() }}
`,
});
expect(definitions!.length).toEqual(1);
@@ -245,7 +257,7 @@ describe('type definitions', () => {
});
it('should work for accessed property reads', () => {
- const definitions = getTypeDefinitionsAndAssertBoundSpan({
+ const definitions = getTypeDefinitions({
templateOverride: `{{heroes[0].addre¦ss}}
`,
});
expect(definitions!.length).toEqual(1);
@@ -256,7 +268,7 @@ describe('type definitions', () => {
});
it('should work for $event', () => {
- const definitions = getTypeDefinitionsAndAssertBoundSpan({
+ const definitions = getTypeDefinitions({
templateOverride: ` `,
});
expect(definitions!.length).toEqual(2);
@@ -269,7 +281,7 @@ describe('type definitions', () => {
});
it('should work for method calls', () => {
- const definitions = getTypeDefinitionsAndAssertBoundSpan({
+ const definitions = getTypeDefinitions({
templateOverride: `
`,
});
expect(definitions!.length).toEqual(1);
@@ -280,7 +292,7 @@ describe('type definitions', () => {
});
it('should work for accessed properties in writes', () => {
- const definitions = getTypeDefinitionsAndAssertBoundSpan({
+ const definitions = getTypeDefinitions({
templateOverride: `
`,
});
expect(definitions!.length).toEqual(1);
@@ -291,21 +303,21 @@ describe('type definitions', () => {
});
it('should work for variables in structural directives', () => {
- const definitions = getTypeDefinitionsAndAssertBoundSpan({
+ const definitions = getTypeDefinitions({
templateOverride: `
`,
});
expectAllDefinitions(definitions, new Set(['Array']), possibleArrayDefFiles);
});
it('should work for uses of members in structural directives', () => {
- const definitions = getTypeDefinitionsAndAssertBoundSpan({
+ const definitions = getTypeDefinitions({
templateOverride: `{{her¦oes2}}
`,
});
expectAllDefinitions(definitions, new Set(['Array']), possibleArrayDefFiles);
});
it('should work for members in structural directives', () => {
- const definitions = getTypeDefinitionsAndAssertBoundSpan({
+ const definitions = getTypeDefinitions({
templateOverride: `
`,
});
expectAllDefinitions(definitions, new Set(['Array']), possibleArrayDefFiles);
@@ -319,7 +331,7 @@ describe('type definitions', () => {
});
});
- function getTypeDefinitionsAndAssertBoundSpan({templateOverride}: {templateOverride: string}):
+ function getTypeDefinitions({templateOverride}: {templateOverride: string}):
HumanizedDefinitionInfo[] {
const {position} = service.overwriteInlineTemplate(APP_COMPONENT, templateOverride);
const defs = ngLS.getTypeDefinitionAtPosition(APP_COMPONENT, position);
diff --git a/packages/language-service/ivy/test/quick_info_spec.ts b/packages/language-service/ivy/test/quick_info_spec.ts
index 45bc6b3eda..0a863ecd09 100644
--- a/packages/language-service/ivy/test/quick_info_spec.ts
+++ b/packages/language-service/ivy/test/quick_info_spec.ts
@@ -309,6 +309,25 @@ describe('quick info', () => {
expectedDisplayString: '(reference) chart: HTMLDivElement'
});
});
+
+ it('should work for $event from native element', () => {
+ expectQuickInfo({
+ templateOverride: `
`,
+ expectedSpanText: '$event',
+ expectedDisplayString: '(parameter) $event: MouseEvent'
+ });
+ });
+
+ it('should work for click output from native element', () => {
+ expectQuickInfo({
+ templateOverride: `
`,
+ expectedSpanText: 'click',
+ expectedDisplayString:
+ '(event) HTMLDivElement.addEventListener<"click">(type: "click", ' +
+ 'listener: (this: HTMLDivElement, ev: MouseEvent) => any, ' +
+ 'options?: boolean | AddEventListenerOptions | undefined): void (+1 overload)'
+ });
+ });
});
describe('variables', () => {