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:
parent
76f8f78920
commit
2e724ec68b
|
@ -6,7 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* 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 * as ts from 'typescript';
|
||||||
|
|
||||||
import {ClassMember, ClassMemberKind, Decorator, Import, ReflectionHost} from '../../host';
|
import {ClassMember, ClassMemberKind, Decorator, Import, ReflectionHost} from '../../host';
|
||||||
|
@ -122,6 +122,8 @@ export function extractDirectiveMetadata(
|
||||||
selector = resolved;
|
selector = resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const host = extractHostBindings(directive, decoratedElements, checker, coreModule);
|
||||||
|
|
||||||
// Determine if `ngOnChanges` is a lifecycle hook defined on the component.
|
// Determine if `ngOnChanges` is a lifecycle hook defined on the component.
|
||||||
const usesOnChanges = members.find(
|
const usesOnChanges = members.find(
|
||||||
member => member.isStatic && member.kind === ClassMemberKind.Method &&
|
member => member.isStatic && member.kind === ClassMemberKind.Method &&
|
||||||
|
@ -132,12 +134,7 @@ export function extractDirectiveMetadata(
|
||||||
clazz.heritageClauses.some(hc => hc.token === ts.SyntaxKind.ExtendsKeyword);
|
clazz.heritageClauses.some(hc => hc.token === ts.SyntaxKind.ExtendsKeyword);
|
||||||
const metadata: R3DirectiveMetadata = {
|
const metadata: R3DirectiveMetadata = {
|
||||||
name: clazz.name !.text,
|
name: clazz.name !.text,
|
||||||
deps: getConstructorDependencies(clazz, reflector, isCore),
|
deps: getConstructorDependencies(clazz, reflector, isCore), host,
|
||||||
host: {
|
|
||||||
attributes: {},
|
|
||||||
listeners: {},
|
|
||||||
properties: {},
|
|
||||||
},
|
|
||||||
lifecycle: {
|
lifecycle: {
|
||||||
usesOnChanges,
|
usesOnChanges,
|
||||||
},
|
},
|
||||||
|
@ -325,6 +322,86 @@ function isPropertyTypeMember(member: ClassMember): boolean {
|
||||||
member.kind === ClassMemberKind.Property;
|
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([
|
const QUERY_TYPES = new Set([
|
||||||
'ContentChild',
|
'ContentChild',
|
||||||
'ContentChildren',
|
'ContentChildren',
|
||||||
|
|
|
@ -37,6 +37,8 @@ export const Optional = callableParamDecorator();
|
||||||
|
|
||||||
export const ContentChild = callablePropDecorator();
|
export const ContentChild = callablePropDecorator();
|
||||||
export const ContentChildren = callablePropDecorator();
|
export const ContentChildren = callablePropDecorator();
|
||||||
|
export const HostBinding = callablePropDecorator();
|
||||||
|
export const HostListener = callablePropDecorator();
|
||||||
export const ViewChild = callablePropDecorator();
|
export const ViewChild = callablePropDecorator();
|
||||||
export const ViewChildren = callablePropDecorator();
|
export const ViewChildren = callablePropDecorator();
|
||||||
|
|
||||||
|
|
|
@ -444,4 +444,41 @@ describe('ngtsc behavioral tests', () => {
|
||||||
expect(jsContents).toContain(`i0.ɵQ(0, ["accessor"], true)`);
|
expect(jsContents).toContain(`i0.ɵQ(0, ["accessor"], true)`);
|
||||||
expect(jsContents).toContain(`i0.ɵQ(1, ["test1"], 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)');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue