angular/packages/core/src/render3/definition.ts
Miško Hevery 2961bf06c6 refactor(ivy): move hostVars/hostAttrs from instruction to DirectiveDef (#34683)
This change moves information from instructions to declarative position:
- `ɵɵallocHostVars(vars)` => `DirectiveDef.hostVars`
- `ɵɵelementHostAttrs(attrs)` => `DirectiveDef.hostAttrs`

When merging directives it is necessary to know about `hostVars` and `hostAttrs`. Before this change the information was stored in the `hostBindings` function. This was problematic, because in order to get to the information the `hostBindings` would have to be executed. In order for `hostBindings` to be executed the directives would have to be instantiated. This means that the directive instantiation would happen before we had knowledge about the `hostAttrs` and as a result the directive could observe in the constructor that not all of the `hostAttrs` have been applied. This further complicates the runtime as we have to apply `hostAttrs` in parts over many invocations.

`ɵɵallocHostVars` was unnecessarily complicated because it would have to update the `LView` (and Blueprint) while existing directives are already executing. By moving it out of `hostBindings` function we can access it statically and we can create correct `LView` (and Blueprint) in a single pass.

This change only changes how the instructions are generated, but does not change the runtime much. (We cheat by emulating the old behavior by calling `ɵɵallocHostVars` and `ɵɵelementHostAttrs`) Subsequent change will refactor the runtime to take advantage of the static information.

PR Close #34683
2020-01-24 12:22:10 -08:00

771 lines
24 KiB
TypeScript

/**
* @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 {ChangeDetectionStrategy} from '../change_detection/constants';
import {Mutable, Type} from '../interface/type';
import {NgModuleDef} from '../metadata/ng_module';
import {SchemaMetadata} from '../metadata/schema';
import {ViewEncapsulation} from '../metadata/view';
import {noSideEffects} from '../util/closure';
import {initNgDevMode} from '../util/ng_dev_mode';
import {stringify} from '../util/stringify';
import {EMPTY_ARRAY, EMPTY_OBJ} from './empty';
import {NG_COMP_DEF, NG_DIR_DEF, NG_FACTORY_DEF, NG_LOC_ID_DEF, NG_MOD_DEF, NG_PIPE_DEF} from './fields';
import {ComponentDef, ComponentDefFeature, ComponentTemplate, ComponentType, ContentQueriesFunction, DirectiveDef, DirectiveDefFeature, DirectiveTypesOrFactory, FactoryFn, HostBindingsFunction, PipeDef, PipeType, PipeTypesOrFactory, ViewQueriesFunction} from './interfaces/definition';
import {AttributeMarker, TAttributes, TConstants} from './interfaces/node';
import {CssSelectorList, SelectorFlags} from './interfaces/projection';
import {NgModuleType} from './ng_module_ref';
let _renderCompCount = 0;
// While these types are unused here, they are required so that types don't
// get resolved lazily. see: https://github.com/Microsoft/web-build-tools/issues/1050
type _web_build_tools_issue_1050_SelectorFlags = SelectorFlags;
type _web_build_tools_issue_1050_AttributeMarker = AttributeMarker;
/**
* Create a component definition object.
*
*
* # Example
* ```
* class MyDirective {
* // Generated by Angular Template Compiler
* // [Symbol] syntax will not be supported by TypeScript until v2.7
* static ɵcmp = defineComponent({
* ...
* });
* }
* ```
* @codeGenApi
*/
export function ɵɵdefineComponent<T>(componentDefinition: {
/**
* Directive type, needed to configure the injector.
*/
type: Type<T>;
/** The selectors that will be used to match nodes to this component. */
selectors?: CssSelectorList;
/**
* The number of nodes, local refs, and pipes in this component template.
*
* Used to calculate the length of this component's LView array, so we
* can pre-fill the array and set the binding start index.
*/
// TODO(kara): remove queries from this count
decls: number;
/**
* The number of bindings in this component template (including pure fn bindings).
*
* Used to calculate the length of this component's LView array, so we
* can pre-fill the array and set the host binding start index.
*/
vars: number;
/**
* A map of input names.
*
* The format is in: `{[actualPropertyName: string]:(string|[string, string])}`.
*
* Given:
* ```
* class MyComponent {
* @Input()
* publicInput1: string;
*
* @Input('publicInput2')
* declaredInput2: string;
* }
* ```
*
* is described as:
* ```
* {
* publicInput1: 'publicInput1',
* declaredInput2: ['publicInput2', 'declaredInput2'],
* }
* ```
*
* Which the minifier may translate to:
* ```
* {
* minifiedPublicInput1: 'publicInput1',
* minifiedDeclaredInput2: ['publicInput2', 'declaredInput2'],
* }
* ```
*
* This allows the render to re-construct the minified, public, and declared names
* of properties.
*
* NOTE:
* - Because declared and public name are usually same we only generate the array
* `['public', 'declared']` format when they differ.
* - The reason why this API and `outputs` API is not the same is that `NgOnChanges` has
* inconsistent behavior in that it uses declared names rather than minified or public. For
* this reason `NgOnChanges` will be deprecated and removed in future version and this
* API will be simplified to be consistent with `output`.
*/
inputs?: {[P in keyof T]?: string | [string, string]};
/**
* A map of output names.
*
* The format is in: `{[actualPropertyName: string]:string}`.
*
* Which the minifier may translate to: `{[minifiedPropertyName: string]:string}`.
*
* This allows the render to re-construct the minified and non-minified names
* of properties.
*/
outputs?: {[P in keyof T]?: string};
/**
* Function executed by the parent template to allow child directive to apply host bindings.
*/
hostBindings?: HostBindingsFunction<T>;
/**
* The number of bindings in this directive `hostBindings` (including pure fn bindings).
*
* Used to calculate the length of the component's LView array, so we
* can pre-fill the array and set the host binding start index.
*/
hostVars?: number;
/**
* Assign static attribute values to a host element.
*
* This property will assign static attribute values as well as class and style
* values to a host element. Since attribute values can consist of different types of values, the
* `hostAttrs` array must include the values in the following format:
*
* attrs = [
* // static attributes (like `title`, `name`, `id`...)
* attr1, value1, attr2, value,
*
* // a single namespace value (like `x:id`)
* NAMESPACE_MARKER, namespaceUri1, name1, value1,
*
* // another single namespace value (like `x:name`)
* NAMESPACE_MARKER, namespaceUri2, name2, value2,
*
* // a series of CSS classes that will be applied to the element (no spaces)
* CLASSES_MARKER, class1, class2, class3,
*
* // a series of CSS styles (property + value) that will be applied to the element
* STYLES_MARKER, prop1, value1, prop2, value2
* ]
*
* All non-class and non-style attributes must be defined at the start of the list
* first before all class and style values are set. When there is a change in value
* type (like when classes and styles are introduced) a marker must be used to separate
* the entries. The marker values themselves are set via entries found in the
* [AttributeMarker] enum.
*/
hostAttrs?: TAttributes;
/**
* Function to create instances of content queries associated with a given directive.
*/
contentQueries?: ContentQueriesFunction<T>;
/**
* Defines the name that can be used in the template to assign this directive to a variable.
*
* See: {@link Directive.exportAs}
*/
exportAs?: string[];
/**
* Template function use for rendering DOM.
*
* This function has following structure.
*
* ```
* function Template<T>(ctx:T, creationMode: boolean) {
* if (creationMode) {
* // Contains creation mode instructions.
* }
* // Contains binding update instructions
* }
* ```
*
* Common instructions are:
* Creation mode instructions:
* - `elementStart`, `elementEnd`
* - `text`
* - `container`
* - `listener`
*
* Binding update instructions:
* - `bind`
* - `elementAttribute`
* - `elementProperty`
* - `elementClass`
* - `elementStyle`
*
*/
template: ComponentTemplate<T>;
/**
* Constants for the nodes in the component's view.
* Includes attribute arrays, local definition arrays etc.
*/
consts?: TConstants;
/**
* An array of `ngContent[selector]` values that were found in the template.
*/
ngContentSelectors?: string[];
/**
* Additional set of instructions specific to view query processing. This could be seen as a
* set of instruction to be inserted into the template function.
*
* Query-related instructions need to be pulled out to a specific function as a timing of
* execution is different as compared to all other instructions (after change detection hooks but
* before view hooks).
*/
viewQuery?: ViewQueriesFunction<T>| null;
/**
* A list of optional features to apply.
*
* See: {@link NgOnChangesFeature}, {@link ProvidersFeature}
*/
features?: ComponentDefFeature[];
/**
* Defines template and style encapsulation options available for Component's {@link Component}.
*/
encapsulation?: ViewEncapsulation;
/**
* Defines arbitrary developer-defined data to be stored on a renderer instance.
* This is useful for renderers that delegate to other renderers.
*
* see: animation
*/
data?: {[kind: string]: any};
/**
* A set of styles that the component needs to be present for component to render correctly.
*/
styles?: string[];
/**
* The strategy that the default change detector uses to detect changes.
* When set, takes effect the next time change detection is triggered.
*/
changeDetection?: ChangeDetectionStrategy;
/**
* Registry of directives and components that may be found in this component's view.
*
* The property is either an array of `DirectiveDef`s or a function which returns the array of
* `DirectiveDef`s. The function is necessary to be able to support forward declarations.
*/
directives?: DirectiveTypesOrFactory | null;
/**
* Registry of pipes that may be found in this component's view.
*
* The property is either an array of `PipeDefs`s or a function which returns the array of
* `PipeDefs`s. The function is necessary to be able to support forward declarations.
*/
pipes?: PipeTypesOrFactory | null;
/**
* The set of schemas that declare elements to be allowed in the component's template.
*/
schemas?: SchemaMetadata[] | null;
}): never {
// Initialize ngDevMode. This must be the first statement in ɵɵdefineComponent.
// See the `initNgDevMode` docstring for more information.
(typeof ngDevMode === 'undefined' || ngDevMode) && initNgDevMode();
const type = componentDefinition.type;
const typePrototype = type.prototype;
const declaredInputs: {[key: string]: string} = {} as any;
const def: Mutable<ComponentDef<any>, keyof ComponentDef<any>> = {
type: type,
providersResolver: null,
decls: componentDefinition.decls,
vars: componentDefinition.vars,
factory: null,
template: componentDefinition.template || null !,
consts: componentDefinition.consts || null,
ngContentSelectors: componentDefinition.ngContentSelectors,
hostBindings: componentDefinition.hostBindings || null,
hostVars: componentDefinition.hostVars || 0,
hostAttrs: componentDefinition.hostAttrs || null,
contentQueries: componentDefinition.contentQueries || null,
declaredInputs: declaredInputs,
inputs: null !, // assigned in noSideEffects
outputs: null !, // assigned in noSideEffects
exportAs: componentDefinition.exportAs || null,
onChanges: null,
onInit: typePrototype.ngOnInit || null,
doCheck: typePrototype.ngDoCheck || null,
afterContentInit: typePrototype.ngAfterContentInit || null,
afterContentChecked: typePrototype.ngAfterContentChecked || null,
afterViewInit: typePrototype.ngAfterViewInit || null,
afterViewChecked: typePrototype.ngAfterViewChecked || null,
onDestroy: typePrototype.ngOnDestroy || null,
onPush: componentDefinition.changeDetection === ChangeDetectionStrategy.OnPush,
directiveDefs: null !, // assigned in noSideEffects
pipeDefs: null !, // assigned in noSideEffects
selectors: componentDefinition.selectors || EMPTY_ARRAY,
viewQuery: componentDefinition.viewQuery || null,
features: componentDefinition.features as DirectiveDefFeature[] || null,
data: componentDefinition.data || {},
// TODO(misko): convert ViewEncapsulation into const enum so that it can be used directly in the
// next line. Also `None` should be 0 not 2.
encapsulation: componentDefinition.encapsulation || ViewEncapsulation.Emulated,
id: 'c',
styles: componentDefinition.styles || EMPTY_ARRAY,
_: null as never,
setInput: null,
schemas: componentDefinition.schemas || null,
tView: null,
};
def._ = noSideEffects(() => {
const directiveTypes = componentDefinition.directives !;
const feature = componentDefinition.features;
const pipeTypes = componentDefinition.pipes !;
def.id += _renderCompCount++;
def.inputs = invertObject(componentDefinition.inputs, declaredInputs),
def.outputs = invertObject(componentDefinition.outputs),
feature && feature.forEach((fn) => fn(def));
def.directiveDefs = directiveTypes ?
() => (typeof directiveTypes === 'function' ? directiveTypes() : directiveTypes)
.map(extractDirectiveDef) :
null;
def.pipeDefs = pipeTypes ?
() => (typeof pipeTypes === 'function' ? pipeTypes() : pipeTypes).map(extractPipeDef) :
null;
}) as never;
return def as never;
}
/**
* @codeGenApi
*/
export function ɵɵsetComponentScope(
type: ComponentType<any>, directives: Type<any>[], pipes: Type<any>[]): void {
const def = (type.ɵcmp as ComponentDef<any>);
def.directiveDefs = () => directives.map(extractDirectiveDef);
def.pipeDefs = () => pipes.map(extractPipeDef);
}
export function extractDirectiveDef(type: Type<any>): DirectiveDef<any>|ComponentDef<any> {
const def = getComponentDef(type) || getDirectiveDef(type);
if (ngDevMode && !def) {
throw new Error(`'${type.name}' is neither 'ComponentType' or 'DirectiveType'.`);
}
return def !;
}
export function extractPipeDef(type: Type<any>): PipeDef<any> {
const def = getPipeDef(type);
if (ngDevMode && !def) {
throw new Error(`'${type.name}' is not a 'PipeType'.`);
}
return def !;
}
export const autoRegisterModuleById: {[id: string]: NgModuleType} = {};
/**
* @codeGenApi
*/
export function ɵɵdefineNgModule<T>(def: {
/** Token representing the module. Used by DI. */
type: T;
/** List of components to bootstrap. */
bootstrap?: Type<any>[] | (() => Type<any>[]);
/** List of components, directives, and pipes declared by this module. */
declarations?: Type<any>[] | (() => Type<any>[]);
/** List of modules or `ModuleWithProviders` imported by this module. */
imports?: Type<any>[] | (() => Type<any>[]);
/**
* List of modules, `ModuleWithProviders`, components, directives, or pipes exported by this
* module.
*/
exports?: Type<any>[] | (() => Type<any>[]);
/** The set of schemas that declare elements to be allowed in the NgModule. */
schemas?: SchemaMetadata[] | null;
/** Unique ID for the module that is used with `getModuleFactory`. */
id?: string | null;
}): never {
const res: NgModuleDef<T> = {
type: def.type,
bootstrap: def.bootstrap || EMPTY_ARRAY,
declarations: def.declarations || EMPTY_ARRAY,
imports: def.imports || EMPTY_ARRAY,
exports: def.exports || EMPTY_ARRAY,
transitiveCompileScopes: null,
schemas: def.schemas || null,
id: def.id || null,
};
if (def.id != null) {
noSideEffects(
() => { autoRegisterModuleById[def.id !] = def.type as unknown as NgModuleType; });
}
return res as never;
}
/**
* Adds the module metadata that is necessary to compute the module's transitive scope to an
* existing module definition.
*
* Scope metadata of modules is not used in production builds, so calls to this function can be
* marked pure to tree-shake it from the bundle, allowing for all referenced declarations
* to become eligible for tree-shaking as well.
*
* @codeGenApi
*/
export function ɵɵsetNgModuleScope(type: any, scope: {
/** List of components, directives, and pipes declared by this module. */
declarations?: Type<any>[] | (() => Type<any>[]);
/** List of modules or `ModuleWithProviders` imported by this module. */
imports?: Type<any>[] | (() => Type<any>[]);
/**
* List of modules, `ModuleWithProviders`, components, directives, or pipes exported by this
* module.
*/
exports?: Type<any>[] | (() => Type<any>[]);
}): void {
return noSideEffects(() => {
const ngModuleDef = getNgModuleDef(type, true);
ngModuleDef.declarations = scope.declarations || EMPTY_ARRAY;
ngModuleDef.imports = scope.imports || EMPTY_ARRAY;
ngModuleDef.exports = scope.exports || EMPTY_ARRAY;
}) as never;
}
/**
* Inverts an inputs or outputs lookup such that the keys, which were the
* minified keys, are part of the values, and the values are parsed so that
* the publicName of the property is the new key
*
* e.g. for
*
* ```
* class Comp {
* @Input()
* propName1: string;
*
* @Input('publicName2')
* declaredPropName2: number;
* }
* ```
*
* will be serialized as
*
* ```
* {
* propName1: 'propName1',
* declaredPropName2: ['publicName2', 'declaredPropName2'],
* }
* ```
*
* which is than translated by the minifier as:
*
* ```
* {
* minifiedPropName1: 'propName1',
* minifiedPropName2: ['publicName2', 'declaredPropName2'],
* }
* ```
*
* becomes: (public name => minifiedName)
*
* ```
* {
* 'propName1': 'minifiedPropName1',
* 'publicName2': 'minifiedPropName2',
* }
* ```
*
* Optionally the function can take `secondary` which will result in: (public name => declared name)
*
* ```
* {
* 'propName1': 'propName1',
* 'publicName2': 'declaredPropName2',
* }
* ```
*
*/
function invertObject<T>(
obj?: {[P in keyof T]?: string | [string, string]},
secondary?: {[key: string]: string}): {[P in keyof T]: string} {
if (obj == null) return EMPTY_OBJ as any;
const newLookup: any = {};
for (const minifiedKey in obj) {
if (obj.hasOwnProperty(minifiedKey)) {
let publicName: string|[string, string] = obj[minifiedKey] !;
let declaredName = publicName;
if (Array.isArray(publicName)) {
declaredName = publicName[1];
publicName = publicName[0];
}
newLookup[publicName] = minifiedKey;
if (secondary) {
(secondary[publicName] = declaredName as string);
}
}
}
return newLookup;
}
/**
* Create a directive definition object.
*
* # Example
* ```ts
* class MyDirective {
* // Generated by Angular Template Compiler
* // [Symbol] syntax will not be supported by TypeScript until v2.7
* static ɵdir = ɵɵdefineDirective({
* ...
* });
* }
* ```
*
* @codeGenApi
*/
export const ɵɵdefineDirective = ɵɵdefineComponent as any as<T>(directiveDefinition: {
/**
* Directive type, needed to configure the injector.
*/
type: Type<T>;
/** The selectors that will be used to match nodes to this directive. */
selectors?: CssSelectorList;
/**
* A map of input names.
*
* The format is in: `{[actualPropertyName: string]:(string|[string, string])}`.
*
* Given:
* ```
* class MyComponent {
* @Input()
* publicInput1: string;
*
* @Input('publicInput2')
* declaredInput2: string;
* }
* ```
*
* is described as:
* ```
* {
* publicInput1: 'publicInput1',
* declaredInput2: ['declaredInput2', 'publicInput2'],
* }
* ```
*
* Which the minifier may translate to:
* ```
* {
* minifiedPublicInput1: 'publicInput1',
* minifiedDeclaredInput2: [ 'publicInput2', 'declaredInput2'],
* }
* ```
*
* This allows the render to re-construct the minified, public, and declared names
* of properties.
*
* NOTE:
* - Because declared and public name are usually same we only generate the array
* `['declared', 'public']` format when they differ.
* - The reason why this API and `outputs` API is not the same is that `NgOnChanges` has
* inconsistent behavior in that it uses declared names rather than minified or public. For
* this reason `NgOnChanges` will be deprecated and removed in future version and this
* API will be simplified to be consistent with `output`.
*/
inputs?: {[P in keyof T]?: string | [string, string]};
/**
* A map of output names.
*
* The format is in: `{[actualPropertyName: string]:string}`.
*
* Which the minifier may translate to: `{[minifiedPropertyName: string]:string}`.
*
* This allows the render to re-construct the minified and non-minified names
* of properties.
*/
outputs?: {[P in keyof T]?: string};
/**
* A list of optional features to apply.
*
* See: {@link NgOnChangesFeature}, {@link ProvidersFeature}, {@link InheritDefinitionFeature}
*/
features?: DirectiveDefFeature[];
/**
* Function executed by the parent template to allow child directive to apply host bindings.
*/
hostBindings?: HostBindingsFunction<T>;
/**
* The number of bindings in this directive `hostBindings` (including pure fn bindings).
*
* Used to calculate the length of the component's LView array, so we
* can pre-fill the array and set the host binding start index.
*/
hostVars?: number;
/**
* Assign static attribute values to a host element.
*
* This property will assign static attribute values as well as class and style
* values to a host element. Since attribute values can consist of different types of values, the
* `hostAttrs` array must include the values in the following format:
*
* attrs = [
* // static attributes (like `title`, `name`, `id`...)
* attr1, value1, attr2, value,
*
* // a single namespace value (like `x:id`)
* NAMESPACE_MARKER, namespaceUri1, name1, value1,
*
* // another single namespace value (like `x:name`)
* NAMESPACE_MARKER, namespaceUri2, name2, value2,
*
* // a series of CSS classes that will be applied to the element (no spaces)
* CLASSES_MARKER, class1, class2, class3,
*
* // a series of CSS styles (property + value) that will be applied to the element
* STYLES_MARKER, prop1, value1, prop2, value2
* ]
*
* All non-class and non-style attributes must be defined at the start of the list
* first before all class and style values are set. When there is a change in value
* type (like when classes and styles are introduced) a marker must be used to separate
* the entries. The marker values themselves are set via entries found in the
* [AttributeMarker] enum.
*/
hostAttrs?: TAttributes;
/**
* Function to create instances of content queries associated with a given directive.
*/
contentQueries?: ContentQueriesFunction<T>;
/**
* Additional set of instructions specific to view query processing. This could be seen as a
* set of instructions to be inserted into the template function.
*/
viewQuery?: ViewQueriesFunction<T>| null;
/**
* Defines the name that can be used in the template to assign this directive to a variable.
*
* See: {@link Directive.exportAs}
*/
exportAs?: string[];
}) => never;
/**
* Create a pipe definition object.
*
* # Example
* ```
* class MyPipe implements PipeTransform {
* // Generated by Angular Template Compiler
* static ɵpipe = definePipe({
* ...
* });
* }
* ```
* @param pipeDef Pipe definition generated by the compiler
*
* @codeGenApi
*/
export function ɵɵdefinePipe<T>(pipeDef: {
/** Name of the pipe. Used for matching pipes in template to pipe defs. */
name: string,
/** Pipe class reference. Needed to extract pipe lifecycle hooks. */
type: Type<T>,
/** Whether the pipe is pure. */
pure?: boolean
}): never {
return (<PipeDef<T>>{
type: pipeDef.type,
name: pipeDef.name,
factory: null,
pure: pipeDef.pure !== false,
onDestroy: pipeDef.type.prototype.ngOnDestroy || null
}) as never;
}
/**
* The following getter methods retrieve the definition form the type. Currently the retrieval
* honors inheritance, but in the future we may change the rule to require that definitions are
* explicit. This would require some sort of migration strategy.
*/
export function getComponentDef<T>(type: any): ComponentDef<T>|null {
return type[NG_COMP_DEF] || null;
}
export function getDirectiveDef<T>(type: any): DirectiveDef<T>|null {
return type[NG_DIR_DEF] || null;
}
export function getPipeDef<T>(type: any): PipeDef<T>|null {
return type[NG_PIPE_DEF] || null;
}
export function getFactoryDef<T>(type: any, throwNotFound: true): FactoryFn<T>;
export function getFactoryDef<T>(type: any): FactoryFn<T>|null;
export function getFactoryDef<T>(type: any, throwNotFound?: boolean): FactoryFn<T>|null {
const hasFactoryDef = type.hasOwnProperty(NG_FACTORY_DEF);
if (!hasFactoryDef && throwNotFound === true && ngDevMode) {
throw new Error(`Type ${stringify(type)} does not have 'ɵfac' property.`);
}
return hasFactoryDef ? type[NG_FACTORY_DEF] : null;
}
export function getNgModuleDef<T>(type: any, throwNotFound: true): NgModuleDef<T>;
export function getNgModuleDef<T>(type: any): NgModuleDef<T>|null;
export function getNgModuleDef<T>(type: any, throwNotFound?: boolean): NgModuleDef<T>|null {
const ngModuleDef = type[NG_MOD_DEF] || null;
if (!ngModuleDef && throwNotFound === true) {
throw new Error(`Type ${stringify(type)} does not have 'ɵmod' property.`);
}
return ngModuleDef;
}
export function getNgLocaleIdDef(type: any): string|null {
return (type as any)[NG_LOC_ID_DEF] || null;
}