/** * @license * Copyright Google Inc. All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import {ConstantPool, R3DirectiveMetadata, WrappedNodeExpr, compileComponentFromMetadata as compileR3Component, compileDirectiveFromMetadata as compileR3Directive, jitExpression, makeBindingParser, parseHostBindings, parseTemplate} from '@angular/compiler'; import {Component, Directive, HostBinding, HostListener, Input, Output} from '../../metadata/directives'; import {componentNeedsResolution, maybeQueueResolutionOfComponentResources} from '../../metadata/resource_loading'; import {ViewEncapsulation} from '../../metadata/view'; import {Type} from '../../type'; import {stringify} from '../../util'; import {NG_COMPONENT_DEF, NG_DIRECTIVE_DEF} from '../fields'; import {angularCoreEnv} from './environment'; import {patchComponentDefWithScope, transitiveScopesFor} from './module'; import {getReflect, reflectDependencies} from './util'; type StringMap = { [key: string]: string }; /** * Compile an Angular component according to its decorator metadata, and patch the resulting * ngComponentDef onto the component type. * * Compilation may be asynchronous (due to the need to resolve URLs for the component template or * other resources, for example). In the event that compilation is not immediate, `compileComponent` * will enqueue resource resolution into a global queue and will fail to return the `ngComponentDef` * until the global queue has been resolved with a call to `resolveComponentResources`. */ export function compileComponent(type: Type, metadata: Component): void { let ngComponentDef: any = null; // Metadata may have resources which need to be resolved. maybeQueueResolutionOfComponentResources(metadata); Object.defineProperty(type, NG_COMPONENT_DEF, { get: () => { if (ngComponentDef === null) { if (componentNeedsResolution(metadata)) { const error = [`Component '${stringify(type)}' is not resolved:`]; if (metadata.templateUrl) { error.push(` - templateUrl: ${stringify(metadata.templateUrl)}`); } if (metadata.styleUrls && metadata.styleUrls.length) { error.push(` - styleUrls: ${JSON.stringify(metadata.styleUrls)}`); } error.push(`Did you run and wait for 'resolveComponentResources()'?`); throw new Error(error.join('\n')); } // The ConstantPool is a requirement of the JIT'er. const constantPool = new ConstantPool(); // Parse the template and check for errors. const template = parseTemplate( metadata.template !, `ng://${stringify(type)}/template.html`, { preserveWhitespaces: metadata.preserveWhitespaces || false, }, ''); if (template.errors !== undefined) { const errors = template.errors.map(err => err.toString()).join(', '); throw new Error( `Errors during JIT compilation of template for ${stringify(type)}: ${errors}`); } const animations = metadata.animations !== null ? new WrappedNodeExpr(metadata.animations) : null; // Compile the component metadata, including template, into an expression. // TODO(alxhub): implement inputs, outputs, queries, etc. const res = compileR3Component( { ...directiveMetadata(type, metadata), template, directives: new Map(), pipes: new Map(), viewQueries: [], wrapDirectivesAndPipesInClosure: false, styles: metadata.styles || [], encapsulation: metadata.encapsulation || ViewEncapsulation.Emulated, animations, viewProviders: metadata.viewProviders ? new WrappedNodeExpr(metadata.viewProviders) : null }, constantPool, makeBindingParser()); const preStatements = [...constantPool.statements, ...res.statements]; ngComponentDef = jitExpression( res.expression, angularCoreEnv, `ng://${type.name}/ngComponentDef.js`, preStatements); // If component compilation is async, then the @NgModule annotation which declares the // component may execute and set an ngSelectorScope property on the component type. This // allows the component to patch itself with directiveDefs from the module after it // finishes compiling. if (hasSelectorScope(type)) { const scopes = transitiveScopesFor(type.ngSelectorScope); patchComponentDefWithScope(ngComponentDef, scopes); } } return ngComponentDef; }, // Make the property configurable in dev mode to allow overriding in tests configurable: !!ngDevMode, }); } function hasSelectorScope(component: Type): component is Type& {ngSelectorScope: Type} { return (component as{ngSelectorScope?: any}).ngSelectorScope !== undefined; } /** * Compile an Angular directive according to its decorator metadata, and patch the resulting * ngDirectiveDef onto the component type. * * In the event that compilation is not immediate, `compileDirective` will return a `Promise` which * will resolve when compilation completes and the directive becomes usable. */ export function compileDirective(type: Type, directive: Directive): void { let ngDirectiveDef: any = null; Object.defineProperty(type, NG_DIRECTIVE_DEF, { get: () => { if (ngDirectiveDef === null) { const constantPool = new ConstantPool(); const sourceMapUrl = `ng://${type && type.name}/ngDirectiveDef.js`; const res = compileR3Directive( directiveMetadata(type, directive), constantPool, makeBindingParser()); const preStatements = [...constantPool.statements, ...res.statements]; ngDirectiveDef = jitExpression(res.expression, angularCoreEnv, sourceMapUrl, preStatements); } return ngDirectiveDef; }, // Make the property configurable in dev mode to allow overriding in tests configurable: !!ngDevMode, }); } export function extendsDirectlyFromObject(type: Type): boolean { return Object.getPrototypeOf(type.prototype) === Object.prototype; } /** * Extract the `R3DirectiveMetadata` for a particular directive (either a `Directive` or a * `Component`). */ function directiveMetadata(type: Type, metadata: Directive): R3DirectiveMetadata { // Reflect inputs and outputs. const propMetadata = getReflect().propMetadata(type); const host = extractHostBindings(metadata, propMetadata); const inputsFromMetadata = parseInputOutputs(metadata.inputs || []); const outputsFromMetadata = parseInputOutputs(metadata.outputs || []); const inputsFromType: StringMap = {}; const outputsFromType: StringMap = {}; for (const field in propMetadata) { if (propMetadata.hasOwnProperty(field)) { propMetadata[field].forEach(ann => { if (isInput(ann)) { inputsFromType[field] = ann.bindingPropertyName || field; } else if (isOutput(ann)) { outputsFromType[field] = ann.bindingPropertyName || field; } }); } } return { name: type.name, type: new WrappedNodeExpr(type), typeArgumentCount: 0, selector: metadata.selector !, deps: reflectDependencies(type), host, inputs: {...inputsFromMetadata, ...inputsFromType}, outputs: {...outputsFromMetadata, ...outputsFromType}, queries: [], lifecycle: { usesOnChanges: type.prototype.ngOnChanges !== undefined, }, typeSourceSpan: null !, usesInheritance: !extendsDirectlyFromObject(type), exportAs: metadata.exportAs || null, providers: metadata.providers ? new WrappedNodeExpr(metadata.providers) : null }; } 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 (const field in propMetadata) { if (propMetadata.hasOwnProperty(field)) { 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 { return value.ngMetadataName === 'Input'; } function isOutput(value: any): value is 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'; } function parseInputOutputs(values: string[]): StringMap { return values.reduce( (map, value) => { const [field, property] = value.split(',').map(piece => piece.trim()); map[field] = property || field; return map; }, {} as StringMap); }