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
This commit is contained in:
@ -117,7 +117,6 @@ export {
|
||||
ɵɵreference,
|
||||
ɵɵenableBindings,
|
||||
ɵɵdisableBindings,
|
||||
ɵɵallocHostVars,
|
||||
ɵɵelementContainerStart,
|
||||
ɵɵelementContainerEnd,
|
||||
ɵɵelementContainer,
|
||||
@ -144,7 +143,6 @@ export {
|
||||
ɵɵstylePropInterpolate8,
|
||||
ɵɵstylePropInterpolateV,
|
||||
ɵɵclassProp,
|
||||
ɵɵelementHostAttrs,
|
||||
|
||||
ɵɵselect,
|
||||
ɵɵadvance,
|
||||
|
@ -37,3 +37,32 @@ export interface Type<T> extends Function { new (...args: any[]): T; }
|
||||
export type Mutable<T extends{[x: string]: any}, K extends string> = {
|
||||
[P in K]: T[P];
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a writable type version of type.
|
||||
*
|
||||
* USAGE:
|
||||
* Given:
|
||||
* ```
|
||||
* interface Person {readonly name: string}
|
||||
* ```
|
||||
*
|
||||
* We would like to get a read/write version of `Person`.
|
||||
* ```
|
||||
* const WritablePerson = Writable<Person>;
|
||||
* ```
|
||||
*
|
||||
* The result is that you can do:
|
||||
*
|
||||
* ```
|
||||
* const readonlyPerson: Person = {name: 'Marry'};
|
||||
* readonlyPerson.name = 'John'; // TypeError
|
||||
* (readonlyPerson as WritablePerson).name = 'John'; // OK
|
||||
*
|
||||
* // Error: Correctly detects that `Person` did not have `age` property.
|
||||
* (readonlyPerson as WritablePerson).age = 30;
|
||||
* ```
|
||||
*/
|
||||
export type Writable<T> = {
|
||||
-readonly[K in keyof T]: T[K];
|
||||
};
|
||||
|
@ -114,7 +114,7 @@ export function renderComponent<T>(
|
||||
const rendererFactory = opts.rendererFactory || domRendererFactory3;
|
||||
const sanitizer = opts.sanitizer || null;
|
||||
const componentDef = getComponentDef<T>(componentType) !;
|
||||
if (componentDef.type != componentType) componentDef.type = componentType;
|
||||
if (componentDef.type != componentType) (componentDef as{type: Type<any>}).type = componentType;
|
||||
|
||||
// The first index of the first selector is the tag name.
|
||||
const componentTag = componentDef.selectors ![0] ![0] as string;
|
||||
@ -211,7 +211,13 @@ export function createRootComponent<T>(
|
||||
}
|
||||
|
||||
const rootTNode = getPreviousOrParentTNode();
|
||||
if (tView.firstCreatePass && componentDef.hostBindings) {
|
||||
// TODO(misko-next): This is a temporary work around for the fact that we moved the information
|
||||
// from instruction to declaration. The workaround is to just call the instruction as if it was
|
||||
// part of the `hostAttrs`.
|
||||
// The check for componentDef.hostBindings is wrong since now some directives may not
|
||||
// have componentDef.hostBindings but they still need to process hostVars and hostAttrs
|
||||
if (tView.firstCreatePass && (componentDef.hostBindings || componentDef.hostVars !== 0 ||
|
||||
componentDef.hostAttrs !== null)) {
|
||||
const elementIndex = rootTNode.index - HEADER_OFFSET;
|
||||
setActiveHostElement(elementIndex);
|
||||
incrementActiveDirectiveId();
|
||||
|
@ -18,14 +18,17 @@ 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 {TConstants} from './interfaces/node';
|
||||
// while SelectorFlags is unused here, it's required so that types don't get resolved lazily
|
||||
// see: https://github.com/Microsoft/web-build-tools/issues/1050
|
||||
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.
|
||||
*
|
||||
@ -130,6 +133,46 @@ export function ɵɵdefineComponent<T>(componentDefinition: {
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
@ -263,6 +306,8 @@ export function ɵɵdefineComponent<T>(componentDefinition: {
|
||||
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
|
||||
@ -588,6 +633,46 @@ export const ɵɵdefineDirective = ɵɵdefineComponent as any as<T>(directiveDef
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
|
@ -55,6 +55,8 @@ export function throwErrorIfNoChangesMode(
|
||||
}
|
||||
// TODO: include debug context, see `viewDebugError` function in
|
||||
// `packages/core/src/view/errors.ts` for reference.
|
||||
// tslint:disable-next-line
|
||||
debugger; // Left intentionally for better debugger experience.
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
|
@ -6,17 +6,22 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Type} from '../../interface/type';
|
||||
import {Type, Writable} from '../../interface/type';
|
||||
import {assertEqual} from '../../util/assert';
|
||||
import {fillProperties} from '../../util/property';
|
||||
import {EMPTY_ARRAY, EMPTY_OBJ} from '../empty';
|
||||
import {ComponentDef, ContentQueriesFunction, DirectiveDef, DirectiveDefFeature, HostBindingsFunction, RenderFlags, ViewQueriesFunction} from '../interfaces/definition';
|
||||
import {AttributeMarker, TAttributes} from '../interfaces/node';
|
||||
import {isComponentDef} from '../interfaces/type_checks';
|
||||
import {mergeHostAttrs} from '../util/attrs_utils';
|
||||
|
||||
export function getSuperType(type: Type<any>): Type<any>&
|
||||
{ɵcmp?: ComponentDef<any>, ɵdir?: DirectiveDef<any>} {
|
||||
return Object.getPrototypeOf(type.prototype).constructor;
|
||||
}
|
||||
|
||||
type WritableDef = Writable<DirectiveDef<any>|ComponentDef<any>>;
|
||||
|
||||
/**
|
||||
* Merges the definition from a super class to a sub class.
|
||||
* @param definition The definition that is a SubClass of another directive of component
|
||||
@ -26,6 +31,7 @@ export function getSuperType(type: Type<any>): Type<any>&
|
||||
export function ɵɵInheritDefinitionFeature(definition: DirectiveDef<any>| ComponentDef<any>): void {
|
||||
let superType = getSuperType(definition.type);
|
||||
let shouldInheritFields = true;
|
||||
const inheritanceChain: WritableDef[] = [definition];
|
||||
|
||||
while (superType) {
|
||||
let superDef: DirectiveDef<any>|ComponentDef<any>|undefined = undefined;
|
||||
@ -42,9 +48,10 @@ export function ɵɵInheritDefinitionFeature(definition: DirectiveDef<any>| Comp
|
||||
|
||||
if (superDef) {
|
||||
if (shouldInheritFields) {
|
||||
inheritanceChain.push(superDef);
|
||||
// Some fields in the definition may be empty, if there were no values to put in them that
|
||||
// would've justified object creation. Unwrap them if necessary.
|
||||
const writeableDef = definition as any;
|
||||
const writeableDef = definition as WritableDef;
|
||||
writeableDef.inputs = maybeUnwrapEmpty(definition.inputs);
|
||||
writeableDef.declaredInputs = maybeUnwrapEmpty(definition.declaredInputs);
|
||||
writeableDef.outputs = maybeUnwrapEmpty(definition.outputs);
|
||||
@ -66,14 +73,14 @@ export function ɵɵInheritDefinitionFeature(definition: DirectiveDef<any>| Comp
|
||||
|
||||
// Inherit hooks
|
||||
// Assume super class inheritance feature has already run.
|
||||
definition.afterContentChecked =
|
||||
definition.afterContentChecked || superDef.afterContentChecked;
|
||||
definition.afterContentInit = definition.afterContentInit || superDef.afterContentInit;
|
||||
definition.afterViewChecked = definition.afterViewChecked || superDef.afterViewChecked;
|
||||
definition.afterViewInit = definition.afterViewInit || superDef.afterViewInit;
|
||||
definition.doCheck = definition.doCheck || superDef.doCheck;
|
||||
definition.onDestroy = definition.onDestroy || superDef.onDestroy;
|
||||
definition.onInit = definition.onInit || superDef.onInit;
|
||||
writeableDef.afterContentChecked =
|
||||
writeableDef.afterContentChecked || superDef.afterContentChecked;
|
||||
writeableDef.afterContentInit = definition.afterContentInit || superDef.afterContentInit;
|
||||
writeableDef.afterViewChecked = definition.afterViewChecked || superDef.afterViewChecked;
|
||||
writeableDef.afterViewInit = definition.afterViewInit || superDef.afterViewInit;
|
||||
writeableDef.doCheck = definition.doCheck || superDef.doCheck;
|
||||
writeableDef.onDestroy = definition.onDestroy || superDef.onDestroy;
|
||||
writeableDef.onInit = definition.onInit || superDef.onInit;
|
||||
}
|
||||
|
||||
// Run parent features
|
||||
@ -100,6 +107,28 @@ export function ɵɵInheritDefinitionFeature(definition: DirectiveDef<any>| Comp
|
||||
|
||||
superType = Object.getPrototypeOf(superType);
|
||||
}
|
||||
mergeHostAttrsAcrossInheritance(inheritanceChain);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the `hostAttrs` and `hostVars` from the inherited parent to the base class.
|
||||
*
|
||||
* @param inheritanceChain A list of `WritableDefs` starting at the top most type and listing
|
||||
* sub-types in order. For each type take the `hostAttrs` and `hostVars` and merge it with the child
|
||||
* type.
|
||||
*/
|
||||
function mergeHostAttrsAcrossInheritance(inheritanceChain: WritableDef[]) {
|
||||
let hostVars: number = 0;
|
||||
let hostAttrs: TAttributes|null = null;
|
||||
// We process the inheritance order from the base to the leaves here.
|
||||
for (let i = inheritanceChain.length - 1; i >= 0; i--) {
|
||||
const def = inheritanceChain[i];
|
||||
// For each `hostVars`, we need to add the superclass amount.
|
||||
def.hostVars = (hostVars += def.hostVars);
|
||||
// for each `hostAttrs` we need to merge it with superclass.
|
||||
def.hostAttrs =
|
||||
mergeHostAttrs(def.hostAttrs, hostAttrs = mergeHostAttrs(hostAttrs, def.hostAttrs));
|
||||
}
|
||||
}
|
||||
|
||||
function maybeUnwrapEmpty<T>(value: T[]): T[];
|
||||
@ -114,8 +143,7 @@ function maybeUnwrapEmpty(value: any): any {
|
||||
}
|
||||
}
|
||||
|
||||
function inheritViewQuery(
|
||||
definition: DirectiveDef<any>| ComponentDef<any>, superViewQuery: ViewQueriesFunction<any>) {
|
||||
function inheritViewQuery(definition: WritableDef, superViewQuery: ViewQueriesFunction<any>) {
|
||||
const prevViewQuery = definition.viewQuery;
|
||||
if (prevViewQuery) {
|
||||
definition.viewQuery = (rf, ctx) => {
|
||||
@ -128,8 +156,7 @@ function inheritViewQuery(
|
||||
}
|
||||
|
||||
function inheritContentQueries(
|
||||
definition: DirectiveDef<any>| ComponentDef<any>,
|
||||
superContentQueries: ContentQueriesFunction<any>) {
|
||||
definition: WritableDef, superContentQueries: ContentQueriesFunction<any>) {
|
||||
const prevContentQueries = definition.contentQueries;
|
||||
if (prevContentQueries) {
|
||||
definition.contentQueries = (rf, ctx, directiveIndex) => {
|
||||
@ -142,8 +169,7 @@ function inheritContentQueries(
|
||||
}
|
||||
|
||||
function inheritHostBindings(
|
||||
definition: DirectiveDef<any>| ComponentDef<any>,
|
||||
superHostBindings: HostBindingsFunction<any>) {
|
||||
definition: WritableDef, superHostBindings: HostBindingsFunction<any>) {
|
||||
const prevHostBindings = definition.hostBindings;
|
||||
if (prevHostBindings) {
|
||||
definition.hostBindings = (rf: RenderFlags, ctx: any, elementIndex: number) => {
|
||||
|
@ -51,7 +51,7 @@ export function ɵɵNgOnChangesFeature<T>(): DirectiveDefFeature {
|
||||
function NgOnChangesFeatureImpl<T>(definition: DirectiveDef<T>): void {
|
||||
if (definition.type.prototype.ngOnChanges) {
|
||||
definition.setInput = ngOnChangesSetInput;
|
||||
definition.onChanges = wrapOnChanges();
|
||||
(definition as{onChanges: Function}).onChanges = wrapOnChanges();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -24,8 +24,6 @@ export {
|
||||
store,
|
||||
tick,
|
||||
|
||||
ɵɵallocHostVars,
|
||||
|
||||
ɵɵattribute,
|
||||
ɵɵattributeInterpolate1,
|
||||
ɵɵattributeInterpolate2,
|
||||
@ -65,7 +63,6 @@ export {
|
||||
ɵɵelementContainerStart,
|
||||
ɵɵelementEnd,
|
||||
|
||||
ɵɵelementHostAttrs,
|
||||
ɵɵelementStart,
|
||||
ɵɵembeddedViewEnd,
|
||||
|
||||
|
@ -12,6 +12,9 @@ import {LView, TVIEW, TView} from '../interfaces/view';
|
||||
import {getCurrentDirectiveDef, getLView} from '../state';
|
||||
import {NO_CHANGE} from '../tokens';
|
||||
|
||||
// TODO(misko-next): delete alloc_host_vars.ts file.
|
||||
// TODO(misko-next): delete `ɵɵallocHostVars`
|
||||
|
||||
/**
|
||||
* Allocates the necessary amount of slots for host vars.
|
||||
*
|
||||
|
@ -219,6 +219,7 @@ export function ɵɵelementHostAttrs(attrs: TAttributes) {
|
||||
// errors...
|
||||
if (tNode.type === TNodeType.Element) {
|
||||
const native = getNativeByTNode(tNode, lView) as RElement;
|
||||
// TODO(misko-next): setup attributes need to be moved out of `ɵɵelementHostAttrs`
|
||||
const lastAttrIndex = setUpAttributes(lView[RENDERER], native, attrs);
|
||||
if (tView.firstCreatePass) {
|
||||
const stylingNeedsToBeRendered = registerInitialStylingOnTNode(tNode, attrs, lastAttrIndex);
|
||||
@ -233,6 +234,7 @@ export function ɵɵelementHostAttrs(attrs: TAttributes) {
|
||||
// attribute values to the element.
|
||||
if (stylingNeedsToBeRendered) {
|
||||
const renderer = lView[RENDERER];
|
||||
// TODO(misko-next): Styling initialization should move out of `ɵɵelementHostAttrs`
|
||||
renderInitialStyling(renderer, native, tNode, true);
|
||||
}
|
||||
}
|
||||
|
@ -41,6 +41,8 @@ import {getLViewParent} from '../util/view_traversal_utils';
|
||||
import {getComponentLViewByIndex, getNativeByIndex, getNativeByTNode, getTNode, isCreationMode, readPatchedLView, resetPreOrderHookFlags, unwrapRNode, viewAttachedToChangeDetector} from '../util/view_utils';
|
||||
|
||||
import {selectIndexInternal} from './advance';
|
||||
import {ɵɵallocHostVars} from './alloc_host_vars';
|
||||
import {ɵɵelementHostAttrs} from './element';
|
||||
import {LCleanup, LViewBlueprint, MatchesArray, TCleanup, TNodeConstructor, TNodeInitialInputs, TNodeLocalNames, TViewComponents, TViewConstructor, attachLContainerDebug, attachLViewDebug, cloneToLViewFromTViewBlueprint, cloneToTViewData} from './lview_debug';
|
||||
|
||||
|
||||
@ -1120,7 +1122,8 @@ export function resolveDirectives(
|
||||
saveNameToExportMap(tView.data !.length - 1, def, exportsMap);
|
||||
|
||||
if (def.contentQueries !== null) tNode.flags |= TNodeFlags.hasContentQuery;
|
||||
if (def.hostBindings !== null) tNode.flags |= TNodeFlags.hasHostBindings;
|
||||
if (def.hostBindings !== null || def.hostAttrs !== null || def.hostVars !== 0)
|
||||
tNode.flags |= TNodeFlags.hasHostBindings;
|
||||
|
||||
// Only push a node index into the preOrderHooks array if this is the first
|
||||
// pre-order hook found on this node.
|
||||
@ -1194,7 +1197,7 @@ function invokeDirectivesHostBindings(tView: TView, viewData: LView, tNode: TNod
|
||||
for (let i = start; i < end; i++) {
|
||||
const def = tView.data[i] as DirectiveDef<any>;
|
||||
const directive = viewData[i];
|
||||
if (def.hostBindings) {
|
||||
if (def.hostBindings !== null || def.hostVars !== 0 || def.hostAttrs !== null) {
|
||||
// It is important that this be called first before the actual instructions
|
||||
// are run because this way the first directive ID value is not zero.
|
||||
incrementActiveDirectiveId();
|
||||
@ -1214,7 +1217,18 @@ export function invokeHostBindingsInCreationMode(
|
||||
const previousExpandoLength = expando.length;
|
||||
setCurrentDirectiveDef(def);
|
||||
const elementIndex = tNode.index - HEADER_OFFSET;
|
||||
def.hostBindings !(RenderFlags.Create, directive, elementIndex);
|
||||
// TODO(misko-next): This is a temporary work around for the fact that we moved the information
|
||||
// from instruction to declaration. The workaround is to just call the instruction as if it was
|
||||
// part of the `hostAttrs`.
|
||||
if (def.hostVars !== 0) {
|
||||
ɵɵallocHostVars(def.hostVars);
|
||||
}
|
||||
if (def.hostAttrs !== null) {
|
||||
ɵɵelementHostAttrs(def.hostAttrs);
|
||||
}
|
||||
if (def.hostBindings !== null) {
|
||||
def.hostBindings !(RenderFlags.Create, directive, elementIndex);
|
||||
}
|
||||
setCurrentDirectiveDef(null);
|
||||
// `hostBindings` function may or may not contain `allocHostVars` call
|
||||
// (e.g. it may not if it only contains host listeners), so we need to check whether
|
||||
@ -1346,7 +1360,8 @@ export function initNodeFlags(tNode: TNode, index: number, numberOfDirectives: n
|
||||
|
||||
function baseResolveDirective<T>(tView: TView, viewData: LView, def: DirectiveDef<T>) {
|
||||
tView.data.push(def);
|
||||
const directiveFactory = def.factory || (def.factory = getFactoryDef(def.type, true));
|
||||
const directiveFactory =
|
||||
def.factory || ((def as{factory: Function}).factory = getFactoryDef(def.type, true));
|
||||
const nodeInjectorFactory = new NodeInjectorFactory(directiveFactory, isComponentDef(def), null);
|
||||
tView.blueprint.push(nodeInjectorFactory);
|
||||
viewData.push(nodeInjectorFactory);
|
||||
|
@ -10,7 +10,7 @@ import {SchemaMetadata, ViewEncapsulation} from '../../core';
|
||||
import {ProcessProvidersFunction} from '../../di/interface/provider';
|
||||
import {Type} from '../../interface/type';
|
||||
|
||||
import {TConstants} from './node';
|
||||
import {TAttributes, TConstants} from './node';
|
||||
import {CssSelectorList} from './projection';
|
||||
import {TView} from './view';
|
||||
|
||||
@ -146,10 +146,50 @@ export interface DirectiveDef<T> {
|
||||
/**
|
||||
* Refreshes host bindings on the associated directive.
|
||||
*/
|
||||
hostBindings: HostBindingsFunction<T>|null;
|
||||
readonly hostBindings: HostBindingsFunction<T>|null;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
readonly 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.
|
||||
*/
|
||||
readonly hostAttrs: TAttributes|null;
|
||||
|
||||
/** Token representing the directive. Used by DI. */
|
||||
type: Type<T>;
|
||||
readonly type: Type<T>;
|
||||
|
||||
/** Function that resolves providers and publishes them into the DI system. */
|
||||
providersResolver:
|
||||
@ -168,17 +208,17 @@ export interface DirectiveDef<T> {
|
||||
* Factory function used to create a new directive instance. Will be null initially.
|
||||
* Populated when the factory is first requested by directive instantiation logic.
|
||||
*/
|
||||
factory: FactoryFn<T>|null;
|
||||
readonly factory: FactoryFn<T>|null;
|
||||
|
||||
/* The following are lifecycle hooks for this component */
|
||||
onChanges: (() => void)|null;
|
||||
onInit: (() => void)|null;
|
||||
doCheck: (() => void)|null;
|
||||
afterContentInit: (() => void)|null;
|
||||
afterContentChecked: (() => void)|null;
|
||||
afterViewInit: (() => void)|null;
|
||||
afterViewChecked: (() => void)|null;
|
||||
onDestroy: (() => void)|null;
|
||||
readonly onChanges: (() => void)|null;
|
||||
readonly onInit: (() => void)|null;
|
||||
readonly doCheck: (() => void)|null;
|
||||
readonly afterContentInit: (() => void)|null;
|
||||
readonly afterContentChecked: (() => void)|null;
|
||||
readonly afterViewInit: (() => void)|null;
|
||||
readonly afterViewChecked: (() => void)|null;
|
||||
readonly onDestroy: (() => void)|null;
|
||||
|
||||
/**
|
||||
* The features applied to this directive
|
||||
|
@ -201,6 +201,15 @@ export const enum TNodeProviderIndexes {
|
||||
* items are not regular attributes and the processing should be adapted accordingly.
|
||||
*/
|
||||
export const enum AttributeMarker {
|
||||
/**
|
||||
* An implicit marker which indicates that the value in the array are of `attributeKey`,
|
||||
* `attributeValue` format.
|
||||
*
|
||||
* NOTE: This is implicit as it is the type when no marker is present in array. We indicate that
|
||||
* it should not be present at runtime by the negative number.
|
||||
*/
|
||||
ImplicitAttributes = -1,
|
||||
|
||||
/**
|
||||
* Marker indicates that the following 3 values in the attributes array are:
|
||||
* namespaceUri, attributeName, attributeValue
|
||||
|
@ -58,7 +58,6 @@ export const angularCoreEnv: {[name: string]: Function} =
|
||||
'ɵɵnamespaceSVG': r3.ɵɵnamespaceSVG,
|
||||
'ɵɵenableBindings': r3.ɵɵenableBindings,
|
||||
'ɵɵdisableBindings': r3.ɵɵdisableBindings,
|
||||
'ɵɵallocHostVars': r3.ɵɵallocHostVars,
|
||||
'ɵɵelementStart': r3.ɵɵelementStart,
|
||||
'ɵɵelementEnd': r3.ɵɵelementEnd,
|
||||
'ɵɵelement': r3.ɵɵelement,
|
||||
@ -107,7 +106,6 @@ export const angularCoreEnv: {[name: string]: Function} =
|
||||
'ɵɵloadQuery': r3.ɵɵloadQuery,
|
||||
'ɵɵcontentQuery': r3.ɵɵcontentQuery,
|
||||
'ɵɵreference': r3.ɵɵreference,
|
||||
'ɵɵelementHostAttrs': r3.ɵɵelementHostAttrs,
|
||||
'ɵɵclassMap': r3.ɵɵclassMap,
|
||||
'ɵɵclassMapInterpolate1': r3.ɵɵclassMapInterpolate1,
|
||||
'ɵɵclassMapInterpolate2': r3.ɵɵclassMapInterpolate2,
|
||||
|
@ -5,12 +5,14 @@
|
||||
* 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 {assertEqual} from '../../util/assert';
|
||||
import {CharCode} from '../../util/char_code';
|
||||
import {AttributeMarker, TAttributes} from '../interfaces/node';
|
||||
import {CssSelector} from '../interfaces/projection';
|
||||
import {ProceduralRenderer3, RElement, Renderer3, isProceduralRenderer} from '../interfaces/renderer';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Assigns all attribute values to the provided element via the inferred renderer.
|
||||
*
|
||||
@ -106,3 +108,114 @@ export function isAnimationProp(name: string): boolean {
|
||||
// charCodeAt doesn't allocate memory to return a substring.
|
||||
return name.charCodeAt(0) === CharCode.AT_SIGN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges `src` `TAttributes` into `dst` `TAttributes` removing any duplicates in the process.
|
||||
*
|
||||
* This merge function keeps the order of attrs same.
|
||||
*
|
||||
* @param dst Location of where the merged `TAttributes` should end up.
|
||||
* @param src `TAttributes` which should be appended to `dst`
|
||||
*/
|
||||
export function mergeHostAttrs(dst: TAttributes | null, src: TAttributes | null): TAttributes|null {
|
||||
if (src === null || src.length === 0) {
|
||||
// do nothing
|
||||
} else if (dst === null || dst.length === 0) {
|
||||
// We have source, but dst is empty, just make a copy.
|
||||
dst = src.slice();
|
||||
} else {
|
||||
let srcMarker: AttributeMarker = AttributeMarker.ImplicitAttributes;
|
||||
for (let i = 0; i < src.length; i++) {
|
||||
const item = src[i];
|
||||
if (typeof item === 'number') {
|
||||
srcMarker = item;
|
||||
} else {
|
||||
if (srcMarker === AttributeMarker.NamespaceURI) {
|
||||
// Case where we need to consume `key1`, `key2`, `value` items.
|
||||
} else if (
|
||||
srcMarker === AttributeMarker.ImplicitAttributes ||
|
||||
srcMarker === AttributeMarker.Styles) {
|
||||
// Case where we have to consume `key1` and `value` only.
|
||||
mergeHostAttribute(dst, srcMarker, item as string, null, src[++i] as string);
|
||||
} else {
|
||||
// Case where we have to consume `key1` only.
|
||||
mergeHostAttribute(dst, srcMarker, item as string, null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return dst;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append `key`/`value` to existing `TAttributes` taking region marker and duplicates into account.
|
||||
*
|
||||
* @param dst `TAttributes` to append to.
|
||||
* @param marker Region where the `key`/`value` should be added.
|
||||
* @param key1 Key to add to `TAttributes`
|
||||
* @param key2 Key to add to `TAttributes` (in case of `AttributeMarker.NamespaceURI`)
|
||||
* @param value Value to add or to overwrite to `TAttributes` Only used if `marker` is not Class.
|
||||
*/
|
||||
export function mergeHostAttribute(
|
||||
dst: TAttributes, marker: AttributeMarker, key1: string, key2: string | null,
|
||||
value: string | null): void {
|
||||
let i = 0;
|
||||
// Assume that new markers will be inserted at the end.
|
||||
let markerInsertPosition = dst.length;
|
||||
// scan until correct type.
|
||||
if (marker === AttributeMarker.ImplicitAttributes) {
|
||||
markerInsertPosition = -1;
|
||||
} else {
|
||||
while (i < dst.length) {
|
||||
const dstValue = dst[i++];
|
||||
if (typeof dstValue === 'number') {
|
||||
if (dstValue === marker) {
|
||||
markerInsertPosition = -1;
|
||||
break;
|
||||
} else if (dstValue > marker) {
|
||||
// We need to save this as we want the markers to be inserted in specific order.
|
||||
markerInsertPosition = i - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// search until you find place of insertion
|
||||
while (i < dst.length) {
|
||||
const item = dst[i];
|
||||
if (typeof item === 'number') {
|
||||
// since `i` started as the index after the marker, we did not find it if we are at the next
|
||||
// marker
|
||||
break;
|
||||
} else if (item === key1) {
|
||||
// We already have same token
|
||||
if (key2 === null) {
|
||||
if (value !== null) {
|
||||
dst[i + 1] = value;
|
||||
}
|
||||
return;
|
||||
} else if (key2 === dst[i + 1]) {
|
||||
dst[i + 2] = value !;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Increment counter.
|
||||
i++;
|
||||
if (key2 !== null) i++;
|
||||
if (value !== null) i++;
|
||||
}
|
||||
|
||||
// insert at location.
|
||||
if (markerInsertPosition !== -1) {
|
||||
dst.splice(markerInsertPosition, 0, marker);
|
||||
i = markerInsertPosition + 1;
|
||||
}
|
||||
dst.splice(i++, 0, key1);
|
||||
if (key2 !== null) {
|
||||
dst.splice(i++, 0, key2);
|
||||
}
|
||||
if (value !== null) {
|
||||
dst.splice(i++, 0, value);
|
||||
}
|
||||
}
|
@ -7,6 +7,8 @@
|
||||
*/
|
||||
|
||||
import {Component, ContentChildren, Directive, EventEmitter, HostBinding, HostListener, Input, OnChanges, Output, QueryList, ViewChildren} from '@angular/core';
|
||||
import {ivyEnabled} from '@angular/core/src/ivy_switch';
|
||||
import {getDirectiveDef} from '@angular/core/src/render3/definition';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {onlyInIvy} from '@angular/private/testing';
|
||||
@ -42,6 +44,86 @@ describe('inheritance', () => {
|
||||
}).toThrowError('Directives cannot inherit Components');
|
||||
});
|
||||
|
||||
describe('multiple children', () => {
|
||||
it('should ensure that multiple child classes don\'t cause multiple parent execution', () => {
|
||||
// Assume this inheritance:
|
||||
// Base
|
||||
// |
|
||||
// Super
|
||||
// / \
|
||||
// Sub1 Sub2
|
||||
//
|
||||
// In the above case:
|
||||
// 1. Sub1 as will walk the inheritance Sub1, Super, Base
|
||||
// 2. Sub2 as will walk the inheritance Sub2, Super, Base
|
||||
//
|
||||
// Notice that Super, Base will get walked twice. Because inheritance works by wrapping parent
|
||||
// hostBindings function in a delegate which calls the hostBindings of the directive as well
|
||||
// as super, we need to ensure that we don't double wrap the hostBindings function. Doing so
|
||||
// would result in calling the hostBindings multiple times (unnecessarily). This would be
|
||||
// especially an issue if we have a lot of sub-classes (as is common in component libraries)
|
||||
const log: string[] = [];
|
||||
|
||||
@Directive({selector: '[superDir]'})
|
||||
class BaseDirective {
|
||||
@HostBinding('style.background-color')
|
||||
get backgroundColor() {
|
||||
log.push('Base.backgroundColor');
|
||||
return 'white';
|
||||
}
|
||||
}
|
||||
|
||||
@Directive({selector: '[superDir]'})
|
||||
class SuperDirective extends BaseDirective {
|
||||
@HostBinding('style.color')
|
||||
get color() {
|
||||
log.push('Super.color');
|
||||
return 'blue';
|
||||
}
|
||||
}
|
||||
|
||||
@Directive({selector: '[subDir1]'})
|
||||
class Sub1Directive extends SuperDirective {
|
||||
@HostBinding('style.height')
|
||||
get height() {
|
||||
log.push('Sub1.height');
|
||||
return '200px';
|
||||
}
|
||||
}
|
||||
|
||||
@Directive({selector: '[subDir2]'})
|
||||
class Sub2Directive extends SuperDirective {
|
||||
@HostBinding('style.width')
|
||||
get width() {
|
||||
log.push('Sub2.width');
|
||||
return '100px';
|
||||
}
|
||||
}
|
||||
|
||||
@Component({template: `<div subDir1 subDir2></div>`})
|
||||
class App {
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [App, Sub1Directive, Sub2Directive, SuperDirective],
|
||||
});
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges(false); // Don't check for no changes (so that assertion does not need
|
||||
// to worry about it.)
|
||||
|
||||
expect(log).toEqual([
|
||||
'Base.backgroundColor', 'Super.color', 'Sub1.height', //
|
||||
'Base.backgroundColor', 'Super.color', 'Sub2.width', //
|
||||
]);
|
||||
if (ivyEnabled) {
|
||||
expect(getDirectiveDef(BaseDirective) !.hostVars).toEqual(1);
|
||||
expect(getDirectiveDef(SuperDirective) !.hostVars).toEqual(2);
|
||||
expect(getDirectiveDef(Sub1Directive) !.hostVars).toEqual(3);
|
||||
expect(getDirectiveDef(Sub2Directive) !.hostVars).toEqual(3);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('ngOnChanges', () => {
|
||||
it('should be inherited when super is a directive', () => {
|
||||
const log: string[] = [];
|
||||
|
@ -314,6 +314,9 @@
|
||||
{
|
||||
"name": "getContainerRenderParent"
|
||||
},
|
||||
{
|
||||
"name": "getCurrentDirectiveDef"
|
||||
},
|
||||
{
|
||||
"name": "getDirectiveDef"
|
||||
},
|
||||
@ -404,6 +407,9 @@
|
||||
{
|
||||
"name": "getStylingMapArray"
|
||||
},
|
||||
{
|
||||
"name": "getTNode"
|
||||
},
|
||||
{
|
||||
"name": "hasActiveElementFlag"
|
||||
},
|
||||
@ -545,6 +551,12 @@
|
||||
{
|
||||
"name": "objectToClassName"
|
||||
},
|
||||
{
|
||||
"name": "prefillHostVars"
|
||||
},
|
||||
{
|
||||
"name": "queueHostBindingForCheck"
|
||||
},
|
||||
{
|
||||
"name": "refreshChildComponents"
|
||||
},
|
||||
@ -695,6 +707,9 @@
|
||||
{
|
||||
"name": "writeStylingValueDirectly"
|
||||
},
|
||||
{
|
||||
"name": "ɵɵallocHostVars"
|
||||
},
|
||||
{
|
||||
"name": "ɵɵdefineComponent"
|
||||
},
|
||||
@ -710,6 +725,9 @@
|
||||
{
|
||||
"name": "ɵɵelementEnd"
|
||||
},
|
||||
{
|
||||
"name": "ɵɵelementHostAttrs"
|
||||
},
|
||||
{
|
||||
"name": "ɵɵelementStart"
|
||||
},
|
||||
|
@ -104,6 +104,9 @@
|
||||
{
|
||||
"name": "RENDERER_FACTORY"
|
||||
},
|
||||
{
|
||||
"name": "RendererStyleFlags3"
|
||||
},
|
||||
{
|
||||
"name": "SANITIZER"
|
||||
},
|
||||
@ -137,12 +140,18 @@
|
||||
{
|
||||
"name": "_renderCompCount"
|
||||
},
|
||||
{
|
||||
"name": "addItemToStylingMap"
|
||||
},
|
||||
{
|
||||
"name": "addToViewTree"
|
||||
},
|
||||
{
|
||||
"name": "allocLFrame"
|
||||
},
|
||||
{
|
||||
"name": "allocStylingMapArray"
|
||||
},
|
||||
{
|
||||
"name": "appendChild"
|
||||
},
|
||||
@ -161,6 +170,9 @@
|
||||
{
|
||||
"name": "callHooks"
|
||||
},
|
||||
{
|
||||
"name": "concatString"
|
||||
},
|
||||
{
|
||||
"name": "createLFrame"
|
||||
},
|
||||
@ -227,6 +239,9 @@
|
||||
{
|
||||
"name": "extractPipeDef"
|
||||
},
|
||||
{
|
||||
"name": "forceStylesAsString"
|
||||
},
|
||||
{
|
||||
"name": "generateExpandoInstructionBlock"
|
||||
},
|
||||
@ -248,6 +263,9 @@
|
||||
{
|
||||
"name": "getContainerRenderParent"
|
||||
},
|
||||
{
|
||||
"name": "getCurrentDirectiveDef"
|
||||
},
|
||||
{
|
||||
"name": "getDirectiveDef"
|
||||
},
|
||||
@ -260,6 +278,9 @@
|
||||
{
|
||||
"name": "getFirstNativeNode"
|
||||
},
|
||||
{
|
||||
"name": "getInitialStylingValue"
|
||||
},
|
||||
{
|
||||
"name": "getInjectorIndex"
|
||||
},
|
||||
@ -275,6 +296,12 @@
|
||||
{
|
||||
"name": "getLViewParent"
|
||||
},
|
||||
{
|
||||
"name": "getMapProp"
|
||||
},
|
||||
{
|
||||
"name": "getMapValue"
|
||||
},
|
||||
{
|
||||
"name": "getNativeAnchorNode"
|
||||
},
|
||||
@ -317,12 +344,21 @@
|
||||
{
|
||||
"name": "getSelectedIndex"
|
||||
},
|
||||
{
|
||||
"name": "getStylingMapArray"
|
||||
},
|
||||
{
|
||||
"name": "getTNode"
|
||||
},
|
||||
{
|
||||
"name": "hasActiveElementFlag"
|
||||
},
|
||||
{
|
||||
"name": "hasParentInjector"
|
||||
},
|
||||
{
|
||||
"name": "hyphenate"
|
||||
},
|
||||
{
|
||||
"name": "includeViewProviders"
|
||||
},
|
||||
@ -350,6 +386,9 @@
|
||||
{
|
||||
"name": "invokeHostBindingsInCreationMode"
|
||||
},
|
||||
{
|
||||
"name": "isAnimationProp"
|
||||
},
|
||||
{
|
||||
"name": "isComponentDef"
|
||||
},
|
||||
@ -365,6 +404,12 @@
|
||||
{
|
||||
"name": "isProceduralRenderer"
|
||||
},
|
||||
{
|
||||
"name": "isStylingContext"
|
||||
},
|
||||
{
|
||||
"name": "isStylingValueDefined"
|
||||
},
|
||||
{
|
||||
"name": "leaveDI"
|
||||
},
|
||||
@ -398,6 +443,15 @@
|
||||
{
|
||||
"name": "noSideEffects"
|
||||
},
|
||||
{
|
||||
"name": "objectToClassName"
|
||||
},
|
||||
{
|
||||
"name": "prefillHostVars"
|
||||
},
|
||||
{
|
||||
"name": "queueHostBindingForCheck"
|
||||
},
|
||||
{
|
||||
"name": "refreshChildComponents"
|
||||
},
|
||||
@ -416,6 +470,9 @@
|
||||
{
|
||||
"name": "refreshView"
|
||||
},
|
||||
{
|
||||
"name": "registerInitialStylingOnTNode"
|
||||
},
|
||||
{
|
||||
"name": "registerPreOrderHooks"
|
||||
},
|
||||
@ -428,9 +485,15 @@
|
||||
{
|
||||
"name": "renderComponent"
|
||||
},
|
||||
{
|
||||
"name": "renderInitialStyling"
|
||||
},
|
||||
{
|
||||
"name": "renderStringify"
|
||||
},
|
||||
{
|
||||
"name": "renderStylingMap"
|
||||
},
|
||||
{
|
||||
"name": "renderView"
|
||||
},
|
||||
@ -449,6 +512,12 @@
|
||||
{
|
||||
"name": "setBindingRoot"
|
||||
},
|
||||
{
|
||||
"name": "setClass"
|
||||
},
|
||||
{
|
||||
"name": "setClassName"
|
||||
},
|
||||
{
|
||||
"name": "setCurrentDirectiveDef"
|
||||
},
|
||||
@ -464,27 +533,54 @@
|
||||
{
|
||||
"name": "setInjectImplementation"
|
||||
},
|
||||
{
|
||||
"name": "setMapValue"
|
||||
},
|
||||
{
|
||||
"name": "setPreviousOrParentTNode"
|
||||
},
|
||||
{
|
||||
"name": "setSelectedIndex"
|
||||
},
|
||||
{
|
||||
"name": "setStyle"
|
||||
},
|
||||
{
|
||||
"name": "setStyleAttr"
|
||||
},
|
||||
{
|
||||
"name": "setUpAttributes"
|
||||
},
|
||||
{
|
||||
"name": "stringifyForError"
|
||||
},
|
||||
{
|
||||
"name": "stylingMapToString"
|
||||
},
|
||||
{
|
||||
"name": "syncViewWithBlueprint"
|
||||
},
|
||||
{
|
||||
"name": "unwrapRNode"
|
||||
},
|
||||
{
|
||||
"name": "updateRawValueOnContext"
|
||||
},
|
||||
{
|
||||
"name": "viewAttachedToChangeDetector"
|
||||
},
|
||||
{
|
||||
"name": "writeStylingValueDirectly"
|
||||
},
|
||||
{
|
||||
"name": "ɵɵallocHostVars"
|
||||
},
|
||||
{
|
||||
"name": "ɵɵdefineComponent"
|
||||
},
|
||||
{
|
||||
"name": "ɵɵelementHostAttrs"
|
||||
},
|
||||
{
|
||||
"name": "ɵɵtext"
|
||||
}
|
||||
|
@ -647,6 +647,9 @@
|
||||
{
|
||||
"name": "getContextLView"
|
||||
},
|
||||
{
|
||||
"name": "getCurrentDirectiveDef"
|
||||
},
|
||||
{
|
||||
"name": "getCurrentStyleSanitizer"
|
||||
},
|
||||
@ -1079,6 +1082,12 @@
|
||||
{
|
||||
"name": "patchHostStylingFlag"
|
||||
},
|
||||
{
|
||||
"name": "prefillHostVars"
|
||||
},
|
||||
{
|
||||
"name": "queueHostBindingForCheck"
|
||||
},
|
||||
{
|
||||
"name": "readPatchedData"
|
||||
},
|
||||
@ -1358,6 +1367,9 @@
|
||||
{
|
||||
"name": "ɵɵadvance"
|
||||
},
|
||||
{
|
||||
"name": "ɵɵallocHostVars"
|
||||
},
|
||||
{
|
||||
"name": "ɵɵclassProp"
|
||||
},
|
||||
@ -1376,6 +1388,9 @@
|
||||
{
|
||||
"name": "ɵɵelementEnd"
|
||||
},
|
||||
{
|
||||
"name": "ɵɵelementHostAttrs"
|
||||
},
|
||||
{
|
||||
"name": "ɵɵelementStart"
|
||||
},
|
||||
|
119
packages/core/test/render3/util/attr_util_spec.ts
Normal file
119
packages/core/test/render3/util/attr_util_spec.ts
Normal file
@ -0,0 +1,119 @@
|
||||
/**
|
||||
* @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 {AttributeMarker} from '@angular/core/src/render3';
|
||||
import {TAttributes} from '@angular/core/src/render3/interfaces/node';
|
||||
import {mergeHostAttribute, mergeHostAttrs} from '@angular/core/src/render3/util/attrs_utils';
|
||||
import {describe} from '@angular/core/testing/src/testing_internal';
|
||||
|
||||
describe('attr_util', () => {
|
||||
describe('mergeHostAttribute', () => {
|
||||
it('should add new attributes', () => {
|
||||
const attrs: TAttributes = [];
|
||||
mergeHostAttribute(attrs, -1, 'Key', null, 'value');
|
||||
expect(attrs).toEqual(['Key', 'value']);
|
||||
|
||||
mergeHostAttribute(attrs, -1, 'A', null, 'a');
|
||||
expect(attrs).toEqual(['Key', 'value', 'A', 'a']);
|
||||
|
||||
mergeHostAttribute(attrs, -1, 'X', null, 'x');
|
||||
expect(attrs).toEqual(['Key', 'value', 'A', 'a', 'X', 'x']);
|
||||
|
||||
mergeHostAttribute(attrs, -1, 'Key', null, 'new');
|
||||
expect(attrs).toEqual(['Key', 'new', 'A', 'a', 'X', 'x']);
|
||||
});
|
||||
|
||||
it('should add new classes', () => {
|
||||
const attrs: TAttributes = [];
|
||||
mergeHostAttribute(attrs, AttributeMarker.Classes, 'CLASS', null, null);
|
||||
expect(attrs).toEqual([AttributeMarker.Classes, 'CLASS']);
|
||||
|
||||
mergeHostAttribute(attrs, AttributeMarker.Classes, 'A', null, null);
|
||||
expect(attrs).toEqual([AttributeMarker.Classes, 'CLASS', 'A']);
|
||||
|
||||
mergeHostAttribute(attrs, AttributeMarker.Classes, 'X', null, null);
|
||||
expect(attrs).toEqual([AttributeMarker.Classes, 'CLASS', 'A', 'X']);
|
||||
|
||||
mergeHostAttribute(attrs, AttributeMarker.Classes, 'CLASS', null, null);
|
||||
expect(attrs).toEqual([AttributeMarker.Classes, 'CLASS', 'A', 'X']);
|
||||
});
|
||||
|
||||
it('should add new styles', () => {
|
||||
const attrs: TAttributes = [];
|
||||
mergeHostAttribute(attrs, AttributeMarker.Styles, 'Style', null, 'v1');
|
||||
expect(attrs).toEqual([AttributeMarker.Styles, 'Style', 'v1']);
|
||||
|
||||
mergeHostAttribute(attrs, AttributeMarker.Styles, 'A', null, 'v2');
|
||||
expect(attrs).toEqual([AttributeMarker.Styles, 'Style', 'v1', 'A', 'v2']);
|
||||
|
||||
mergeHostAttribute(attrs, AttributeMarker.Styles, 'X', null, 'v3');
|
||||
expect(attrs).toEqual([AttributeMarker.Styles, 'Style', 'v1', 'A', 'v2', 'X', 'v3']);
|
||||
|
||||
mergeHostAttribute(attrs, AttributeMarker.Styles, 'Style', null, 'new');
|
||||
expect(attrs).toEqual([AttributeMarker.Styles, 'Style', 'new', 'A', 'v2', 'X', 'v3']);
|
||||
});
|
||||
|
||||
it('should keep different types together', () => {
|
||||
const attrs: TAttributes = [];
|
||||
mergeHostAttribute(attrs, -1, 'Key', null, 'value');
|
||||
expect(attrs).toEqual(['Key', 'value']);
|
||||
|
||||
mergeHostAttribute(attrs, AttributeMarker.Classes, 'CLASS', null, null);
|
||||
expect(attrs).toEqual(['Key', 'value', AttributeMarker.Classes, 'CLASS']);
|
||||
|
||||
mergeHostAttribute(attrs, AttributeMarker.Styles, 'Style', null, 'v1');
|
||||
expect(attrs).toEqual([
|
||||
'Key', 'value', AttributeMarker.Classes, 'CLASS', AttributeMarker.Styles, 'Style', 'v1'
|
||||
]);
|
||||
|
||||
mergeHostAttribute(attrs, -1, 'Key2', null, 'value2');
|
||||
expect(attrs).toEqual([
|
||||
'Key', 'value', 'Key2', 'value2', AttributeMarker.Classes, 'CLASS', AttributeMarker.Styles,
|
||||
'Style', 'v1'
|
||||
]);
|
||||
|
||||
mergeHostAttribute(attrs, AttributeMarker.Classes, 'CLASS2', null, null);
|
||||
expect(attrs).toEqual([
|
||||
'Key', 'value', 'Key2', 'value2', AttributeMarker.Classes, 'CLASS', 'CLASS2',
|
||||
AttributeMarker.Styles, 'Style', 'v1'
|
||||
]);
|
||||
|
||||
mergeHostAttribute(attrs, AttributeMarker.Styles, 'Style2', null, 'v2');
|
||||
expect(attrs).toEqual([
|
||||
'Key', 'value', 'Key2', 'value2', AttributeMarker.Classes, 'CLASS', 'CLASS2',
|
||||
AttributeMarker.Styles, 'Style', 'v1', 'Style2', 'v2'
|
||||
]);
|
||||
|
||||
mergeHostAttribute(attrs, AttributeMarker.NamespaceURI, 'uri', 'key', 'value');
|
||||
expect(attrs).toEqual([
|
||||
'Key', 'value', 'Key2', 'value2', AttributeMarker.NamespaceURI, 'uri', 'key', 'value',
|
||||
AttributeMarker.Classes, 'CLASS', 'CLASS2', AttributeMarker.Styles, 'Style', 'v1', 'Style2',
|
||||
'v2'
|
||||
]);
|
||||
mergeHostAttribute(attrs, AttributeMarker.NamespaceURI, 'uri', 'key', 'new value');
|
||||
expect(attrs).toEqual([
|
||||
'Key', 'value', 'Key2', 'value2', AttributeMarker.NamespaceURI, 'uri', 'key', 'new value',
|
||||
AttributeMarker.Classes, 'CLASS', 'CLASS2', AttributeMarker.Styles, 'Style', 'v1', 'Style2',
|
||||
'v2'
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeHostAttrs', () => {
|
||||
it('should ignore nulls/empty', () => {
|
||||
expect(mergeHostAttrs(null, null)).toEqual(null);
|
||||
expect(mergeHostAttrs([], null)).toEqual([]);
|
||||
expect(mergeHostAttrs(null, [])).toEqual(null);
|
||||
});
|
||||
|
||||
it('should copy if dst is null', () => {
|
||||
expect(mergeHostAttrs(null, ['K', 'v'])).toEqual(['K', 'v']);
|
||||
expect(mergeHostAttrs(['K', '', 'X', 'x'], ['K', 'v'])).toEqual(['K', 'v', 'X', 'x']);
|
||||
});
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user