feat(ivy): implement host bindings in JIT mode (#24479)
PR Close #24479
This commit is contained in:

committed by
Miško Hevery

parent
6d246d6c72
commit
f00ae516eb
@ -85,5 +85,5 @@ export {jitExpression} from './render3/r3_jit';
|
|||||||
export {R3DependencyMetadata, R3FactoryMetadata, R3ResolvedDependencyType} from './render3/r3_factory';
|
export {R3DependencyMetadata, R3FactoryMetadata, R3ResolvedDependencyType} from './render3/r3_factory';
|
||||||
export {compileNgModule, R3NgModuleMetadata} from './render3/r3_module_compiler';
|
export {compileNgModule, R3NgModuleMetadata} from './render3/r3_module_compiler';
|
||||||
export {makeBindingParser, parseTemplate} from './render3/view/template';
|
export {makeBindingParser, parseTemplate} from './render3/view/template';
|
||||||
export {compileComponentFromMetadata, compileDirectiveFromMetadata} from './render3/view/compiler';
|
export {compileComponentFromMetadata, compileDirectiveFromMetadata, parseHostBindings} from './render3/view/compiler';
|
||||||
// This file only reexports content of the `src` folder. Keep it that way.
|
// This file only reexports content of the `src` folder. Keep it that way.
|
@ -445,3 +445,45 @@ function typeMapToExpressionMap(
|
|||||||
([key, type]): [string, o.Expression] => [key, outputCtx.importExpr(type)]);
|
([key, type]): [string, o.Expression] => [key, outputCtx.importExpr(type)]);
|
||||||
return new Map(entries);
|
return new Map(entries);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const HOST_REG_EXP = /^(?:(?:\[([^\]]+)\])|(?:\(([^\)]+)\)))|(\@[-\w]+)$/;
|
||||||
|
|
||||||
|
// Represents the groups in the above regex.
|
||||||
|
const enum HostBindingGroup {
|
||||||
|
// group 1: "prop" from "[prop]"
|
||||||
|
Property = 1,
|
||||||
|
|
||||||
|
// group 2: "event" from "(event)"
|
||||||
|
Event = 2,
|
||||||
|
|
||||||
|
// group 3: "@trigger" from "@trigger"
|
||||||
|
Animation = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseHostBindings(host: {[key: string]: string}): {
|
||||||
|
attributes: {[key: string]: string},
|
||||||
|
listeners: {[key: string]: string},
|
||||||
|
properties: {[key: string]: string},
|
||||||
|
animations: {[key: string]: string},
|
||||||
|
} {
|
||||||
|
const attributes: {[key: string]: string} = {};
|
||||||
|
const listeners: {[key: string]: string} = {};
|
||||||
|
const properties: {[key: string]: string} = {};
|
||||||
|
const animations: {[key: string]: string} = {};
|
||||||
|
|
||||||
|
Object.keys(host).forEach(key => {
|
||||||
|
const value = host[key];
|
||||||
|
const matches = key.match(HOST_REG_EXP);
|
||||||
|
if (matches === null) {
|
||||||
|
attributes[key] = value;
|
||||||
|
} else if (matches[HostBindingGroup.Property] != null) {
|
||||||
|
properties[matches[HostBindingGroup.Property]] = value;
|
||||||
|
} else if (matches[HostBindingGroup.Event] != null) {
|
||||||
|
listeners[matches[HostBindingGroup.Event]] = value;
|
||||||
|
} else if (matches[HostBindingGroup.Animation] != null) {
|
||||||
|
animations[matches[HostBindingGroup.Animation]] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {attributes, listeners, properties, animations};
|
||||||
|
}
|
@ -221,7 +221,7 @@ export class BindingParser {
|
|||||||
|
|
||||||
private _parseBinding(value: string, isHostBinding: boolean, sourceSpan: ParseSourceSpan):
|
private _parseBinding(value: string, isHostBinding: boolean, sourceSpan: ParseSourceSpan):
|
||||||
ASTWithSource {
|
ASTWithSource {
|
||||||
const sourceInfo = sourceSpan.start.toString();
|
const sourceInfo = (sourceSpan && sourceSpan.start || '(unknown)').toString();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const ast = isHostBinding ?
|
const ast = isHostBinding ?
|
||||||
@ -343,7 +343,7 @@ export class BindingParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _parseAction(value: string, sourceSpan: ParseSourceSpan): ASTWithSource {
|
private _parseAction(value: string, sourceSpan: ParseSourceSpan): ASTWithSource {
|
||||||
const sourceInfo = sourceSpan.start.toString();
|
const sourceInfo = (sourceSpan && sourceSpan.start || '(unknown').toString();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const ast = this._exprParser.parseAction(value, sourceInfo, this._interpolationConfig);
|
const ast = this._exprParser.parseAction(value, sourceInfo, this._interpolationConfig);
|
||||||
|
@ -6,9 +6,9 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {ConstantPool, R3DirectiveMetadata, WrappedNodeExpr, compileComponentFromMetadata as compileR3Component, compileDirectiveFromMetadata as compileR3Directive, jitExpression, makeBindingParser, parseTemplate} from '@angular/compiler';
|
import {ConstantPool, R3DirectiveMetadata, WrappedNodeExpr, compileComponentFromMetadata as compileR3Component, compileDirectiveFromMetadata as compileR3Directive, jitExpression, makeBindingParser, parseHostBindings, parseTemplate} from '@angular/compiler';
|
||||||
|
|
||||||
import {Component, Directive, HostBinding, Input, Output} from '../../metadata/directives';
|
import {Component, Directive, HostBinding, HostListener, Input, Output} from '../../metadata/directives';
|
||||||
import {ReflectionCapabilities} from '../../reflection/reflection_capabilities';
|
import {ReflectionCapabilities} from '../../reflection/reflection_capabilities';
|
||||||
import {Type} from '../../type';
|
import {Type} from '../../type';
|
||||||
|
|
||||||
@ -19,6 +19,10 @@ import {getReflect, reflectDependencies} from './util';
|
|||||||
|
|
||||||
let _pendingPromises: Promise<void>[] = [];
|
let _pendingPromises: Promise<void>[] = [];
|
||||||
|
|
||||||
|
type StringMap = {
|
||||||
|
[key: string]: string
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compile an Angular component according to its decorator metadata, and patch the resulting
|
* Compile an Angular component according to its decorator metadata, and patch the resulting
|
||||||
* ngComponentDef onto the component type.
|
* ngComponentDef onto the component type.
|
||||||
@ -137,12 +141,14 @@ export function awaitCurrentlyCompilingComponents(): Promise<void> {
|
|||||||
*/
|
*/
|
||||||
function directiveMetadata(type: Type<any>, metadata: Directive): R3DirectiveMetadata {
|
function directiveMetadata(type: Type<any>, metadata: Directive): R3DirectiveMetadata {
|
||||||
// Reflect inputs and outputs.
|
// Reflect inputs and outputs.
|
||||||
const props = getReflect().propMetadata(type);
|
const propMetadata = getReflect().propMetadata(type);
|
||||||
const inputs: {[key: string]: string} = {};
|
const inputs: StringMap = {};
|
||||||
const outputs: {[key: string]: string} = {};
|
const outputs: StringMap = {};
|
||||||
|
|
||||||
for (let field in props) {
|
const host = extractHostBindings(metadata, propMetadata);
|
||||||
props[field].forEach(ann => {
|
|
||||||
|
for (let field in propMetadata) {
|
||||||
|
propMetadata[field].forEach(ann => {
|
||||||
if (isInput(ann)) {
|
if (isInput(ann)) {
|
||||||
inputs[field] = ann.bindingPropertyName || field;
|
inputs[field] = ann.bindingPropertyName || field;
|
||||||
} else if (isOutput(ann)) {
|
} else if (isOutput(ann)) {
|
||||||
@ -155,14 +161,7 @@ function directiveMetadata(type: Type<any>, metadata: Directive): R3DirectiveMet
|
|||||||
name: type.name,
|
name: type.name,
|
||||||
type: new WrappedNodeExpr(type),
|
type: new WrappedNodeExpr(type),
|
||||||
selector: metadata.selector !,
|
selector: metadata.selector !,
|
||||||
deps: reflectDependencies(type),
|
deps: reflectDependencies(type), host, inputs, outputs,
|
||||||
host: {
|
|
||||||
attributes: {},
|
|
||||||
listeners: {},
|
|
||||||
properties: {},
|
|
||||||
},
|
|
||||||
inputs,
|
|
||||||
outputs,
|
|
||||||
queries: [],
|
queries: [],
|
||||||
lifecycle: {
|
lifecycle: {
|
||||||
usesOnChanges: type.prototype.ngOnChanges !== undefined,
|
usesOnChanges: type.prototype.ngOnChanges !== undefined,
|
||||||
@ -171,6 +170,32 @@ function directiveMetadata(type: Type<any>, metadata: Directive): R3DirectiveMet
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractHostBindings(metadata: Directive, propMetadata: {[key: string]: any[]}): {
|
||||||
|
attributes: StringMap,
|
||||||
|
listeners: StringMap,
|
||||||
|
properties: StringMap,
|
||||||
|
} {
|
||||||
|
// First parse the declarations from the metadata.
|
||||||
|
const {attributes, listeners, properties, animations} = parseHostBindings(metadata.host || {});
|
||||||
|
|
||||||
|
if (Object.keys(animations).length > 0) {
|
||||||
|
throw new Error(`Animation bindings are as-of-yet unsupported in Ivy`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, loop over the properties of the object, looking for @HostBinding and @HostListener.
|
||||||
|
for (let field in propMetadata) {
|
||||||
|
propMetadata[field].forEach(ann => {
|
||||||
|
if (isHostBinding(ann)) {
|
||||||
|
properties[ann.hostPropertyName || field] = field;
|
||||||
|
} else if (isHostListener(ann)) {
|
||||||
|
listeners[ann.eventName || field] = `${field}(${(ann.args || []).join(',')})`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {attributes, listeners, properties};
|
||||||
|
}
|
||||||
|
|
||||||
function isInput(value: any): value is Input {
|
function isInput(value: any): value is Input {
|
||||||
return value.ngMetadataName === 'Input';
|
return value.ngMetadataName === 'Input';
|
||||||
}
|
}
|
||||||
@ -178,3 +203,11 @@ function isInput(value: any): value is Input {
|
|||||||
function isOutput(value: any): value is Output {
|
function isOutput(value: any): value is Output {
|
||||||
return value.ngMetadataName === 'Output';
|
return value.ngMetadataName === 'Output';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isHostBinding(value: any): value is HostBinding {
|
||||||
|
return value.ngMetadataName === 'HostBinding';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isHostListener(value: any): value is HostListener {
|
||||||
|
return value.ngMetadataName === 'HostListener';
|
||||||
|
}
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
import {Injectable} from '@angular/core/src/di/injectable';
|
import {Injectable} from '@angular/core/src/di/injectable';
|
||||||
import {inject, setCurrentInjector} from '@angular/core/src/di/injector';
|
import {inject, setCurrentInjector} from '@angular/core/src/di/injector';
|
||||||
import {ivyEnabled} from '@angular/core/src/ivy_switch';
|
import {ivyEnabled} from '@angular/core/src/ivy_switch';
|
||||||
import {Component} from '@angular/core/src/metadata/directives';
|
import {Component, HostBinding, HostListener} from '@angular/core/src/metadata/directives';
|
||||||
import {NgModule, NgModuleDef} from '@angular/core/src/metadata/ng_module';
|
import {NgModule, NgModuleDef} from '@angular/core/src/metadata/ng_module';
|
||||||
import {ComponentDef} from '@angular/core/src/render3/interfaces/definition';
|
import {ComponentDef} from '@angular/core/src/render3/interfaces/definition';
|
||||||
|
|
||||||
@ -161,6 +161,29 @@ ivyEnabled && describe('render3 jit', () => {
|
|||||||
expect(cmpDef.directiveDefs instanceof Function).toBe(true);
|
expect(cmpDef.directiveDefs instanceof Function).toBe(true);
|
||||||
expect((cmpDef.directiveDefs as Function)()).toEqual([cmpDef]);
|
expect((cmpDef.directiveDefs as Function)()).toEqual([cmpDef]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should add hostbindings and hostlisteners', () => {
|
||||||
|
@Component({
|
||||||
|
template: 'foo',
|
||||||
|
selector: 'foo',
|
||||||
|
host: {
|
||||||
|
'[class.red]': 'isRed',
|
||||||
|
'(click)': 'onClick()',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
class Cmp {
|
||||||
|
@HostBinding('class.green')
|
||||||
|
green: boolean = false;
|
||||||
|
|
||||||
|
@HostListener('change', ['$event'])
|
||||||
|
onChange(event: any): void {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cmpDef = (Cmp as any).ngComponentDef as ComponentDef<Cmp>;
|
||||||
|
|
||||||
|
expect(cmpDef.hostBindings).toBeDefined();
|
||||||
|
expect(cmpDef.hostBindings !.length).toBe(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('ensure at least one spec exists', () => {});
|
it('ensure at least one spec exists', () => {});
|
||||||
|
Reference in New Issue
Block a user