feat(ivy): support host bindings in ngtsc (#24862)

This change adds support for host bindings to ngtsc, and parses them
both from decorators and from the metadata in the top-level annotation.

PR Close #24862
This commit is contained in:
Alex Rickabaugh 2018-07-18 09:50:16 -07:00 committed by Victor Berchet
parent 76f8f78920
commit 2e724ec68b
3 changed files with 123 additions and 7 deletions

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ConstantPool, Expression, R3DirectiveMetadata, R3QueryMetadata, WrappedNodeExpr, compileDirectiveFromMetadata, makeBindingParser} from '@angular/compiler';
import {ConstantPool, Expression, R3DirectiveMetadata, R3QueryMetadata, WrappedNodeExpr, compileDirectiveFromMetadata, makeBindingParser, parseHostBindings} from '@angular/compiler';
import * as ts from 'typescript';
import {ClassMember, ClassMemberKind, Decorator, Import, ReflectionHost} from '../../host';
@ -122,6 +122,8 @@ export function extractDirectiveMetadata(
selector = resolved;
}
const host = extractHostBindings(directive, decoratedElements, checker, coreModule);
// Determine if `ngOnChanges` is a lifecycle hook defined on the component.
const usesOnChanges = members.find(
member => member.isStatic && member.kind === ClassMemberKind.Method &&
@ -132,12 +134,7 @@ export function extractDirectiveMetadata(
clazz.heritageClauses.some(hc => hc.token === ts.SyntaxKind.ExtendsKeyword);
const metadata: R3DirectiveMetadata = {
name: clazz.name !.text,
deps: getConstructorDependencies(clazz, reflector, isCore),
host: {
attributes: {},
listeners: {},
properties: {},
},
deps: getConstructorDependencies(clazz, reflector, isCore), host,
lifecycle: {
usesOnChanges,
},
@ -325,6 +322,86 @@ function isPropertyTypeMember(member: ClassMember): boolean {
member.kind === ClassMemberKind.Property;
}
type StringMap = {
[key: string]: string
};
function extractHostBindings(
metadata: Map<string, ts.Expression>, members: ClassMember[], checker: ts.TypeChecker,
coreModule: string | undefined): {
attributes: StringMap,
listeners: StringMap,
properties: StringMap,
} {
let hostMetadata: StringMap = {};
if (metadata.has('host')) {
const hostMetaMap = staticallyResolve(metadata.get('host') !, checker);
if (!(hostMetaMap instanceof Map)) {
throw new Error(`Decorator host metadata must be an object`);
}
hostMetaMap.forEach((value, key) => {
if (typeof value !== 'string' || typeof key !== 'string') {
throw new Error(`Decorator host metadata must be a string -> string object, got ${value}`);
}
hostMetadata[key] = value;
});
}
const {attributes, listeners, properties, animations} = parseHostBindings(hostMetadata);
filterToMembersWithDecorator(members, 'HostBinding', coreModule)
.forEach(({member, decorators}) => {
decorators.forEach(decorator => {
let hostPropertyName: string = member.name;
if (decorator.args !== null && decorator.args.length > 0) {
if (decorator.args.length !== 1) {
throw new Error(`@HostBinding() can have at most one argument`);
}
const resolved = staticallyResolve(decorator.args[0], checker);
if (typeof resolved !== 'string') {
throw new Error(`@HostBinding()'s argument must be a string`);
}
hostPropertyName = resolved;
}
properties[hostPropertyName] = member.name;
});
});
filterToMembersWithDecorator(members, 'HostListener', coreModule)
.forEach(({member, decorators}) => {
decorators.forEach(decorator => {
let eventName: string = member.name;
let args: string[] = [];
if (decorator.args !== null && decorator.args.length > 0) {
if (decorator.args.length > 2) {
throw new Error(`@HostListener() can have at most two arguments`);
}
const resolved = staticallyResolve(decorator.args[0], checker);
if (typeof resolved !== 'string') {
throw new Error(`@HostListener()'s event name argument must be a string`);
}
eventName = resolved;
if (decorator.args.length === 2) {
const resolvedArgs = staticallyResolve(decorator.args[1], checker);
if (!isStringArrayOrDie(resolvedArgs, '@HostListener.args')) {
throw new Error(`@HostListener second argument must be a string array`);
}
args = resolvedArgs;
}
}
listeners[eventName] = `${member.name}(${args.join(',')})`;
});
});
return {attributes, properties, listeners};
}
const QUERY_TYPES = new Set([
'ContentChild',
'ContentChildren',

View File

@ -37,6 +37,8 @@ export const Optional = callableParamDecorator();
export const ContentChild = callablePropDecorator();
export const ContentChildren = callablePropDecorator();
export const HostBinding = callablePropDecorator();
export const HostListener = callablePropDecorator();
export const ViewChild = callablePropDecorator();
export const ViewChildren = callablePropDecorator();

View File

@ -444,4 +444,41 @@ describe('ngtsc behavioral tests', () => {
expect(jsContents).toContain(`i0.ɵQ(0, ["accessor"], true)`);
expect(jsContents).toContain(`i0.ɵQ(1, ["test1"], true)`);
});
it('should generate host bindings for directives', () => {
writeConfig();
write(`test.ts`, `
import {Component, HostBinding, HostListener, TemplateRef} from '@angular/core';
@Component({
selector: 'test',
template: 'Test',
host: {
'[attr.hello]': 'foo',
'(click)': 'onClick($event)',
'[prop]': 'bar',
},
})
class FooCmp {
onClick(event: any): void {}
@HostBinding('class.someclass')
get someClass(): boolean { return false; }
@HostListener('onChange', ['arg'])
onChange(event: any, arg: any): void {}
}
`);
const exitCode = main(['-p', basePath], errorSpy);
expect(errorSpy).not.toHaveBeenCalled();
expect(exitCode).toBe(0);
const jsContents = getContents('test.js');
expect(jsContents).toContain(`i0.ɵp(elIndex, "attr.hello", i0.ɵb(i0.ɵd(dirIndex).foo));`);
expect(jsContents).toContain(`i0.ɵp(elIndex, "prop", i0.ɵb(i0.ɵd(dirIndex).bar));`);
expect(jsContents)
.toContain('i0.ɵp(elIndex, "class.someclass", i0.ɵb(i0.ɵd(dirIndex).someClass))');
expect(jsContents).toContain('i0.ɵd(dirIndex).onClick($event)');
expect(jsContents).toContain('i0.ɵd(dirIndex).onChange(i0.ɵd(dirIndex).arg)');
});
});