...
+ * ```
+ *
+ * the generated code is:
+ * ```
+ * var _c1 = [AttributeMarker.Classes, 'foo', 'bar', 'baz'];
+ * ```
+ */
+ Classes = 1,
+
+ /**
+ * Signals style declaration.
+ *
+ * Each pair of values following `Styles` designates a style name and value to include on the
+ * element.
+ * ## Example:
+ *
+ * Given:
+ * ```
+ *
...
+ * ```
+ *
+ * the generated code is:
+ * ```
+ * var _c1 = [AttributeMarker.Styles, 'width', '100px', 'height'. '200px', 'color', 'red'];
+ * ```
+ */
+ Styles = 2,
+
/**
* This marker indicates that the following attribute names were extracted from bindings (ex.:
* [foo]="exp") and / or event handlers (ex. (bar)="doSth()").
* Taking the above bindings and outputs as an example an attributes array could look as follows:
* ['class', 'fade in', AttributeMarker.SelectOnly, 'foo', 'bar']
*/
- SelectOnly = 1
-}
\ No newline at end of file
+ SelectOnly = 3,
+}
diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts
index 79a29effdf..33422adde6 100644
--- a/packages/compiler/src/render3/r3_identifiers.ts
+++ b/packages/compiler/src/render3/r3_identifiers.ts
@@ -43,6 +43,8 @@ export class Identifiers {
static elementStyling: o.ExternalReference = {name: 'ɵelementStyling', moduleName: CORE};
+ static elementHostAttrs: o.ExternalReference = {name: 'ɵelementHostAttrs', moduleName: CORE};
+
static elementStylingMap: o.ExternalReference = {name: 'ɵelementStylingMap', moduleName: CORE};
static elementStyleProp: o.ExternalReference = {name: 'ɵelementStyleProp', moduleName: CORE};
diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts
index 22705300ff..9b37d76c2b 100644
--- a/packages/compiler/src/render3/view/compiler.ts
+++ b/packages/compiler/src/render3/view/compiler.ts
@@ -28,7 +28,7 @@ import {Render3ParseResult} from '../r3_template_transform';
import {typeWithParameters} from '../util';
import {R3ComponentDef, R3ComponentMetadata, R3DirectiveDef, R3DirectiveMetadata, R3QueryMetadata} from './api';
-import {StylingBuilder, StylingInstruction} from './styling';
+import {StylingBuilder, StylingInstruction} from './styling_builder';
import {BindingScope, TemplateDefinitionBuilder, ValueConverter, renderFlagCheckIfStmt} from './template';
import {CONTEXT_NAME, DefinitionMap, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, conditionallyCreateMapObjectLiteral, getQueryPredicate, temporaryAllocator} from './util';
@@ -709,16 +709,35 @@ function createHostBindingsFunction(
}
}
- if (styleBuilder.hasBindingsOrInitialValues) {
- const createInstruction = styleBuilder.buildCreateLevelInstruction(null, constantPool);
- if (createInstruction) {
- const createStmt = createStylingStmt(createInstruction, bindingContext, bindingFn);
- createStatements.push(createStmt);
+ if (styleBuilder.hasBindingsOrInitialValues()) {
+ // since we're dealing with directives here and directives have a hostBinding
+ // function, we need to generate special instructions that deal with styling
+ // (both bindings and initial values). The instruction below will instruct
+ // all initial styling (styling that is inside of a host binding within a
+ // directive) to be attached to the host element of the directive.
+ const hostAttrsInstruction =
+ styleBuilder.buildDirectiveHostAttrsInstruction(null, constantPool);
+ if (hostAttrsInstruction) {
+ createStatements.push(createStylingStmt(hostAttrsInstruction, bindingContext, bindingFn));
}
+ // singular style/class bindings (things like `[style.prop]` and `[class.name]`)
+ // MUST be registered on a given element within the component/directive
+ // templateFn/hostBindingsFn functions. The instruction below will figure out
+ // what all the bindings are and then generate the statements required to register
+ // those bindings to the element via `elementStyling`.
+ const elementStylingInstruction =
+ styleBuilder.buildElementStylingInstruction(null, constantPool);
+ if (elementStylingInstruction) {
+ createStatements.push(
+ createStylingStmt(elementStylingInstruction, bindingContext, bindingFn));
+ }
+
+ // finally each binding that was registered in the statement above will need to be added to
+ // the update block of a component/directive templateFn/hostBindingsFn so that the bindings
+ // are evaluated and updated for the element.
styleBuilder.buildUpdateLevelInstructions(valueConverter).forEach(instruction => {
- const updateStmt = createStylingStmt(instruction, bindingContext, bindingFn);
- updateStatements.push(updateStmt);
+ updateStatements.push(createStylingStmt(instruction, bindingContext, bindingFn));
});
}
}
diff --git a/packages/compiler/src/render3/view/style_parser.ts b/packages/compiler/src/render3/view/style_parser.ts
index 28d38c6f4f..97b3f8c83f 100644
--- a/packages/compiler/src/render3/view/style_parser.ts
+++ b/packages/compiler/src/render3/view/style_parser.ts
@@ -23,10 +23,15 @@ const enum Char {
*
* @param value string representation of style as used in the `style` attribute in HTML.
* Example: `color: red; height: auto`.
- * @returns an object literal. `{ color: 'red', height: 'auto'}`.
+ * @returns An array of style property name and value pairs, e.g. `['color', 'red', 'height',
+ * 'auto']`
*/
-export function parse(value: string): {[key: string]: any} {
- const styles: {[key: string]: any} = {};
+export function parse(value: string): string[] {
+ // we use a string array here instead of a string map
+ // because a string-map is not guaranteed to retain the
+ // order of the entries whereas a string array can be
+ // construted in a [key, value, key, value] format.
+ const styles: string[] = [];
let i = 0;
let parenDepth = 0;
@@ -72,7 +77,7 @@ export function parse(value: string): {[key: string]: any} {
case Char.Semicolon:
if (currentProp && valueStart > 0 && parenDepth === 0 && quote === Char.QuoteNone) {
const styleVal = value.substring(valueStart, i - 1).trim();
- styles[currentProp] = valueHasQuotes ? stripUnnecessaryQuotes(styleVal) : styleVal;
+ styles.push(currentProp, valueHasQuotes ? stripUnnecessaryQuotes(styleVal) : styleVal);
propStart = i;
valueStart = 0;
currentProp = null;
@@ -84,7 +89,7 @@ export function parse(value: string): {[key: string]: any} {
if (currentProp && valueStart) {
const styleVal = value.substr(valueStart).trim();
- styles[currentProp] = valueHasQuotes ? stripUnnecessaryQuotes(styleVal) : styleVal;
+ styles.push(currentProp, valueHasQuotes ? stripUnnecessaryQuotes(styleVal) : styleVal);
}
return styles;
diff --git a/packages/compiler/src/render3/view/styling.ts b/packages/compiler/src/render3/view/styling_builder.ts
similarity index 53%
rename from packages/compiler/src/render3/view/styling.ts
rename to packages/compiler/src/render3/view/styling_builder.ts
index 983a5f1db3..aebc989304 100644
--- a/packages/compiler/src/render3/view/styling.ts
+++ b/packages/compiler/src/render3/view/styling_builder.ts
@@ -6,8 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ConstantPool} from '../../constant_pool';
-import {InitialStylingFlags} from '../../core';
-import {AST, BindingType, ParseSpan} from '../../expression_parser/ast';
+import {AttributeMarker} from '../../core';
+import {AST, BindingType} from '../../expression_parser/ast';
import * as o from '../../output/output_ast';
import {ParseSourceSpan} from '../../parse_util';
import * as t from '../r3_ast';
@@ -40,6 +40,10 @@ interface BoundStylingEntry {
/**
* Produces creation/update instructions for all styling bindings (class and style)
*
+ * It also produces the creation instruction to register all initial styling values
+ * (which are all the static class="..." and style="..." attribute values that exist
+ * on an element within a template).
+ *
* The builder class below handles producing instructions for the following cases:
*
* - Static style/class attributes (style="..." and class="...")
@@ -63,25 +67,57 @@ interface BoundStylingEntry {
* The creation/update methods within the builder class produce these instructions.
*/
export class StylingBuilder {
- public readonly hasBindingsOrInitialValues = false;
+ /** Whether or not there are any static styling values present */
+ private _hasInitialValues = false;
+ /**
+ * Whether or not there are any styling bindings present
+ * (i.e. `[style]`, `[class]`, `[style.prop]` or `[class.name]`)
+ */
+ private _hasBindings = false;
+ /** the input for [class] (if it exists) */
private _classMapInput: BoundStylingEntry|null = null;
+ /** the input for [style] (if it exists) */
private _styleMapInput: BoundStylingEntry|null = null;
+ /** an array of each [style.prop] input */
private _singleStyleInputs: BoundStylingEntry[]|null = null;
+ /** an array of each [class.name] input */
private _singleClassInputs: BoundStylingEntry[]|null = null;
private _lastStylingInput: BoundStylingEntry|null = null;
// maps are used instead of hash maps because a Map will
// retain the ordering of the keys
+
+ /**
+ * Represents the location of each style binding in the template
+ * (e.g. `
` implies
+ * that `width=0` and `height=1`)
+ */
private _stylesIndex = new Map();
+
+ /**
+ * Represents the location of each class binding in the template
+ * (e.g. `
` implies
+ * that `big=0` and `hidden=1`)
+ */
private _classesIndex = new Map();
- private _initialStyleValues: {[propName: string]: string} = {};
- private _initialClassValues: {[className: string]: boolean} = {};
+ private _initialStyleValues: string[] = [];
+ private _initialClassValues: string[] = [];
+
+ // certain style properties ALWAYS need sanitization
+ // this is checked each time new styles are encountered
private _useDefaultSanitizer = false;
- private _applyFnRequired = false;
constructor(private _elementIndexExpr: o.Expression, private _directiveExpr: o.Expression|null) {}
+ hasBindingsOrInitialValues() { return this._hasBindings || this._hasInitialValues; }
+
+ /**
+ * Registers a given input to the styling builder to be later used when producing AOT code.
+ *
+ * The code below will only accept the input if it is somehow tied to styling (whether it be
+ * style/class bindings or static style/class attributes).
+ */
registerBoundInput(input: t.BoundAttribute): boolean {
// [attr.style] or [attr.class] are skipped in the code below,
// they should not be treated as styling-based bindings since
@@ -117,14 +153,12 @@ export class StylingBuilder {
(this._singleStyleInputs = this._singleStyleInputs || []).push(entry);
this._useDefaultSanitizer = this._useDefaultSanitizer || isStyleSanitizable(propertyName);
registerIntoMap(this._stylesIndex, propertyName);
- (this as any).hasBindingsOrInitialValues = true;
} else {
this._useDefaultSanitizer = true;
this._styleMapInput = entry;
}
this._lastStylingInput = entry;
- (this as any).hasBindingsOrInitialValues = true;
- this._applyFnRequired = true;
+ this._hasBindings = true;
return entry;
}
@@ -133,107 +167,152 @@ export class StylingBuilder {
const entry = { name: className, value, sourceSpan } as BoundStylingEntry;
if (className) {
(this._singleClassInputs = this._singleClassInputs || []).push(entry);
- (this as any).hasBindingsOrInitialValues = true;
registerIntoMap(this._classesIndex, className);
} else {
this._classMapInput = entry;
}
this._lastStylingInput = entry;
- (this as any).hasBindingsOrInitialValues = true;
- this._applyFnRequired = true;
+ this._hasBindings = true;
return entry;
}
+ /**
+ * Registers the element's static style string value to the builder.
+ *
+ * @param value the style string (e.g. `width:100px; height:200px;`)
+ */
registerStyleAttr(value: string) {
this._initialStyleValues = parseStyle(value);
- Object.keys(this._initialStyleValues).forEach(prop => {
- registerIntoMap(this._stylesIndex, prop);
- (this as any).hasBindingsOrInitialValues = true;
- });
+ this._hasInitialValues = true;
}
+ /**
+ * Registers the element's static class string value to the builder.
+ *
+ * @param value the className string (e.g. `disabled gold zoom`)
+ */
registerClassAttr(value: string) {
- this._initialClassValues = {};
- value.split(/\s+/g).forEach(className => {
- this._initialClassValues[className] = true;
- registerIntoMap(this._classesIndex, className);
- (this as any).hasBindingsOrInitialValues = true;
- });
+ this._initialClassValues = value.trim().split(/\s+/g);
+ this._hasInitialValues = true;
}
- private _buildInitExpr(registry: Map, initialValues: {[key: string]: any}):
- o.Expression|null {
- const exprs: o.Expression[] = [];
- const nameAndValueExprs: o.Expression[] = [];
-
- // _c0 = [prop, prop2, prop3, ...]
- registry.forEach((value, key) => {
- const keyLiteral = o.literal(key);
- exprs.push(keyLiteral);
- const initialValue = initialValues[key];
- if (initialValue) {
- nameAndValueExprs.push(keyLiteral, o.literal(initialValue));
+ /**
+ * Appends all styling-related expressions to the provided attrs array.
+ *
+ * @param attrs an existing array where each of the styling expressions
+ * will be inserted into.
+ */
+ populateInitialStylingAttrs(attrs: o.Expression[]): void {
+ // [CLASS_MARKER, 'foo', 'bar', 'baz' ...]
+ if (this._initialClassValues.length) {
+ attrs.push(o.literal(AttributeMarker.Classes));
+ for (let i = 0; i < this._initialClassValues.length; i++) {
+ attrs.push(o.literal(this._initialClassValues[i]));
}
- });
-
- if (nameAndValueExprs.length) {
- // _c0 = [... MARKER ...]
- exprs.push(o.literal(InitialStylingFlags.VALUES_MODE));
- // _c0 = [prop, VALUE, prop2, VALUE2, ...]
- exprs.push(...nameAndValueExprs);
}
- return exprs.length ? o.literalArr(exprs) : null;
+ // [STYLE_MARKER, 'width', '200px', 'height', '100px', ...]
+ if (this._initialStyleValues.length) {
+ attrs.push(o.literal(AttributeMarker.Styles));
+ for (let i = 0; i < this._initialStyleValues.length; i += 2) {
+ attrs.push(
+ o.literal(this._initialStyleValues[i]), o.literal(this._initialStyleValues[i + 1]));
+ }
+ }
}
- buildCreateLevelInstruction(sourceSpan: ParseSourceSpan|null, constantPool: ConstantPool):
+ /**
+ * Builds an instruction with all the expressions and parameters for `elementHostAttrs`.
+ *
+ * The instruction generation code below is used for producing the AOT statement code which is
+ * responsible for registering initial styles (within a directive hostBindings' creation block)
+ * to the directive host element.
+ */
+ buildDirectiveHostAttrsInstruction(sourceSpan: ParseSourceSpan|null, constantPool: ConstantPool):
StylingInstruction|null {
- if (this.hasBindingsOrInitialValues) {
- const initialClasses = this._buildInitExpr(this._classesIndex, this._initialClassValues);
- const initialStyles = this._buildInitExpr(this._stylesIndex, this._initialStyleValues);
-
- // in the event that a [style] binding is used then sanitization will
- // always be imported because it is not possible to know ahead of time
- // whether style bindings will use or not use any sanitizable properties
- // that isStyleSanitizable() will detect
- const useSanitizer = this._useDefaultSanitizer;
- const params: (o.Expression)[] = [];
-
- if (initialClasses) {
- // the template compiler handles initial class styling (e.g. class="foo") values
- // in a special command called `elementClass` so that the initial class
- // can be processed during runtime. These initial class values are bound to
- // a constant because the inital class values do not change (since they're static).
- params.push(constantPool.getConstLiteral(initialClasses, true));
- } else if (initialStyles || useSanitizer || this._directiveExpr) {
- // no point in having an extra `null` value unless there are follow-up params
- params.push(o.NULL_EXPR);
- }
-
- if (initialStyles) {
- // the template compiler handles initial style (e.g. style="foo") values
- // in a special command called `elementStyle` so that the initial styles
- // can be processed during runtime. These initial styles values are bound to
- // a constant because the inital style values do not change (since they're static).
- params.push(constantPool.getConstLiteral(initialStyles, true));
- } else if (useSanitizer || this._directiveExpr) {
- // no point in having an extra `null` value unless there are follow-up params
- params.push(o.NULL_EXPR);
- }
-
- if (useSanitizer || this._directiveExpr) {
- params.push(useSanitizer ? o.importExpr(R3.defaultStyleSanitizer) : o.NULL_EXPR);
- if (this._directiveExpr) {
- params.push(this._directiveExpr);
+ if (this._hasInitialValues && this._directiveExpr) {
+ return {
+ sourceSpan,
+ reference: R3.elementHostAttrs,
+ buildParams: () => {
+ const attrs: o.Expression[] = [];
+ this.populateInitialStylingAttrs(attrs);
+ return [this._directiveExpr !, getConstantLiteralFromArray(constantPool, attrs)];
}
- }
-
- return {sourceSpan, reference: R3.elementStyling, buildParams: () => params};
+ };
}
return null;
}
- private _buildStylingMap(valueConverter: ValueConverter): StylingInstruction|null {
+ /**
+ * Builds an instruction with all the expressions and parameters for `elementStyling`.
+ *
+ * The instruction generation code below is used for producing the AOT statement code which is
+ * responsible for registering style/class bindings to an element.
+ */
+ buildElementStylingInstruction(sourceSpan: ParseSourceSpan|null, constantPool: ConstantPool):
+ StylingInstruction|null {
+ if (this._hasBindings) {
+ return {
+ sourceSpan,
+ reference: R3.elementStyling,
+ buildParams: () => {
+ // a string array of every style-based binding
+ const styleBindingProps =
+ this._singleStyleInputs ? this._singleStyleInputs.map(i => o.literal(i.name)) : [];
+ // a string array of every class-based binding
+ const classBindingNames =
+ this._singleClassInputs ? this._singleClassInputs.map(i => o.literal(i.name)) : [];
+
+ // to salvage space in the AOT generated code, there is no point in passing
+ // in `null` into a param if any follow-up params are not used. Therefore,
+ // only when a trailing param is used then it will be filled with nulls in between
+ // (otherwise a shorter amount of params will be filled). The code below helps
+ // determine how many params are required in the expression code.
+ //
+ // min params => elementStyling()
+ // max params => elementStyling(classBindings, styleBindings, sanitizer, directive)
+ let expectedNumberOfArgs = 0;
+ if (this._directiveExpr) {
+ expectedNumberOfArgs = 4;
+ } else if (this._useDefaultSanitizer) {
+ expectedNumberOfArgs = 3;
+ } else if (styleBindingProps.length) {
+ expectedNumberOfArgs = 2;
+ } else if (classBindingNames.length) {
+ expectedNumberOfArgs = 1;
+ }
+
+ const params: o.Expression[] = [];
+ addParam(
+ params, classBindingNames.length > 0,
+ getConstantLiteralFromArray(constantPool, classBindingNames), 1,
+ expectedNumberOfArgs);
+ addParam(
+ params, styleBindingProps.length > 0,
+ getConstantLiteralFromArray(constantPool, styleBindingProps), 2,
+ expectedNumberOfArgs);
+ addParam(
+ params, this._useDefaultSanitizer, o.importExpr(R3.defaultStyleSanitizer), 3,
+ expectedNumberOfArgs);
+ if (this._directiveExpr) {
+ params.push(this._directiveExpr);
+ }
+ return params;
+ }
+ };
+ }
+ return null;
+ }
+
+ /**
+ * Builds an instruction with all the expressions and parameters for `elementStylingMap`.
+ *
+ * The instruction data will contain all expressions for `elementStylingMap` to function
+ * which include the `[style]` and `[class]` expression params (if they exist) as well as
+ * the sanitizer and directive reference expression.
+ */
+ buildElementStylingMapInstruction(valueConverter: ValueConverter): StylingInstruction|null {
if (this._classMapInput || this._styleMapInput) {
const stylingInput = this._classMapInput ! || this._styleMapInput !;
@@ -332,18 +411,20 @@ export class StylingBuilder {
};
}
+ /**
+ * Constructs all instructions which contain the expressions that will be placed
+ * into the update block of a template function or a directive hostBindings function.
+ */
buildUpdateLevelInstructions(valueConverter: ValueConverter) {
const instructions: StylingInstruction[] = [];
- if (this.hasBindingsOrInitialValues) {
- const mapInstruction = this._buildStylingMap(valueConverter);
+ if (this._hasBindings) {
+ const mapInstruction = this.buildElementStylingMapInstruction(valueConverter);
if (mapInstruction) {
instructions.push(mapInstruction);
}
instructions.push(...this._buildStyleInputs(valueConverter));
instructions.push(...this._buildClassInputs(valueConverter));
- if (this._applyFnRequired) {
- instructions.push(this._buildApplyFn());
- }
+ instructions.push(this._buildApplyFn());
}
return instructions;
}
@@ -363,3 +444,26 @@ function isStyleSanitizable(prop: string): boolean {
return prop === 'background-image' || prop === 'background' || prop === 'border-image' ||
prop === 'filter' || prop === 'list-style' || prop === 'list-style-image';
}
+
+/**
+ * Simple helper function to either provide the constant literal that will house the value
+ * here or a null value if the provided values are empty.
+ */
+function getConstantLiteralFromArray(
+ constantPool: ConstantPool, values: o.Expression[]): o.Expression {
+ return values.length ? constantPool.getConstLiteral(o.literalArr(values), true) : o.NULL_EXPR;
+}
+
+/**
+ * Simple helper function that adds a parameter or does nothing at all depending on the provided
+ * predicate and totalExpectedArgs values
+ */
+function addParam(
+ params: o.Expression[], predicate: boolean, value: o.Expression, argNumber: number,
+ totalExpectedArgs: number) {
+ if (predicate) {
+ params.push(value);
+ } else if (argNumber < totalExpectedArgs) {
+ params.push(o.NULL_EXPR);
+ }
+}
diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts
index 9ec9f62ab8..188c0e4712 100644
--- a/packages/compiler/src/render3/view/template.ts
+++ b/packages/compiler/src/render3/view/template.ts
@@ -35,7 +35,7 @@ import {I18nContext} from './i18n/context';
import {I18nMetaVisitor} from './i18n/meta';
import {getSerializedI18nContent} from './i18n/serializer';
import {I18N_ICU_MAPPING_PREFIX, assembleBoundTextPlaceholders, assembleI18nBoundString, formatI18nPlaceholderName, getTranslationConstPrefix, getTranslationDeclStmts, icuFromI18nMessage, isI18nRootNode, isSingleI18nIcu, metaFromI18nMessage, placeholdersToParams, wrapI18nPlaceholder} from './i18n/util';
-import {StylingBuilder, StylingInstruction} from './styling';
+import {StylingBuilder, StylingInstruction} from './styling_builder';
import {CONTEXT_NAME, IMPLICIT_REFERENCE, NON_BINDABLE_ATTR, REFERENCE_PREFIX, RENDER_FLAGS, asLiteral, getAttrsForDirectiveMatching, invalid, trimTrailingNulls, unsupported} from './util';
function mapBindingToInstruction(type: BindingType): o.ExternalReference|undefined {
@@ -532,7 +532,8 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
// this will build the instructions so that they fall into the following syntax
// add attributes for directive matching purposes
- attributes.push(...this.prepareSyntheticAndSelectOnlyAttrs(allOtherInputs, element.outputs));
+ attributes.push(...this.prepareSyntheticAndSelectOnlyAttrs(
+ allOtherInputs, element.outputs, stylingBuilder));
parameters.push(this.toAttrsParam(attributes));
// local refs (ex.:
)
@@ -562,11 +563,11 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
return element.children.length > 0;
};
- const createSelfClosingInstruction = !stylingBuilder.hasBindingsOrInitialValues &&
+ const createSelfClosingInstruction = !stylingBuilder.hasBindingsOrInitialValues() &&
!isNgContainer && element.outputs.length === 0 && i18nAttrs.length === 0 && !hasChildren();
const createSelfClosingI18nInstruction = !createSelfClosingInstruction &&
- !stylingBuilder.hasBindingsOrInitialValues && hasTextChildrenOnly(element.children);
+ !stylingBuilder.hasBindingsOrInitialValues() && hasTextChildrenOnly(element.children);
if (createSelfClosingInstruction) {
this.creationInstruction(element.sourceSpan, R3.element, trimTrailingNulls(parameters));
@@ -616,10 +617,16 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
}
}
- // initial styling for static style="..." and class="..." attributes
+ // The style bindings code is placed into two distinct blocks within the template function AOT
+ // code: creation and update. The creation code contains the `elementStyling` instructions
+ // which will apply the collected binding values to the element. `elementStyling` is
+ // designed to run inside of `elementStart` and `elementEnd`. The update instructions
+ // (things like `elementStyleProp`, `elementClassProp`, etc..) are applied later on in this
+ // file
this.processStylingInstruction(
implicit,
- stylingBuilder.buildCreateLevelInstruction(element.sourceSpan, this.constantPool), true);
+ stylingBuilder.buildElementStylingInstruction(element.sourceSpan, this.constantPool),
+ true);
// Generate Listeners (outputs)
element.outputs.forEach((outputAst: t.BoundEvent) => {
@@ -629,6 +636,10 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
});
}
+ // the code here will collect all update-level styling instructions and add them to the
+ // update block of the template function AOT code. Instructions like `elementStyleProp`,
+ // `elementStylingMap`, `elementClassProp` and `elementStylingApply` are all generated
+ // and assign in the code below.
stylingBuilder.buildUpdateLevelInstructions(this._valueConverter).forEach(instruction => {
this.processStylingInstruction(implicit, instruction, false);
});
@@ -934,8 +945,26 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
}
}
- private prepareSyntheticAndSelectOnlyAttrs(inputs: t.BoundAttribute[], outputs: t.BoundEvent[]):
- o.Expression[] {
+ /**
+ * Prepares all attribute expression values for the `TAttributes` array.
+ *
+ * The purpose of this function is to properly construct an attributes array that
+ * is passed into the `elementStart` (or just `element`) functions. Because there
+ * are many different types of attributes, the array needs to be constructed in a
+ * special way so that `elementStart` can properly evaluate them.
+ *
+ * The format looks like this:
+ *
+ * ```
+ * attrs = [prop, value, prop2, value2,
+ * CLASSES, class1, class2,
+ * STYLES, style1, value1, style2, value2,
+ * SELECT_ONLY, name1, name2, name2, ...]
+ * ```
+ */
+ private prepareSyntheticAndSelectOnlyAttrs(
+ inputs: t.BoundAttribute[], outputs: t.BoundEvent[],
+ styles?: StylingBuilder): o.Expression[] {
const attrExprs: o.Expression[] = [];
const nonSyntheticInputs: t.BoundAttribute[] = [];
@@ -954,6 +983,13 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
});
}
+ // it's important that this occurs before SelectOnly because once `elementStart`
+ // comes across the SelectOnly marker then it will continue reading each value as
+ // as single property value cell by cell.
+ if (styles) {
+ styles.populateInitialStylingAttrs(attrExprs);
+ }
+
if (nonSyntheticInputs.length || outputs.length) {
attrExprs.push(o.literal(core.AttributeMarker.SelectOnly));
nonSyntheticInputs.forEach((i: t.BoundAttribute) => attrExprs.push(asLiteral(i.name)));
diff --git a/packages/compiler/test/render3/style_parser_spec.ts b/packages/compiler/test/render3/style_parser_spec.ts
index d0f9e4ae11..9fe365bca9 100644
--- a/packages/compiler/test/render3/style_parser_spec.ts
+++ b/packages/compiler/test/render3/style_parser_spec.ts
@@ -10,55 +10,53 @@ import {hyphenate, parse as parseStyle, stripUnnecessaryQuotes} from '../../src/
describe('style parsing', () => {
it('should parse empty or blank strings', () => {
const result1 = parseStyle('');
- expect(result1).toEqual({});
+ expect(result1).toEqual([]);
const result2 = parseStyle(' ');
- expect(result2).toEqual({});
+ expect(result2).toEqual([]);
});
it('should parse a string into a key/value map', () => {
const result = parseStyle('width:100px;height:200px;opacity:0');
- expect(result).toEqual({width: '100px', height: '200px', opacity: '0'});
+ expect(result).toEqual(['width', '100px', 'height', '200px', 'opacity', '0']);
});
it('should trim values and properties', () => {
const result = parseStyle('width :333px ; height:666px ; opacity: 0.5;');
- expect(result).toEqual({width: '333px', height: '666px', opacity: '0.5'});
+ expect(result).toEqual(['width', '333px', 'height', '666px', 'opacity', '0.5']);
});
it('should chomp out start/end quotes', () => {
const result = parseStyle(
'content: "foo"; opacity: \'0.5\'; font-family: "Verdana", Helvetica, "sans-serif"');
expect(result).toEqual(
- {content: 'foo', opacity: '0.5', 'font-family': '"Verdana", Helvetica, "sans-serif"'});
+ ['content', 'foo', 'opacity', '0.5', 'font-family', '"Verdana", Helvetica, "sans-serif"']);
});
it('should not mess up with quoted strings that contain [:;] values', () => {
const result = parseStyle('content: "foo; man: guy"; width: 100px');
- expect(result).toEqual({content: 'foo; man: guy', width: '100px'});
+ expect(result).toEqual(['content', 'foo; man: guy', 'width', '100px']);
});
it('should not mess up with quoted strings that contain inner quote values', () => {
const quoteStr = '"one \'two\' three \"four\" five"';
const result = parseStyle(`content: ${quoteStr}; width: 123px`);
- expect(result).toEqual({content: quoteStr, width: '123px'});
+ expect(result).toEqual(['content', quoteStr, 'width', '123px']);
});
it('should respect parenthesis that are placed within a style', () => {
const result = parseStyle('background-image: url("foo.jpg")');
- expect(result).toEqual({'background-image': 'url("foo.jpg")'});
+ expect(result).toEqual(['background-image', 'url("foo.jpg")']);
});
it('should respect multi-level parenthesis that contain special [:;] characters', () => {
const result = parseStyle('color: rgba(calc(50 * 4), var(--cool), :5;); height: 100px;');
- expect(result).toEqual({color: 'rgba(calc(50 * 4), var(--cool), :5;)', height: '100px'});
+ expect(result).toEqual(['color', 'rgba(calc(50 * 4), var(--cool), :5;)', 'height', '100px']);
});
it('should hyphenate style properties from camel case', () => {
const result = parseStyle('borderWidth: 200px');
- expect(result).toEqual({
- 'border-width': '200px',
- });
+ expect(result).toEqual(['border-width', '200px']);
});
describe('quote chomping', () => {
diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts
index cca9436342..f384eba657 100644
--- a/packages/core/src/core_render3_private_export.ts
+++ b/packages/core/src/core_render3_private_export.ts
@@ -92,6 +92,7 @@ export {
elementContainerStart as ɵelementContainerStart,
elementContainerEnd as ɵelementContainerEnd,
elementStyling as ɵelementStyling,
+ elementHostAttrs as ɵelementHostAttrs,
elementStylingMap as ɵelementStylingMap,
elementStyleProp as ɵelementStyleProp,
elementStylingApply as ɵelementStylingApply,
diff --git a/packages/core/src/debug/debug_node.ts b/packages/core/src/debug/debug_node.ts
index 9623b44846..54c2c6cd70 100644
--- a/packages/core/src/debug/debug_node.ts
+++ b/packages/core/src/debug/debug_node.ts
@@ -12,7 +12,7 @@ import {getComponent, getContext, getInjectionTokens, getInjector, getListeners,
import {TNode} from '../render3/interfaces/node';
import {StylingIndex} from '../render3/interfaces/styling';
import {TVIEW} from '../render3/interfaces/view';
-import {getProp, getValue, isClassBased} from '../render3/styling/class_and_style_bindings';
+import {getProp, getValue, isClassBasedValue} from '../render3/styling/class_and_style_bindings';
import {getStylingContext} from '../render3/styling/util';
import {DebugContext} from '../view/index';
@@ -273,7 +273,7 @@ class DebugElement__POST_R3__ extends DebugNode__POST_R3__ implements DebugEleme
if (stylingContext) {
for (let i = StylingIndex.SingleStylesStartPosition; i < lNode.length;
i += StylingIndex.Size) {
- if (isClassBased(lNode, i)) {
+ if (isClassBasedValue(lNode, i)) {
const className = getProp(lNode, i);
const value = getValue(lNode, i);
if (typeof value == 'boolean') {
@@ -303,7 +303,7 @@ class DebugElement__POST_R3__ extends DebugNode__POST_R3__ implements DebugEleme
if (stylingContext) {
for (let i = StylingIndex.SingleStylesStartPosition; i < lNode.length;
i += StylingIndex.Size) {
- if (!isClassBased(lNode, i)) {
+ if (!isClassBasedValue(lNode, i)) {
const styleName = getProp(lNode, i);
const value = getValue(lNode, i) as string | null;
if (value !== null) {
diff --git a/packages/core/src/render3/context_discovery.ts b/packages/core/src/render3/context_discovery.ts
index 6ce8791ba4..651f37cf20 100644
--- a/packages/core/src/render3/context_discovery.ts
+++ b/packages/core/src/render3/context_discovery.ts
@@ -7,12 +7,13 @@
*/
import './ng_dev_mode';
import {assertDomNode} from './assert';
-import {EMPTY_ARRAY} from './definition';
+import {EMPTY_ARRAY} from './empty';
import {LContext, MONKEY_PATCH_KEY_NAME} from './interfaces/context';
import {TNode, TNodeFlags} from './interfaces/node';
import {RElement} from './interfaces/renderer';
import {CONTEXT, HEADER_OFFSET, HOST, LView, TVIEW} from './interfaces/view';
-import {getComponentViewByIndex, getNativeByTNode, getTNode, readElementValue, readPatchedData} from './util';
+import {getComponentViewByIndex, getNativeByTNode, readElementValue, readPatchedData} from './util';
+
/** Returns the matching `LContext` data for a given DOM node, directive or component instance.
diff --git a/packages/core/src/render3/definition.ts b/packages/core/src/render3/definition.ts
index 52843b367e..3d1d0d3aea 100644
--- a/packages/core/src/render3/definition.ts
+++ b/packages/core/src/render3/definition.ts
@@ -9,22 +9,15 @@
import './ng_dev_mode';
import {ChangeDetectionStrategy} from '../change_detection/constants';
-import {Provider} from '../di/provider';
import {NgModuleDef} from '../metadata/ng_module';
import {ViewEncapsulation} from '../metadata/view';
import {Mutable, Type} from '../type';
import {noSideEffects, stringify} from '../util';
-
+import {EMPTY_ARRAY, EMPTY_OBJ} from './empty';
import {NG_COMPONENT_DEF, NG_DIRECTIVE_DEF, NG_MODULE_DEF, NG_PIPE_DEF} from './fields';
import {BaseDef, ComponentDef, ComponentDefFeature, ComponentQuery, ComponentTemplate, ComponentType, DirectiveDef, DirectiveDefFeature, DirectiveType, DirectiveTypesOrFactory, HostBindingsFunction, PipeDef, PipeType, PipeTypesOrFactory} from './interfaces/definition';
-import {CssSelectorList, SelectorFlags} from './interfaces/projection';
+import {CssSelectorList} from './interfaces/projection';
-export const EMPTY: {} = {};
-export const EMPTY_ARRAY: any[] = [];
-if (typeof ngDevMode !== 'undefined' && ngDevMode) {
- Object.freeze(EMPTY);
- Object.freeze(EMPTY_ARRAY);
-}
let _renderCompCount = 0;
/**
@@ -389,7 +382,7 @@ export function defineNgModule(def: {type: T} & Partial>): nev
*/
function invertObject(obj: any, secondary?: any): any {
- if (obj == null) return EMPTY;
+ if (obj == null) return EMPTY_OBJ;
const newLookup: any = {};
for (const minifiedKey in obj) {
if (obj.hasOwnProperty(minifiedKey)) {
diff --git a/packages/core/src/render3/discovery_utils.ts b/packages/core/src/render3/discovery_utils.ts
index 66fe7b29e9..08ccd32fc4 100644
--- a/packages/core/src/render3/discovery_utils.ts
+++ b/packages/core/src/render3/discovery_utils.ts
@@ -7,6 +7,7 @@
*/
import {Injector} from '../di/injector';
+
import {assertDefined} from './assert';
import {discoverLocalRefs, getComponentAtNodeIndex, getDirectivesAtNodeIndex, getLContext} from './context_discovery';
import {NodeInjector} from './di';
@@ -14,7 +15,8 @@ import {LContext} from './interfaces/context';
import {DirectiveDef} from './interfaces/definition';
import {TElementNode, TNode, TNodeProviderIndexes} from './interfaces/node';
import {CLEANUP, CONTEXT, FLAGS, HOST, LView, LViewFlags, PARENT, RootContext, TVIEW} from './interfaces/view';
-import {readPatchedLView, stringify} from './util';
+import {readElementValue, readPatchedLView, stringify} from './util';
+
/**
@@ -327,7 +329,7 @@ export function getListeners(element: Element): Listener[] {
const secondParam = tCleanup[i++];
if (typeof firstParam === 'string') {
const name: string = firstParam;
- const listenerElement: Element = lView[secondParam];
+ const listenerElement = readElementValue(lView[secondParam]) as any as Element;
const callback: (value: any) => any = lCleanup[tCleanup[i++]];
const useCaptureOrIndx = tCleanup[i++];
// if useCaptureOrIndx is boolean then report it as is.
diff --git a/packages/core/src/render3/empty.ts b/packages/core/src/render3/empty.ts
new file mode 100644
index 0000000000..46dc61bdf6
--- /dev/null
+++ b/packages/core/src/render3/empty.ts
@@ -0,0 +1,24 @@
+/**
+* @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 './ng_dev_mode';
+
+/**
+ * This file contains reuseable "empty" symbols that can be used as default return values
+ * in different parts of the rendering code. Because the same symbols are returned, this
+ * allows for identity checks against these values to be consistently used by the framework
+ * code.
+ */
+
+export const EMPTY_OBJ: {} = {};
+export const EMPTY_ARRAY: any[] = [];
+
+// freezing the values prevents any code from accidentally inserting new values in
+if (typeof ngDevMode !== 'undefined' && ngDevMode) {
+ Object.freeze(EMPTY_OBJ);
+ Object.freeze(EMPTY_ARRAY);
+}
diff --git a/packages/core/src/render3/features/inherit_definition_feature.ts b/packages/core/src/render3/features/inherit_definition_feature.ts
index 33e76644b2..5e2f7cc775 100644
--- a/packages/core/src/render3/features/inherit_definition_feature.ts
+++ b/packages/core/src/render3/features/inherit_definition_feature.ts
@@ -8,8 +8,8 @@
import {Type} from '../../type';
import {fillProperties} from '../../util/property';
-import {EMPTY, EMPTY_ARRAY} from '../definition';
-import {ComponentDef, ComponentTemplate, DirectiveDef, DirectiveDefFeature, RenderFlags} from '../interfaces/definition';
+import {EMPTY_ARRAY, EMPTY_OBJ} from '../empty';
+import {ComponentDef, DirectiveDef, DirectiveDefFeature, RenderFlags} from '../interfaces/definition';
@@ -178,7 +178,7 @@ export function InheritDefinitionFeature(definition: DirectiveDef| Componen
function maybeUnwrapEmpty(value: T[]): T[];
function maybeUnwrapEmpty(value: T): T;
function maybeUnwrapEmpty(value: any): any {
- if (value === EMPTY) {
+ if (value === EMPTY_OBJ) {
return {};
} else if (value === EMPTY_ARRAY) {
return [];
diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts
index 9128b9901d..f1971992b5 100644
--- a/packages/core/src/render3/index.ts
+++ b/packages/core/src/render3/index.ts
@@ -48,8 +48,8 @@ export {
elementContainerStart,
elementContainerEnd,
-
elementStyling,
+ elementHostAttrs,
elementStylingMap,
elementStyleProp,
elementStylingApply,
@@ -79,7 +79,7 @@ export {
directiveInject,
injectAttribute,
-
+
getCurrentView
} from './instructions';
diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts
index 4ca0b1aae1..6ed84dd888 100644
--- a/packages/core/src/render3/instructions.ts
+++ b/packages/core/src/render3/instructions.ts
@@ -15,6 +15,7 @@ import {Sanitizer} from '../sanitization/security';
import {StyleSanitizeFn} from '../sanitization/style_sanitizer';
import {Type} from '../type';
import {normalizeDebugBindingName, normalizeDebugBindingValue} from '../util/ng_reflect';
+
import {assertDataInRange, assertDefined, assertEqual, assertHasParent, assertLessThan, assertNotEqual, assertPreviousIsParent} from './assert';
import {bindingUpdated, bindingUpdated2, bindingUpdated3, bindingUpdated4} from './bindings';
import {attachPatchData, getComponentViewByInstance} from './context_discovery';
@@ -22,7 +23,7 @@ import {diPublicInInjector, getNodeInjectable, getOrCreateInjectable, getOrCreat
import {throwMultipleComponentError} from './errors';
import {executeHooks, executeInitHooks, queueInitHooks, queueLifecycleHooks} from './hooks';
import {ACTIVE_INDEX, LContainer, VIEWS} from './interfaces/container';
-import {ComponentDef, ComponentQuery, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, InitialStylingFlags, PipeDefListOrFactory, RenderFlags} from './interfaces/definition';
+import {ComponentDef, ComponentQuery, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, PipeDefListOrFactory, RenderFlags} from './interfaces/definition';
import {INJECTOR_BLOOM_PARENT_SIZE, NodeInjectorFactory} from './interfaces/injector';
import {AttributeMarker, InitialInputData, InitialInputs, LocalRefExtractor, PropertyAliasValue, PropertyAliases, TAttributes, TContainerNode, TElementContainerNode, TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TProjectionNode, TViewNode} from './interfaces/node';
import {PlayerFactory} from './interfaces/player';
@@ -30,19 +31,19 @@ import {CssSelectorList, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection'
import {LQueries} from './interfaces/query';
import {ProceduralRenderer3, RComment, RElement, RText, Renderer3, RendererFactory3, isProceduralRenderer} from './interfaces/renderer';
import {SanitizerFn} from './interfaces/sanitization';
-import {StylingIndex} from './interfaces/styling';
import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTENT_QUERIES, CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, INJECTOR, LView, LViewFlags, NEXT, OpaqueViewState, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, TAIL, TVIEW, TView} from './interfaces/view';
import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert';
import {appendChild, appendProjectedNode, createTextNode, getLViewChild, getRenderParent, insertView, removeView} from './node_manipulation';
import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher';
import {decreaseElementDepthCount, enterView, getBindingsEnabled, getCheckNoChangesMode, getContextLView, getCurrentDirectiveDef, getElementDepthCount, getFirstTemplatePass, getIsParent, getLView, getPreviousOrParentTNode, increaseElementDepthCount, isCreationMode, leaveView, nextContextImpl, resetComponentState, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setFirstTemplatePass, setIsParent, setPreviousOrParentTNode} from './state';
-import {createStylingContextTemplate, renderStyleAndClassBindings, setStyle, updateClassProp as updateElementClassProp, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling/class_and_style_bindings';
+import {getInitialClassNameValue, initializeStaticContext as initializeStaticStylingContext, patchContextWithStaticAttrs, renderInitialStylesAndClasses, renderStyling, updateClassProp as updateElementClassProp, updateContextWithBindings, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling/class_and_style_bindings';
import {BoundPlayerFactory} from './styling/player_factory';
-import {getStylingContext, isAnimationProp} from './styling/util';
+import {createEmptyStylingContext, getStylingContext, hasClassInput, hasStyling, isAnimationProp} from './styling/util';
import {NO_CHANGE} from './tokens';
import {findComponentView, getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getRootContext, getRootView, getTNode, isComponent, isComponentDef, loadInternal, readElementValue, readPatchedLView, stringify} from './util';
+
/**
* A permanent marker promise which signifies that the current CD tree is
* clean.
@@ -456,7 +457,8 @@ export function namespaceHTML() {
*
* @param index Index of the element in the data array
* @param name Name of the DOM Node
- * @param attrs Statically bound set of attributes to be written into the DOM element on creation.
+ * @param attrs Statically bound set of attributes, classes, and styles to be written into the DOM
+ * element on creation. Use [AttributeMarker] to denote the meaning of this array.
* @param localRefs A set of local reference bindings on the element.
*/
export function element(
@@ -526,7 +528,8 @@ export function elementContainerEnd(): void {
*
* @param index Index of the element in the LView array
* @param name Name of the DOM Node
- * @param attrs Statically bound set of attributes to be written into the DOM element on creation.
+ * @param attrs Statically bound set of attributes, classes, and styles to be written into the DOM
+ * element on creation. Use [AttributeMarker] to denote the meaning of this array.
* @param localRefs A set of local reference bindings on the element.
*
* Attributes and localRefs are passed as an array of strings where elements with an even index
@@ -550,6 +553,14 @@ export function elementStart(
const tNode = createNodeAtIndex(index, TNodeType.Element, native !, name, attrs || null);
if (attrs) {
+ // it's important to only prepare styling-related datastructures once for a given
+ // tNode and not each time an element is created. Also, the styling code is designed
+ // to be patched and constructed at various points, but only up until the first element
+ // is created. Then the styling context is locked and can only be instantiated for each
+ // successive element that is created.
+ if (tView.firstTemplatePass && !tNode.stylingTemplate && hasStyling(attrs)) {
+ tNode.stylingTemplate = initializeStaticStylingContext(attrs);
+ }
setUpAttributes(native, attrs);
}
@@ -563,6 +574,23 @@ export function elementStart(
attachPatchData(native, lView);
}
increaseElementDepthCount();
+
+ // if a directive contains a host binding for "class" then all class-based data will
+ // flow through that (except for `[class.prop]` bindings). This also includes initial
+ // static class values as well. (Note that this will be fixed once map-based `[style]`
+ // and `[class]` bindings work for multiple directives.)
+ if (tView.firstTemplatePass) {
+ const inputData = initializeTNodeInputs(tNode);
+ if (inputData && inputData.hasOwnProperty('class')) {
+ tNode.flags |= TNodeFlags.hasClassInput;
+ }
+ }
+
+ // There is no point in rendering styles when a class directive is present since
+ // it will take that over for us (this will be removed once #FW-882 is in).
+ if (tNode.stylingTemplate && (tNode.flags & TNodeFlags.hasClassInput) === 0) {
+ renderInitialStylesAndClasses(native, tNode.stylingTemplate, lView[RENDERER]);
+ }
}
/**
@@ -721,25 +749,28 @@ function setUpAttributes(native: RElement, attrs: TAttributes): void {
let i = 0;
while (i < attrs.length) {
- const attrName = attrs[i];
- if (attrName === AttributeMarker.SelectOnly) break;
- if (attrName === NG_PROJECT_AS_ATTR_NAME) {
- i += 2;
- } else {
- ngDevMode && ngDevMode.rendererSetAttribute++;
+ const attrName = attrs[i++];
+ if (typeof attrName == 'number') {
if (attrName === AttributeMarker.NamespaceURI) {
// Namespaced attributes
- const namespaceURI = attrs[i + 1] as string;
- const attrName = attrs[i + 2] as string;
- const attrVal = attrs[i + 3] as string;
+ const namespaceURI = attrs[i++] as string;
+ const attrName = attrs[i++] as string;
+ const attrVal = attrs[i++] as string;
+ ngDevMode && ngDevMode.rendererSetAttribute++;
isProc ?
(renderer as ProceduralRenderer3)
.setAttribute(native, attrName, attrVal, namespaceURI) :
native.setAttributeNS(namespaceURI, attrName, attrVal);
- i += 4;
} else {
+ // All other `AttributeMarker`s are ignored here.
+ break;
+ }
+ } else {
+ /// attrName is string;
+ const attrVal = attrs[i++];
+ if (attrName !== NG_PROJECT_AS_ATTR_NAME) {
// Standard attributes
- const attrVal = attrs[i + 1];
+ ngDevMode && ngDevMode.rendererSetAttribute++;
if (isAnimationProp(attrName)) {
if (isProc) {
(renderer as ProceduralRenderer3).setProperty(native, attrName, attrVal);
@@ -750,7 +781,6 @@ function setUpAttributes(native: RElement, attrs: TAttributes): void {
.setAttribute(native, attrName as string, attrVal as string) :
native.setAttribute(attrName as string, attrVal as string);
}
- i += 2;
}
}
}
@@ -902,6 +932,15 @@ export function elementEnd(): void {
queueLifecycleHooks(getLView()[TVIEW], previousOrParentTNode);
decreaseElementDepthCount();
+
+ // this is fired at the end of elementEnd because ALL of the stylingBindings code
+ // (for directives and the template) have now executed which means the styling
+ // context can be instantiated properly.
+ if (hasClassInput(previousOrParentTNode)) {
+ const stylingContext = getStylingContext(previousOrParentTNode.index, lView);
+ setInputsForProperty(
+ lView, previousOrParentTNode.inputs !['class'] !, getInitialClassNameValue(stylingContext));
+ }
}
/**
@@ -1096,119 +1135,84 @@ function generatePropertyAliases(tNode: TNode, direction: BindingDirection): Pro
return propStore;
}
-/**
- * Add or remove a class in a `classList` on a DOM element.
- *
- * This instruction is meant to handle the [class.foo]="exp" case
- *
- * @param index The index of the element to update in the data array
- * @param classIndex Index of class to toggle. Because it is going to DOM, this is not subject to
- * renaming as part of minification.
- * @param value A value indicating if a given class should be added or removed.
- * @param directive the ref to the directive that is attempting to change styling.
- */
-export function elementClassProp(
- index: number, classIndex: number, value: boolean | PlayerFactory, directive?: {}): void {
- if (directive != undefined) {
- return hackImplementationOfElementClassProp(
- index, classIndex, value, directive); // proper supported in next PR
- }
- const val =
- (value instanceof BoundPlayerFactory) ? (value as BoundPlayerFactory) : (!!value);
- updateElementClassProp(getStylingContext(index + HEADER_OFFSET, getLView()), classIndex, val);
-}
-
/**
* Assign any inline style values to the element during creation mode.
*
- * This instruction is meant to be called during creation mode to apply all styling
- * (e.g. `style="..."`) values to the element. This is also where the provided index
- * value is allocated for the styling details for its corresponding element (the element
- * index is the previous index value from this one).
+ * This instruction is meant to be called during creation mode to register all
+ * dynamic style and class bindings on the element. Note for static values (no binding)
+ * see `elementStart` and `elementHostAttrs`.
*
- * (Note this function calls `elementStylingApply` immediately when called.)
+ * @param classBindingNames An array containing bindable class names.
+ * The `elementClassProp` refers to the class name by index in this array.
+ * (i.e. `['foo', 'bar']` means `foo=0` and `bar=1`).
+ * @param styleBindingNames An array containing bindable style properties.
+ * The `elementStyleProp` refers to the class name by index in this array.
+ * (i.e. `['width', 'height']` means `width=0` and `height=1`).
+ * @param styleSanitizer An optional sanitizer function that will be used to sanitize any CSS
+ * property values that are applied to the element (during rendering).
+ * Note that the sanitizer instance itself is tied to the `directive` (if provided).
+ * @param directive A directive instance the styling is associated with. If not provided
+ * current view's controller instance is assumed.
*
- *
- * @param index Index value which will be allocated to store styling data for the element.
- * (Note that this is not the element index, but rather an index value allocated
- * specifically for element styling--the index must be the next index after the element
- * index.)
- * @param classDeclarations A key/value array of CSS classes that will be registered on the element.
- * Each individual style will be used on the element as long as it is not overridden
- * by any classes placed on the element by multiple (`[class]`) or singular (`[class.named]`)
- * bindings. If a class binding changes its value to a falsy value then the matching initial
- * class value that are passed in here will be applied to the element (if matched).
- * @param styleDeclarations A key/value array of CSS styles that will be registered on the element.
- * Each individual style will be used on the element as long as it is not overridden
- * by any styles placed on the element by multiple (`[style]`) or singular (`[style.prop]`)
- * bindings. If a style binding changes its value to null then the initial styling
- * values that are passed in here will be applied to the element (if matched).
- * @param styleSanitizer An optional sanitizer function that will be used (if provided)
- * to sanitize the any CSS property values that are applied to the element (during rendering).
- * @param directive the ref to the directive that is attempting to change styling.
+ * @publicApi
*/
export function elementStyling(
- classDeclarations?: (string | boolean | InitialStylingFlags)[] | null,
- styleDeclarations?: (string | boolean | InitialStylingFlags)[] | null,
+ classBindingNames?: string[] | null, styleBindingNames?: string[] | null,
styleSanitizer?: StyleSanitizeFn | null, directive?: {}): void {
- if (directive != undefined) {
- isCreationMode() &&
- hackImplementationOfElementStyling(
- classDeclarations || null, styleDeclarations || null, styleSanitizer || null,
- directive); // supported in next PR
- return;
- }
const tNode = getPreviousOrParentTNode();
- const inputData = initializeTNodeInputs(tNode);
-
if (!tNode.stylingTemplate) {
- const hasClassInput = inputData && inputData.hasOwnProperty('class') ? true : false;
- if (hasClassInput) {
- tNode.flags |= TNodeFlags.hasClassInput;
- }
-
- // initialize the styling template.
- tNode.stylingTemplate = createStylingContextTemplate(
- classDeclarations, styleDeclarations, styleSanitizer, hasClassInput);
- }
-
- if (styleDeclarations && styleDeclarations.length ||
- classDeclarations && classDeclarations.length) {
- const index = tNode.index;
- if (delegateToClassInput(tNode)) {
- const lView = getLView();
- const stylingContext = getStylingContext(index, lView);
- const initialClasses = stylingContext[StylingIndex.PreviousOrCachedMultiClassValue] as string;
- setInputsForProperty(lView, tNode.inputs !['class'] !, initialClasses);
- }
- elementStylingApply(index - HEADER_OFFSET);
+ tNode.stylingTemplate = createEmptyStylingContext();
}
+ updateContextWithBindings(
+ tNode.stylingTemplate !, directive || null, classBindingNames, styleBindingNames,
+ styleSanitizer, hasClassInput(tNode));
}
+/**
+ * Assign static styling values to a host element.
+ *
+ * NOTE: This instruction is meant to used from `hostBindings` function only.
+ *
+ * @param directive A directive instance the styling is associated with.
+ * @param attrs An array containing class and styling information. The values must be marked with
+ * `AttributeMarker`.
+ *
+ * ```
+ * var attrs = [AttributeMarker.Classes, 'foo', 'bar',
+ * AttributeMarker.Styles, 'width', '100px', 'height, '200px']
+ * elementHostAttrs(directive, attrs);
+ * ```
+ *
+ * @publicApi
+ */
+export function elementHostAttrs(directive: any, attrs: TAttributes) {
+ const tNode = getPreviousOrParentTNode();
+ if (!tNode.stylingTemplate) {
+ tNode.stylingTemplate = initializeStaticStylingContext(attrs);
+ }
+ patchContextWithStaticAttrs(tNode.stylingTemplate, attrs, directive);
+}
/**
- * Apply all styling values to the element which have been queued by any styling instructions.
+ * Apply styling binding to the element.
*
- * This instruction is meant to be run once one or more `elementStyle` and/or `elementStyleProp`
- * have been issued against the element. This function will also determine if any styles have
- * changed and will then skip the operation if there is nothing new to render.
+ * This instruction is meant to be run after `elementStyle` and/or `elementStyleProp`.
+ * if any styling bindings have changed then the changes are flushed to the element.
*
- * Once called then all queued styles will be flushed.
*
- * @param index Index of the element's styling storage that will be rendered.
- * (Note that this is not the element index, but rather an index value allocated
- * specifically for element styling--the index must be the next index after the element
- * index.)
- * @param directive the ref to the directive that is attempting to change styling.
+ * @param index Index of the element's with which styling is associated.
+ * @param directive Directive instance that is attempting to change styling. (Defaults to the
+ * component of the current view).
+components
+ *
+ * @publicApi
*/
-export function elementStylingApply(index: number, directive?: {}): void {
- if (directive != undefined) {
- return hackImplementationOfElementStylingApply(index, directive); // supported in next PR
- }
+export function elementStylingApply(index: number, directive?: any): void {
const lView = getLView();
const isFirstRender = (lView[FLAGS] & LViewFlags.FirstLViewPass) !== 0;
- const totalPlayersQueued = renderStyleAndClassBindings(
- getStylingContext(index + HEADER_OFFSET, lView), lView[RENDERER], lView, isFirstRender);
+ const totalPlayersQueued = renderStyling(
+ getStylingContext(index + HEADER_OFFSET, lView), lView[RENDERER], lView, isFirstRender, null,
+ null, directive);
if (totalPlayersQueued > 0) {
const rootContext = getRootContext(lView);
scheduleTick(rootContext, RootContextFlags.FlushPlayers);
@@ -1216,29 +1220,34 @@ export function elementStylingApply(index: number, directive?: {}): void {
}
/**
- * Queue a given style to be rendered on an Element.
+ * Update a style bindings value on an element.
*
* If the style value is `null` then it will be removed from the element
* (or assigned a different value depending if there are any styles placed
* on the element with `elementStyle` or any styles that are present
* from when the element was created (with `elementStyling`).
*
- * (Note that the styling instruction will not be applied until `elementStylingApply` is called.)
+ * (Note that the styling element is updated as part of `elementStylingApply`.)
*
- * @param index Index of the element's styling storage to change in the data array.
- * (Note that this is not the element index, but rather an index value allocated
- * specifically for element styling--the index must be the next index after the element
- * index.)
- * @param styleIndex Index of the style property on this element. (Monotonically increasing.)
- * @param value New value to write (null to remove).
+ * @param index Index of the element's with which styling is associated.
+ * @param styleIndex Index of style to update. This index value refers to the
+ * index of the style in the style bindings array that was passed into
+ * `elementStlyingBindings`.
+ * @param value New value to write (null to remove). Note that if a directive also
+ * attempts to write to the same binding value then it will only be able to
+ * do so if the template binding value is `null` (or doesn't exist at all).
* @param suffix Optional suffix. Used with scalar values to add unit such as `px`.
* Note that when a suffix is provided then the underlying sanitizer will
* be ignored.
- * @param directive the ref to the directive that is attempting to change styling.
+ * @param directive Directive instance that is attempting to change styling. (Defaults to the
+ * component of the current view).
+components
+ *
+ * @publicApi
*/
export function elementStyleProp(
index: number, styleIndex: number, value: string | number | String | PlayerFactory | null,
- suffix?: string, directive?: {}): void {
+ suffix?: string | null, directive?: {}): void {
let valueToAdd: string|null = null;
if (value !== null) {
if (suffix) {
@@ -1253,35 +1262,59 @@ export function elementStyleProp(
valueToAdd = value as any as string;
}
}
- if (directive != undefined) {
- hackImplementationOfElementStyleProp(index, styleIndex, valueToAdd, suffix, directive);
- } else {
- updateElementStyleProp(
- getStylingContext(index + HEADER_OFFSET, getLView()), styleIndex, valueToAdd);
- }
+ updateElementStyleProp(
+ getStylingContext(index + HEADER_OFFSET, getLView()), styleIndex, valueToAdd, directive);
}
/**
- * Queue a key/value map of styles to be rendered on an Element.
+ * Add or remove a class via a class binding on a DOM element.
*
- * This instruction is meant to handle the `[style]="exp"` usage. When styles are applied to
- * the Element they will then be placed with respect to any styles set with `elementStyleProp`.
- * If any styles are set to `null` then they will be removed from the element (unless the same
- * style properties have been assigned to the element during creation using `elementStyling`).
+ * This instruction is meant to handle the [class.foo]="exp" case and, therefore,
+ * the class itself must already be applied using `elementStyling` within
+ * the creation block.
+ *
+ * @param index Index of the element's with which styling is associated.
+ * @param classIndex Index of class to toggle. This index value refers to the
+ * index of the class in the class bindings array that was passed into
+ * `elementStlyingBindings` (which is meant to be called before this
+ * function is).
+ * @param value A true/false value which will turn the class on or off.
+ * @param directive Directive instance that is attempting to change styling. (Defaults to the
+ * component of the current view).
+components
+ *
+ * @publicApi
+ */
+export function elementClassProp(
+ index: number, classIndex: number, value: boolean | PlayerFactory, directive?: {}): void {
+ const onOrOffClassValue =
+ (value instanceof BoundPlayerFactory) ? (value as BoundPlayerFactory) : (!!value);
+ updateElementClassProp(
+ getStylingContext(index + HEADER_OFFSET, getLView()), classIndex, onOrOffClassValue,
+ directive);
+}
+
+/**
+ * Update style and/or class bindings using object literal.
+ *
+ * This instruction is meant apply styling via the `[style]="exp"` and `[class]="exp"` template
+ * bindings. When styles are applied to the Element they will then be placed with respect to
+ * any styles set with `elementStyleProp`. If any styles are set to `null` then they will be
+ * removed from the element.
*
* (Note that the styling instruction will not be applied until `elementStylingApply` is called.)
*
- * @param index Index of the element's styling storage to change in the data array.
- * (Note that this is not the element index, but rather an index value allocated
- * specifically for element styling--the index must be the next index after the element
- * index.)
+ * @param index Index of the element's with which styling is associated.
* @param classes A key/value style map of CSS classes that will be added to the given element.
* Any missing classes (that have already been applied to the element beforehand) will be
* removed (unset) from the element's list of CSS classes.
* @param styles A key/value style map of the styles that will be applied to the given element.
* Any missing styles (that have already been applied to the element beforehand) will be
* removed (unset) from the element's styling.
- * @param directive the ref to the directive that is attempting to change styling.
+ * @param directive Directive instance that is attempting to change styling. (Defaults to the
+ * component of the current view).
+ *
+ * @publicApi
*/
export function elementStylingMap(
index: number, classes: {[key: string]: any} | string | NO_CHANGE | null,
@@ -1292,113 +1325,24 @@ export function elementStylingMap(
const lView = getLView();
const tNode = getTNode(index, lView);
const stylingContext = getStylingContext(index + HEADER_OFFSET, lView);
- if (delegateToClassInput(tNode) && classes !== NO_CHANGE) {
- const initialClasses = stylingContext[StylingIndex.PreviousOrCachedMultiClassValue] as string;
+ if (hasClassInput(tNode) && classes !== NO_CHANGE) {
+ const initialClasses = getInitialClassNameValue(stylingContext);
const classInputVal =
(initialClasses.length ? (initialClasses + ' ') : '') + (classes as string);
setInputsForProperty(lView, tNode.inputs !['class'] !, classInputVal);
+ } else {
+ updateStylingMap(stylingContext, classes, styles);
}
- updateStylingMap(stylingContext, classes, styles);
}
/* START OF HACK BLOCK */
-/*
- * HACK
- * The code below is a quick and dirty implementation of the host style binding so that we can make
- * progress on TestBed. Once the correct implementation is created this code should be removed.
- */
-interface HostStylingHack {
- classDeclarations: string[];
- styleDeclarations: string[];
- styleSanitizer: StyleSanitizeFn|null;
-}
-type HostStylingHackMap = Map<{}, HostStylingHack>;
-
-function hackImplementationOfElementStyling(
- classDeclarations: (string | boolean | InitialStylingFlags)[] | null,
- styleDeclarations: (string | boolean | InitialStylingFlags)[] | null,
- styleSanitizer: StyleSanitizeFn | null, directive: {}): void {
- const node = getNativeByTNode(getPreviousOrParentTNode(), getLView()) as RElement;
- ngDevMode && assertDefined(node, 'expecting parent DOM node');
- const hostStylingHackMap: HostStylingHackMap =
- ((node as any).hostStylingHack || ((node as any).hostStylingHack = new Map()));
- const squashedClassDeclarations = hackSquashDeclaration(classDeclarations);
- hostStylingHackMap.set(directive, {
- classDeclarations: squashedClassDeclarations,
- styleDeclarations: hackSquashDeclaration(styleDeclarations), styleSanitizer
- });
- hackSetStaticClasses(node, squashedClassDeclarations);
-}
-
-function hackSetStaticClasses(node: RElement, classDeclarations: (string | boolean)[]) {
- // Static classes need to be set here because static classes don't generate
- // elementClassProp instructions.
- const lView = getLView();
- const staticClassStartIndex =
- classDeclarations.indexOf(InitialStylingFlags.VALUES_MODE as any) + 1;
- const renderer = lView[RENDERER];
-
- for (let i = staticClassStartIndex; i < classDeclarations.length; i += 2) {
- const className = classDeclarations[i] as string;
- const value = classDeclarations[i + 1];
- // if value is true, then this is a static class and we should set it now.
- // class bindings are set separately in elementClassProp.
- if (value === true) {
- if (isProceduralRenderer(renderer)) {
- renderer.addClass(node, className);
- } else {
- const classList = (node as HTMLElement).classList;
- classList.add(className);
- }
- }
- }
-}
-
-function hackSquashDeclaration(declarations: (string | boolean | InitialStylingFlags)[] | null):
- string[] {
- // assume the array is correct. This should be fine for View Engine compatibility.
- return declarations || [] as any;
-}
-
-function hackImplementationOfElementClassProp(
- index: number, classIndex: number, value: boolean | PlayerFactory, directive: {}): void {
- const lView = getLView();
- const node = getNativeByIndex(index, lView);
- ngDevMode && assertDefined(node, 'could not locate node');
- const hostStylingHack: HostStylingHack = (node as any).hostStylingHack.get(directive);
- const className = hostStylingHack.classDeclarations[classIndex];
- const renderer = lView[RENDERER];
- if (isProceduralRenderer(renderer)) {
- value ? renderer.addClass(node, className) : renderer.removeClass(node, className);
- } else {
- const classList = (node as HTMLElement).classList;
- value ? classList.add(className) : classList.remove(className);
- }
-}
-
-function hackImplementationOfElementStylingApply(index: number, directive?: {}): void {
- // Do nothing because the hack implementation is eager.
-}
-
-function hackImplementationOfElementStyleProp(
- index: number, styleIndex: number, value: string | null, suffix?: string,
- directive?: {}): void {
- const lView = getLView();
- const node = getNativeByIndex(index, lView);
- ngDevMode && assertDefined(node, 'could not locate node');
- const hostStylingHack: HostStylingHack = (node as any).hostStylingHack.get(directive);
- const styleName = hostStylingHack.styleDeclarations[styleIndex];
- const renderer = lView[RENDERER];
- setStyle(node, styleName, value as string, renderer, null);
-}
-
function hackImplementationOfElementStylingMap(
index: number, classes: {[key: string]: any} | string | NO_CHANGE | null,
styles?: {[styleName: string]: any} | NO_CHANGE | null, directive?: {}): void {
throw new Error('unimplemented. Should not be needed by ViewEngine compatibility');
}
-
/* END OF HACK BLOCK */
+
//////////////////////////
//// Text
//////////////////////////
@@ -2885,10 +2829,6 @@ function initializeTNodeInputs(tNode: TNode | null) {
return null;
}
-export function delegateToClassInput(tNode: TNode) {
- return tNode.flags & TNodeFlags.hasClassInput;
-}
-
/**
* Returns the current OpaqueViewState instance.
diff --git a/packages/core/src/render3/interfaces/container.ts b/packages/core/src/render3/interfaces/container.ts
index 5a0e0e50be..a8378c2c9c 100644
--- a/packages/core/src/render3/interfaces/container.ts
+++ b/packages/core/src/render3/interfaces/container.ts
@@ -23,6 +23,14 @@ export const VIEWS = 1;
// As we already have these constants in LView, we don't need to re-create them.
export const NATIVE = 6;
export const RENDER_PARENT = 7;
+// Because interfaces in TS/JS cannot be instanceof-checked this means that we
+// need to rely on predictable characteristics of data-structures to check if they
+// are what we expect for them to be. The `LContainer` interface code below has a
+// fixed length and the constant value below references that. Using the length value
+// below we can predictably gaurantee that we are dealing with an `LContainer` array.
+// This value MUST be kept up to date with the length of the `LContainer` array
+// interface below so that runtime type checking can work.
+export const LCONTAINER_LENGTH = 8;
/**
* The state associated with a container.
diff --git a/packages/core/src/render3/interfaces/definition.ts b/packages/core/src/render3/interfaces/definition.ts
index 356ad5d5bb..f530b5cea2 100644
--- a/packages/core/src/render3/interfaces/definition.ts
+++ b/packages/core/src/render3/interfaces/definition.ts
@@ -344,8 +344,4 @@ export type PipeTypeList =
// Note: This hack is necessary so we don't erroneously get a circular dependency
// failure based on types.
-export const unusedValueExportToPlacateAjd = 1;
-
-export const enum InitialStylingFlags {
- VALUES_MODE = 0b1,
-}
+export const unusedValueExportToPlacateAjd = 1;
\ No newline at end of file
diff --git a/packages/core/src/render3/interfaces/node.ts b/packages/core/src/render3/interfaces/node.ts
index ae8b49682f..dcf5468846 100644
--- a/packages/core/src/render3/interfaces/node.ts
+++ b/packages/core/src/render3/interfaces/node.ts
@@ -54,7 +54,7 @@ export const enum TNodeProviderIndexes {
CptViewProvidersCountShifter = 0b00000000000000010000000000000000,
}
/**
- * A set of marker values to be used in the attributes arrays. Those markers indicate that some
+ * A set of marker values to be used in the attributes arrays. These markers indicate that some
* items are not regular attributes and the processing should be adapted accordingly.
*/
export const enum AttributeMarker {
@@ -65,13 +65,50 @@ export const enum AttributeMarker {
*/
NamespaceURI = 0,
+ /**
+ * Signals class declaration.
+ *
+ * Each value following `Classes` designates a class name to include on the element.
+ * ## Example:
+ *
+ * Given:
+ * ```
+ *
...
+ * ```
+ *
+ * the generated code is:
+ * ```
+ * var _c1 = [AttributeMarker.Classes, 'foo', 'bar', 'baz'];
+ * ```
+ */
+ Classes = 1,
+
+ /**
+ * Signals style declaration.
+ *
+ * Each pair of values following `Styles` designates a style name and value to include on the
+ * element.
+ * ## Example:
+ *
+ * Given:
+ * ```
+ *
...
+ * ```
+ *
+ * the generated code is:
+ * ```
+ * var _c1 = [AttributeMarker.Styles, 'width', '100px', 'height'. '200px', 'color', 'red'];
+ * ```
+ */
+ Styles = 2,
+
/**
* This marker indicates that the following attribute names were extracted from bindings (ex.:
* [foo]="exp") and / or event handlers (ex. (bar)="doSth()").
* Taking the above bindings and outputs as an example an attributes array could look as follows:
* ['class', 'fade in', AttributeMarker.SelectOnly, 'foo', 'bar']
*/
- SelectOnly = 1
+ SelectOnly = 3,
}
/**
diff --git a/packages/core/src/render3/interfaces/styling.ts b/packages/core/src/render3/interfaces/styling.ts
index 7f1c6349a8..fa2091b342 100644
--- a/packages/core/src/render3/interfaces/styling.ts
+++ b/packages/core/src/render3/interfaces/styling.ts
@@ -9,130 +9,199 @@ import {StyleSanitizeFn} from '../../sanitization/style_sanitizer';
import {RElement} from '../interfaces/renderer';
import {PlayerContext} from './player';
-
/**
* The styling context acts as a styling manifest (shaped as an array) for determining which
* styling properties have been assigned via the provided `updateStylingMap`, `updateStyleProp`
- * and `updateClassProp` functions. There are also two initialization functions
- * `allocStylingContext` and `createStylingContextTemplate` which are used to initialize
- * and/or clone the context.
+ * and `updateClassProp` functions. It also stores the static style/class values that were
+ * extracted from the template by the compiler.
*
- * The context is an array where the first two cells are used for static data (initial styling)
- * and dirty flags / index offsets). The remaining set of cells is used for multi (map) and single
- * (prop) style values.
+ * A context is created by Angular when:
+ * 1. An element contains static styling values (like style="..." or class="...")
+ * 2. An element contains single property binding values (like [style.prop]="x" or
+ * [class.prop]="y")
+ * 3. An element contains multi property binding values (like [style]="x" or [class]="y")
+ * 4. A directive contains host bindings for static, single or multi styling properties/bindings.
+ * 5. An animation player is added to an element via `addPlayer`
*
- * each value from here onwards is mapped as so:
- * [i] = mutation/type flag for the style/class value
- * [i + 1] = prop string (or null incase it has been removed)
- * [i + 2] = value string (or null incase it has been removed)
- *
- * There are three types of styling types stored in this context:
- * initial: any styles that are passed in once the context is created
- * (these are stored in the first cell of the array and the first
- * value of this array is always `null` even if no initial styling exists.
- * the `null` value is there so that any new styles have a parent to point
- * to. This way we can always assume that there is a parent.)
- *
- * single: any styles that are updated using `updateStyleProp` or `updateClassProp` (fixed set)
- *
- * multi: any styles that are updated using `updateStylingMap` (dynamic set)
- *
- * Note that context is only used to collect style information. Only when `renderStyling`
- * is called is when the styling payload will be rendered (or built as a key/value map).
- *
- * When the context is created, depending on what initial styling values are passed in, the
- * context itself will be pre-filled with slots based on the initial style properties. Say
- * for example we have a series of initial styles that look like so:
- *
- * style="width:100px; height:200px;"
- * class="foo"
- *
- * Then the initial state of the context (once initialized) will look like so:
+ * Note that even if an element contains static styling then this context will be created and
+ * attached to it. The reason why this happens (instead of treating styles/classes as regular
+ * HTML attributes) is because the style/class bindings must be able to default themselves back
+ * to their respective static values when they are set to null.
*
+ * Say for example we have this:
* ```
+ *
+ *
+ * ```
+ *
+ * Even in the situation where there are no bindings, the static styling is still placed into the
+ * context because there may be another directive on the same element that has styling.
+ *
+ * When Angular initializes styling data for an element then it will first register the static
+ * styling values on the element using one of these two instructions:
+ *
+ * 1. elementStart or element (within the template function of a component)
+ * 2. elementHostAttrs (for directive host bindings)
+ *
+ * In either case, a styling context will be created and stored within an element's LViewData. Once
+ * the styling context is created then single and multi properties can stored within it. For this to
+ * happen, the following function needs to be called:
+ *
+ * `elementStyling` (called with style properties, class properties and a sanitizer + a directive
+ * instance).
+ *
+ * When this instruction is called it will populate the styling context with the provided style
+ * and class names into the context.
+ *
+ * The context itself looks like this:
+ *
* context = [
- * element,
- * playerContext | null,
- * styleSanitizer | null,
- * [null, '100px', '200px', true], // property names are not needed since they have already been
- * written to DOM.
- *
- * configMasterVal,
- * 1, // this instructs how many `style` values there are so that class index values can be
- * offsetted
- * { classOne: true, classTwo: false } | 'classOne classTwo' | null // last class value provided
- * into updateStylingMap
- * { styleOne: '100px', styleTwo: 0 } | null // last style value provided into updateStylingMap
- *
- * // 8
- * 'width',
- * pointers(1, 15); // Point to static `width`: `100px` and multi `width`.
- * null,
- *
- * // 11
- * 'height',
- * pointers(2, 18); // Point to static `height`: `200px` and multi `height`.
- * null,
- *
- * // 14
- * 'foo',
- * pointers(1, 21); // Point to static `foo`: `true` and multi `foo`.
- * null,
- *
- * // 17
- * 'width',
- * pointers(1, 6); // Point to static `width`: `100px` and single `width`.
- * null,
- *
- * // 21
- * 'height',
- * pointers(2, 9); // Point to static `height`: `200px` and single `height`.
- * null,
- *
- * // 24
- * 'foo',
- * pointers(3, 12); // Point to static `foo`: `true` and single `foo`.
- * null,
+ * // 0-8: header values (about 8 entries of configuration data)
+ * // 9+: this is where each entry is stored:
* ]
*
- * function pointers(staticIndex: number, dynamicIndex: number) {
- * // combine the two indices into a single word.
- * return (staticIndex << StylingFlags.BitCountSize) |
- * (dynamicIndex << (StylingIndex.BitCountSize + StylingFlags.BitCountSize));
+ * Let's say we have the following template code:
+ *
+ * ```
+ *
+ * ```
+ *
+ * The context generated from these values will look like this (note that
+ * for each binding name (the class and style bindings) the values will
+ * be inserted twice into the array (once for single property entries) and
+ * another for multi property entries).
+ *
+ * context = [
+ * // 0-8: header values (about 8 entries of configuration data)
+ * // 9+: this is where each entry is stored:
+ *
+ * // SINGLE PROPERTIES
+ * configForWidth,
+ * 'width'
+ * myWidthExp, // the binding value not the binding itself
+ * 0, // the directive owner
+ *
+ * configForHeight,
+ * 'height'
+ * myHeightExp, // the binding value not the binding itself
+ * 0, // the directive owner
+ *
+ * configForBazClass,
+ * 'baz
+ * myBazClassExp, // the binding value not the binding itself
+ * 0, // the directive owner
+ *
+ * // MULTI PROPERTIES
+ * configForWidth,
+ * 'width'
+ * myWidthExp, // the binding value not the binding itself
+ * 0, // the directive owner
+ *
+ * configForHeight,
+ * 'height'
+ * myHeightExp, // the binding value not the binding itself
+ * 0, // the directive owner
+ *
+ * configForBazClass,
+ * 'baz
+ * myBazClassExp, // the binding value not the binding itself
+ * 0, // the directive owner
+ * ]
+ *
+ * The configuration values are left out of the example above because
+ * the ordering of them could change between code patches. Please read the
+ * documentation below to get a better understand of what the configuration
+ * values are and how they work.
+ *
+ * Each time a binding property is updated (whether it be through a single
+ * property instruction like `elementStyleProp`, `elementClassProp` or
+ * `elementStylingMap`) then the values in the context will be updated as
+ * well.
+ *
+ * If for example `[style.width]` updates to `555px` then its value will be reflected
+ * in the context as so:
+ *
+ * context = [
+ * // ...
+ * configForWidth, // this will be marked DIRTY
+ * 'width'
+ * '555px',
+ * 0,
+ * //..
+ * ]
+ *
+ * The context and directive data will also be marked dirty.
+ *
+ * Despite the context being updated, nothing has been rendered on screen (not styles or
+ * classes have been set on the element). To kick off rendering for an element the following
+ * function needs to be run `elementStylingApply`.
+ *
+ * `elementStylingApply` will run through the context and find each dirty value and render them onto
+ * the element. Once complete, all styles/classes will be set to clean. Because of this, the render
+ * function will now know not to rerun itself again if called again unless new style/class values
+ * have changed.
+ *
+ * ## Directives
+ * Directives style values (which are provided through host bindings) are also supported and
+ * housed within the same styling context as are template-level style/class properties/bindings.
+ * Both directive-level and template-level styling bindings share the same context.
+ *
+ * Each of the following instructions supports accepting a directive instance as an input parameter:
+ *
+ * - `elementHostAttrs`
+ * - `elementStyling`
+ * - `elementStyleProp`
+ * - `elementClassProp`
+ * - `elementStylingMap`
+ * - `elementStylingApply`
+ *
+ * Each time a directiveRef is passed in, it will be converted into an index by examining the
+ * directive registry (which lives in the context configuration area). The index is then used
+ * to help single style properties figure out where a value is located in the context.
+ *
+ * If two directives or a directive + a template binding both write to the same style/class
+ * binding then the styling context code will decide which one wins based on the following
+ * rule:
+ *
+ * 1. If the template binding has a value then it always wins
+ * 2. If not then whichever first-registered directive that has that value first will win
+ *
+ * The code example helps make this clear:
+ *
+ * ```
+ *
+ * @Directive({ selector: '[my-width-directive' ]})
+ * class MyWidthDirective {
+ * @Input('my-width-directive')
+ * @HostBinding('style.width')
+ * public width = null;
* }
* ```
*
- * The values are duplicated so that space is set aside for both multi ([style] and [class])
- * and single ([style.prop] or [class.named]) values. The respective config values
- * (configValA, configValB, etc...) are a combination of the StylingFlags with two index
- * values: the `initialIndex` (which points to the index location of the style value in
- * the initial styles array in slot 0) and the `dynamicIndex` (which points to the
- * matching single/multi index position in the context array for the same prop).
+ * Since there is a style binding for width present on the element (`[style.width]`) then
+ * it will always win over the width binding that is present as a host binding within
+ * the `MyWidthDirective`. However, if `[style.width]` renders as `null` (so `myWidth=null`)
+ * then the `MyWidthDirective` will be able to write to the `width` style within the context.
+ * Simply put, whichever directive writes to a value ends up having ownership of it.
*
- * This means that every time `updateStyleProp` or `updateClassProp` are called then they
- * must be called using an index value (not a property string) which references the index
- * value of the initial style prop/class when the context was created. This also means that
- * `updateStyleProp` or `updateClassProp` cannot be called with a new property (only
- * `updateStylingMap` can include new CSS properties that will be added to the context).
+ * The way in which the ownership is facilitated is through index value. The earliest directives
+ * get the smallest index values (with 0 being reserved for the template element bindings). Each
+ * time a value is written from a directive or the template bindings, the value itself gets
+ * assigned the directive index value in its data. If another directive writes a value again then
+ * its directive index gets compared against the directive index that exists on the element. Only
+ * when the new value's directive index is less than the existing directive index then the new
+ * value will be written to the context.
+ *
+ * Each directive also has its own sanitizer and dirty flags. These values are consumed within the
+ * rendering function.
*/
-export interface StylingContext extends Array {
- /**
- * Location of animation context (which contains the active players) for this element styling
- * context.
- */
- [StylingIndex.PlayerContext]: PlayerContext|null;
-
- /**
- * The style sanitizer that is used within this context
- */
- [StylingIndex.StyleSanitizerPosition]: StyleSanitizeFn|null;
-
- /**
- * Location of initial data shared by all instances of this style.
- */
- [StylingIndex.InitialStylesPosition]: InitialStyles;
-
+export interface StylingContext extends
+ Array<{[key: string]: any}|number|string|boolean|RElement|StyleSanitizeFn|PlayerContext|null> {
/**
* A numeric value representing the configuration status (whether the context is dirty or not)
* mixed together (using bit shifting) with a index value which tells the starting index value
@@ -140,12 +209,27 @@ export interface StylingContext extends Array { [0]: null; }
+export interface InitialStylingValues extends Array { [0]: null; }
+
+/**
+ * Used as an offset/position index to figure out where initial styling
+ * values are located.
+ *
+ * Used as a reference point to provide markers to all static styling
+ * values (the initial style and class values on an element) within an
+ * array within the StylingContext. This array contains key/value pairs
+ * where the key is the style property name or className and the value is
+ * the style value or whether or not a class is present on the elment.
+ *
+ * The first value is also always null so that a initial index value of
+ * `0` will always point to a null value.
+ *
+ * If a
elements contains a list of static styling values like so:
+ *
+ *
+ *
+ * Then the initial styles for that will look like so:
+ *
+ * Styles:
+ * StylingContext[InitialStylesIndex] = [
+ * null, 'width', '100px', height, '200px'
+ * ]
+ *
+ * Classes:
+ * StylingContext[InitialStylesIndex] = [
+ * null, 'foo', true, 'bar', true, 'baz', true
+ * ]
+ *
+ * Initial style and class entries have their own arrays. This is because
+ * it's easier to add to the end of one array and not then have to update
+ * every context entries' pointer index to the newly offseted values.
+ *
+ * When property bindinds are added to a context then initial style/class
+ * values will also be inserted into the array. This is to create a space
+ * in the situation when a follow-up directive inserts static styling into
+ * the array. By default style values are `null` and class values are
+ * `false` when inserted by property bindings.
+ *
+ * For example:
+ *
+ *
+ * Will construct initial styling values that look like:
+ *
+ * Styles:
+ * StylingContext[InitialStylesIndex] = [
+ * null, 'width', '100px', height, '200px', 'opacity', null
+ * ]
+ *
+ * Classes:
+ * StylingContext[InitialStylesIndex] = [
+ * null, 'foo', true, 'bar', true, 'baz', true, 'car', false
+ * ]
+ *
+ * Now if a directive comes along and introduces `car` as a static
+ * class value or `opacity` then those values will be filled into
+ * the initial styles array.
+ *
+ * For example:
+ *
+ * @Directive({
+ * selector: 'opacity-car-directive',
+ * host: {
+ * 'style': 'opacity:0.5',
+ * 'class': 'car'
+ * }
+ * })
+ * class OpacityCarDirective {}
+ *
+ * This will render itself as:
+ *
+ * Styles:
+ * StylingContext[InitialStylesIndex] = [
+ * null, 'width', '100px', height, '200px', 'opacity', null
+ * ]
+ *
+ * Classes:
+ * StylingContext[InitialStylesIndex] = [
+ * null, 'foo', true, 'bar', true, 'baz', true, 'car', false
+ * ]
+ */
+export const enum InitialStylingValuesIndex {
+ KeyValueStartPosition = 1,
+ PropOffset = 0,
+ ValueOffset = 1,
+ Size = 2
+}
+
+/**
+ * An array located in the StylingContext that houses all directive instances and additional
+ * data about them.
+ *
+ * Each entry in this array represents a source of where style/class binding values could
+ * come from. By default, there is always at least one directive here with a null value and
+ * that represents bindings that live directly on an element (not host bindings).
+ *
+ * Each successive entry in the array is an actual instance of an array as well as some
+ * additional info.
+ *
+ * An entry within this array has the following values:
+ * [0] = The instance of the directive (or null when it is not a directive, but a template binding
+ * source)
+ * [1] = The pointer that tells where the single styling (stuff like [class.foo] and [style.prop])
+ * offset values are located. This value will allow for a binding instruction to find exactly
+ * where a style is located.
+ * [2] = Whether or not the directive has any styling values that are dirty. This is used as
+ * reference within the renderClassAndStyleBindings function to decide whether to skip
+ * iterating through the context when rendering is executed.
+ * [3] = The styleSanitizer instance that is assigned to the directive. Although it's unlikely,
+ * a directive could introduce its own special style sanitizer and for this reach each
+ * directive will get its own space for it (if null then the very first sanitizer is used).
+ *
+ * Each time a new directive is added it will insert these four values at the end of the array.
+ * When this array is examined (using indexOf) then the resulting directiveIndex will be resolved
+ * by dividing the index value by the size of the array entries (so if DirA is at spot 8 then its
+ * index will be 2).
+ */
+export interface DirectiveRegistryValues extends Array {
+ [DirectiveRegistryValuesIndex.DirectiveValueOffset]: null;
+ [DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset]: number;
+ [DirectiveRegistryValuesIndex.DirtyFlagOffset]: boolean;
+ [DirectiveRegistryValuesIndex.StyleSanitizerOffset]: StyleSanitizeFn|null;
+}
+
+/**
+ * An enum that outlines the offset/position values for each directive entry and its data
+ * that are housed inside of [DirectiveRegistryValues].
+ */
+export const enum DirectiveRegistryValuesIndex {
+ DirectiveValueOffset = 0,
+ SinglePropValuesIndexOffset = 1,
+ DirtyFlagOffset = 2,
+ StyleSanitizerOffset = 3,
+ Size = 4
+}
+
+/**
+ * An array that contains the index pointer values for every single styling property
+ * that exists in the context and for every directive. It also contains the total
+ * single styles and single classes that exists in the context as the first two values.
+ *
+ * Let's say we have the following template code:
+ *
+ *
+ * directive-with-foo-bar-classes>
+ *
+ * We have two directive and template-binding sources,
+ * 2 + 1 styles and 1 + 1 classes. When the bindings are
+ * registered the SinglePropOffsets array will look like so:
+ *
+ * s_0/c_0 = template directive value
+ * s_1/c_1 = directive one (directive-with-opacity)
+ * s_2/c_2 = directive two (directive-with-foo-bar-classes)
+ *
+ * [3, 2, 2, 1, s_00, s01, c_01, 1, 0, s_10, 0, 1, c_20
+ */
+export interface SinglePropOffsetValues extends Array {
+ [SinglePropOffsetValuesIndex.StylesCountPosition]: number;
+ [SinglePropOffsetValuesIndex.ClassesCountPosition]: number;
+}
+
+/**
+ * An enum that outlines the offset/position values for each single prop/class entry
+ * that are housed inside of [SinglePropOffsetValues].
+ */
+export const enum SinglePropOffsetValuesIndex {
+ StylesCountPosition = 0,
+ ClassesCountPosition = 1,
+ ValueStartPosition = 2
+}
/**
* Used to set the context to be dirty or not both on the master flag (position 1)
@@ -181,47 +447,49 @@ export interface InitialStyles extends Array { [0]: null; }
*/
export const enum StylingFlags {
// Implies no configurations
- None = 0b00000,
+ None = 0b000000,
// Whether or not the entry or context itself is dirty
- Dirty = 0b00001,
+ Dirty = 0b000001,
// Whether or not this is a class-based assignment
- Class = 0b00010,
+ Class = 0b000010,
// Whether or not a sanitizer was applied to this property
- Sanitize = 0b00100,
+ Sanitize = 0b000100,
// Whether or not any player builders within need to produce new players
- PlayerBuildersDirty = 0b01000,
+ PlayerBuildersDirty = 0b001000,
// If NgClass is present (or some other class handler) then it will handle the map expressions and
// initial classes
- OnlyProcessSingleClasses = 0b10000,
+ OnlyProcessSingleClasses = 0b010000,
// The max amount of bits used to represent these configuration values
- BitCountSize = 5,
- // There are only five bits here
- BitMask = 0b11111
+ BindingAllocationLocked = 0b100000,
+ BitCountSize = 6,
+ // There are only six bits here
+ BitMask = 0b111111
}
/** Used as numeric pointer values to determine what cells to update in the `StylingContext` */
export const enum StylingIndex {
- // Position of where the initial styles are stored in the styling context
- PlayerContext = 0,
- // Position of where the style sanitizer is stored within the styling context
- StyleSanitizerPosition = 1,
- // Position of where the initial styles are stored in the styling context
- InitialStylesPosition = 2,
// Index of location where the start of single properties are stored. (`updateStyleProp`)
- MasterFlagPosition = 3,
+ MasterFlagPosition = 0,
+ // Position of where the registered directives exist for this styling context
+ DirectiveRegistryPosition = 1,
+ // Position of where the initial styles are stored in the styling context
+ InitialStyleValuesPosition = 2,
+ InitialClassValuesPosition = 3,
// Index of location where the class index offset value is located
- ClassOffsetPosition = 4,
+ SinglePropOffsetPositions = 4,
// Position of where the initial styles are stored in the styling context
// This index must align with HOST, see interfaces/view.ts
ElementPosition = 5,
// Position of where the last string-based CSS class value was stored (or a cached version of the
// initial styles when a [class] directive is present)
- PreviousOrCachedMultiClassValue = 6,
+ CachedClassValueOrInitialClassString = 6,
// Position of where the last string-based CSS class value was stored
- PreviousMultiStyleValue = 7,
- // Location of single (prop) value entries are stored within the context
- SingleStylesStartPosition = 8,
+ CachedStyleValue = 7,
// Multi and single entries are stored in `StylingContext` as: Flag; PropertyName; PropertyValue
+ // Position of where the initial styles are stored in the styling context
+ PlayerContext = 8,
+ // Location of single (prop) value entries are stored within the context
+ SingleStylesStartPosition = 9,
FlagsOffset = 0,
PropertyOffset = 1,
ValueOffset = 2,
@@ -233,3 +501,16 @@ export const enum StylingIndex {
// The binary digit value as a mask
BitMask = 0b11111111111111, // 14 bits
}
+
+/**
+ * An enum that outlines the bit flag data for directive owner and player index
+ * values that exist within en entry that lives in the StylingContext.
+ *
+ * The values here split a number value into two sets of bits:
+ * - The first 16 bits are used to store the directiveIndex that owns this style value
+ * - The other 16 bits are used to store the playerBuilderIndex that is attached to this style
+ */
+export const enum DirectiveOwnerAndPlayerBuilderIndex {
+ BitCountSize = 16,
+ BitMask = 0b1111111111111111
+}
diff --git a/packages/core/src/render3/jit/directive.ts b/packages/core/src/render3/jit/directive.ts
index 383f124240..89e2696718 100644
--- a/packages/core/src/render3/jit/directive.ts
+++ b/packages/core/src/render3/jit/directive.ts
@@ -12,9 +12,9 @@ import {Component, Directive} from '../../metadata/directives';
import {componentNeedsResolution, maybeQueueResolutionOfComponentResources} from '../../metadata/resource_loading';
import {ViewEncapsulation} from '../../metadata/view';
import {Type} from '../../type';
-import {stringify} from '../../util';
-import {EMPTY_ARRAY} from '../definition';
+import {EMPTY_ARRAY, EMPTY_OBJ} from '../empty';
import {NG_COMPONENT_DEF, NG_DIRECTIVE_DEF} from '../fields';
+import {stringify} from '../util';
import {R3DirectiveMetadataFacade, getCompilerFacade} from './compiler_facade';
import {R3ComponentMetadataFacade, R3QueryMetadataFacade} from './compiler_facade_interface';
@@ -154,8 +154,6 @@ function directiveMetadata(type: Type, metadata: Directive): R3DirectiveMet
};
}
-const EMPTY_OBJ = {};
-
function convertToR3QueryPredicate(selector: any): any|string[] {
return typeof selector === 'string' ? splitByComma(selector) : selector;
}
diff --git a/packages/core/src/render3/jit/environment.ts b/packages/core/src/render3/jit/environment.ts
index a40d6fa0fc..1a30e41a4c 100644
--- a/packages/core/src/render3/jit/environment.ts
+++ b/packages/core/src/render3/jit/environment.ts
@@ -90,6 +90,7 @@ export const angularCoreEnv: {[name: string]: Function} = {
'ɵregisterContentQuery': r3.registerContentQuery,
'ɵreference': r3.reference,
'ɵelementStyling': r3.elementStyling,
+ 'ɵelementHostAttrs': r3.elementHostAttrs,
'ɵelementStylingMap': r3.elementStylingMap,
'ɵelementStyleProp': r3.elementStyleProp,
'ɵelementStylingApply': r3.elementStylingApply,
diff --git a/packages/core/src/render3/styling/class_and_style_bindings.ts b/packages/core/src/render3/styling/class_and_style_bindings.ts
index bf2046dd86..a84d3b7041 100644
--- a/packages/core/src/render3/styling/class_and_style_bindings.ts
+++ b/packages/core/src/render3/styling/class_and_style_bindings.ts
@@ -1,15 +1,17 @@
/**
- * @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
- */
+* @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 {StyleSanitizeFn} from '../../sanitization/style_sanitizer';
-import {InitialStylingFlags} from '../interfaces/definition';
+import {assertNotEqual} from '../assert';
+import {EMPTY_ARRAY, EMPTY_OBJ} from '../empty';
+import {AttributeMarker, TAttributes} from '../interfaces/node';
import {BindingStore, BindingType, Player, PlayerBuilder, PlayerFactory, PlayerIndex} from '../interfaces/player';
-import {Renderer3, RendererStyleFlags3, isProceduralRenderer} from '../interfaces/renderer';
-import {InitialStyles, StylingContext, StylingFlags, StylingIndex} from '../interfaces/styling';
+import {RElement, Renderer3, RendererStyleFlags3, isProceduralRenderer} from '../interfaces/renderer';
+import {DirectiveOwnerAndPlayerBuilderIndex, DirectiveRegistryValues, DirectiveRegistryValuesIndex, InitialStylingValues, InitialStylingValuesIndex, SinglePropOffsetValues, SinglePropOffsetValuesIndex, StylingContext, StylingFlags, StylingIndex} from '../interfaces/styling';
import {LView, RootContext} from '../interfaces/view';
import {NO_CHANGE} from '../tokens';
import {getRootContext} from '../util';
@@ -17,150 +19,434 @@ import {getRootContext} from '../util';
import {BoundPlayerFactory} from './player_factory';
import {addPlayerInternal, allocPlayerContext, createEmptyStylingContext, getPlayerContext} from './util';
-const EMPTY_ARR: any[] = [];
-const EMPTY_OBJ: {[key: string]: any} = {};
+
+/**
+ * This file includes the code to power all styling-binding operations in Angular.
+ *
+ * These include:
+ * [style]="myStyleObj"
+ * [class]="myClassObj"
+ * [style.prop]="myPropValue"
+ * [class.name]="myClassValue"
+ *
+ * There are many different ways in which these functions below are called. Please see
+ * `interfaces/styles.ts` to get a better idea of how the styling algorithm works.
+ */
+
/**
- * Creates a styling context template where styling information is stored.
- * Any styles that are later referenced using `updateStyleProp` must be
- * passed in within this function. Initial values for those styles are to
- * be declared after all initial style properties are declared (this change in
- * mode between declarations and initial styles is made possible using a special
- * enum value found in `definition.ts`).
- *
- * @param initialStyleDeclarations a list of style declarations and initial style values
- * that are used later within the styling context.
- *
- * -> ['width', 'height', SPECIAL_ENUM_VAL, 'width', '100px']
- * This implies that `width` and `height` will be later styled and that the `width`
- * property has an initial value of `100px`.
- *
- * @param initialClassDeclarations a list of class declarations and initial class values
- * that are used later within the styling context.
- *
- * -> ['foo', 'bar', SPECIAL_ENUM_VAL, 'foo', true]
- * This implies that `foo` and `bar` will be later styled and that the `foo`
- * class will be applied to the element as an initial class since it's true
+ * Creates a new StylingContext an fills it with the provided static styling attribute values.
*/
-export function createStylingContextTemplate(
- initialClassDeclarations?: (string | boolean | InitialStylingFlags)[] | null,
- initialStyleDeclarations?: (string | boolean | InitialStylingFlags)[] | null,
- styleSanitizer?: StyleSanitizeFn | null, onlyProcessSingleClasses?: boolean): StylingContext {
- const initialStylingValues: InitialStyles = [null];
- const context: StylingContext =
- createEmptyStylingContext(null, styleSanitizer, initialStylingValues);
+export function initializeStaticContext(attrs: TAttributes) {
+ const context = createEmptyStylingContext();
+ const initialClasses: InitialStylingValues = context[StylingIndex.InitialClassValuesPosition] =
+ [null];
+ const initialStyles: InitialStylingValues = context[StylingIndex.InitialStyleValuesPosition] =
+ [null];
- // we use two maps since a class name might collide with a CSS style prop
- const stylesLookup: {[key: string]: number} = {};
- const classesLookup: {[key: string]: number} = {};
-
- let totalStyleDeclarations = 0;
- if (initialStyleDeclarations) {
- let hasPassedDeclarations = false;
- for (let i = 0; i < initialStyleDeclarations.length; i++) {
- const v = initialStyleDeclarations[i] as string | InitialStylingFlags;
-
- // this flag value marks where the declarations end the initial values begin
- if (v === InitialStylingFlags.VALUES_MODE) {
- hasPassedDeclarations = true;
- } else {
- const prop = v as string;
- if (hasPassedDeclarations) {
- const value = initialStyleDeclarations[++i] as string;
- initialStylingValues.push(value);
- stylesLookup[prop] = initialStylingValues.length - 1;
- } else {
- totalStyleDeclarations++;
- stylesLookup[prop] = 0;
- }
- }
+ // The attributes array has marker values (numbers) indicating what the subsequent
+ // values represent. When we encounter a number, we set the mode to that type of attribute.
+ let mode = -1;
+ for (let i = 0; i < attrs.length; i++) {
+ const attr = attrs[i];
+ if (typeof attr == 'number') {
+ mode = attr;
+ } else if (mode === AttributeMarker.Styles) {
+ initialStyles.push(attr as string, attrs[++i] as string);
+ } else if (mode === AttributeMarker.Classes) {
+ initialClasses.push(attr as string, true);
+ } else if (mode === AttributeMarker.SelectOnly) {
+ break;
}
}
- // make where the class offsets begin
- context[StylingIndex.ClassOffsetPosition] = totalStyleDeclarations;
-
- const initialStaticClasses: string[]|null = onlyProcessSingleClasses ? [] : null;
- if (initialClassDeclarations) {
- let hasPassedDeclarations = false;
- for (let i = 0; i < initialClassDeclarations.length; i++) {
- const v = initialClassDeclarations[i] as string | boolean | InitialStylingFlags;
- // this flag value marks where the declarations end the initial values begin
- if (v === InitialStylingFlags.VALUES_MODE) {
- hasPassedDeclarations = true;
- } else {
- const className = v as string;
- if (hasPassedDeclarations) {
- const value = initialClassDeclarations[++i] as boolean;
- initialStylingValues.push(value);
- classesLookup[className] = initialStylingValues.length - 1;
- initialStaticClasses && initialStaticClasses.push(className);
- } else {
- classesLookup[className] = 0;
- }
- }
- }
- }
-
- const styleProps = Object.keys(stylesLookup);
- const classNames = Object.keys(classesLookup);
- const classNamesIndexStart = styleProps.length;
- const totalProps = styleProps.length + classNames.length;
-
- // *2 because we are filling for both single and multi style spaces
- const maxLength = totalProps * StylingIndex.Size * 2 + StylingIndex.SingleStylesStartPosition;
-
- // we need to fill the array from the start so that we can access
- // both the multi and the single array positions in the same loop block
- for (let i = StylingIndex.SingleStylesStartPosition; i < maxLength; i++) {
- context.push(null);
- }
-
- const singleStart = StylingIndex.SingleStylesStartPosition;
- const multiStart = totalProps * StylingIndex.Size + StylingIndex.SingleStylesStartPosition;
-
- // fill single and multi-level styles
- for (let i = 0; i < totalProps; i++) {
- const isClassBased = i >= classNamesIndexStart;
- const prop = isClassBased ? classNames[i - classNamesIndexStart] : styleProps[i];
- const indexForInitial = isClassBased ? classesLookup[prop] : stylesLookup[prop];
- const initialValue = initialStylingValues[indexForInitial];
-
- const indexForMulti = i * StylingIndex.Size + multiStart;
- const indexForSingle = i * StylingIndex.Size + singleStart;
- const initialFlag = prepareInitialFlag(prop, isClassBased, styleSanitizer || null);
-
- setFlag(context, indexForSingle, pointers(initialFlag, indexForInitial, indexForMulti));
- setProp(context, indexForSingle, prop);
- setValue(context, indexForSingle, null);
- setPlayerBuilderIndex(context, indexForSingle, 0);
-
- const flagForMulti =
- initialFlag | (initialValue !== null ? StylingFlags.Dirty : StylingFlags.None);
- setFlag(context, indexForMulti, pointers(flagForMulti, indexForInitial, indexForSingle));
- setProp(context, indexForMulti, prop);
- setValue(context, indexForMulti, null);
- setPlayerBuilderIndex(context, indexForMulti, 0);
- }
-
- // there is no initial value flag for the master index since it doesn't
- // reference an initial style value
- const masterFlag = pointers(0, 0, multiStart) |
- (onlyProcessSingleClasses ? StylingFlags.OnlyProcessSingleClasses : 0);
- setFlag(context, StylingIndex.MasterFlagPosition, masterFlag);
- setContextDirty(context, initialStylingValues.length > 1);
-
- if (initialStaticClasses) {
- context[StylingIndex.PreviousOrCachedMultiClassValue] = initialStaticClasses.join(' ');
- }
-
return context;
}
+/**
+ * Designed to update an existing styling context with new static styling
+ * data (classes and styles).
+ *
+ * @param context the existing styling context
+ * @param attrs an array of new static styling attributes that will be
+ * assigned to the context
+ * @param directive the directive instance with which static data is associated with.
+ */
+export function patchContextWithStaticAttrs(
+ context: StylingContext, attrs: TAttributes, directive: any): void {
+ // If the styling context has already been patched with the given directive's bindings,
+ // then there is no point in doing it again. The reason why this may happen (the directive
+ // styling being patched twice) is because the `stylingBinding` function is called each time
+ // an element is created (both within a template function and within directive host bindings).
+ const directives = context[StylingIndex.DirectiveRegistryPosition];
+ if (getDirectiveRegistryValuesIndexOf(directives, directive) == -1) {
+ // this is a new directive which we have not seen yet.
+ directives.push(directive, -1, false, null);
+
+ let initialClasses: InitialStylingValues|null = null;
+ let initialStyles: InitialStylingValues|null = null;
+
+ let mode = -1;
+ for (let i = 0; i < attrs.length; i++) {
+ const attr = attrs[i];
+ if (typeof attr == 'number') {
+ mode = attr;
+ } else if (mode == AttributeMarker.Classes) {
+ initialClasses = initialClasses || context[StylingIndex.InitialClassValuesPosition];
+ patchInitialStylingValue(initialClasses, attr, true);
+ } else if (mode == AttributeMarker.Styles) {
+ initialStyles = initialStyles || context[StylingIndex.InitialStyleValuesPosition];
+ patchInitialStylingValue(initialStyles, attr, attrs[++i]);
+ }
+ }
+ }
+}
+
+/**
+ * Designed to add a style or class value into the existing set of initial styles.
+ *
+ * The function will search and figure out if a style/class value is already present
+ * within the provided initial styling array. If and when a style/class value is not
+ * present (or if it's value is falsy) then it will be inserted/updated in the list
+ * of initial styling values.
+ */
+function patchInitialStylingValue(
+ initialStyling: InitialStylingValues, prop: string, value: any): void {
+ // Even values are keys; Odd numbers are values; Search keys only
+ for (let i = InitialStylingValuesIndex.KeyValueStartPosition; i < initialStyling.length;) {
+ const key = initialStyling[i];
+ if (key === prop) {
+ const existingValue = initialStyling[i + InitialStylingValuesIndex.ValueOffset];
+
+ // If there is no previous style value (when `null`) or no previous class
+ // applied (when `false`) then we update the the newly given value.
+ if (existingValue == null || existingValue == false) {
+ initialStyling[i + InitialStylingValuesIndex.ValueOffset] = value;
+ }
+ return;
+ }
+ i = i + InitialStylingValuesIndex.Size;
+ }
+ // We did not find existing key, add a new one.
+ initialStyling.push(prop, value);
+}
+
+/**
+ * Runs through the initial styling data present in the context and renders
+ * them via the renderer on the element.
+ */
+export function renderInitialStylesAndClasses(
+ element: RElement, context: StylingContext, renderer: Renderer3) {
+ const initialClasses = context[StylingIndex.InitialClassValuesPosition];
+ renderInitialStylingValues(element, renderer, initialClasses, true);
+
+ const initialStyles = context[StylingIndex.InitialStyleValuesPosition];
+ renderInitialStylingValues(element, renderer, initialStyles, false);
+}
+
+/**
+ * This is a helper function designed to render each entry present within the
+ * provided list of initialStylingValues.
+ */
+function renderInitialStylingValues(
+ element: RElement, renderer: Renderer3, initialStylingValues: InitialStylingValues,
+ isEntryClassBased: boolean) {
+ for (let i = InitialStylingValuesIndex.KeyValueStartPosition; i < initialStylingValues.length;
+ i += InitialStylingValuesIndex.Size) {
+ const value = initialStylingValues[i + InitialStylingValuesIndex.ValueOffset];
+ if (value) {
+ if (isEntryClassBased) {
+ setClass(
+ element, initialStylingValues[i + InitialStylingValuesIndex.PropOffset] as string, true,
+ renderer, null);
+ } else {
+ setStyle(
+ element, initialStylingValues[i + InitialStylingValuesIndex.PropOffset] as string,
+ value as string, renderer, null);
+ }
+ }
+ }
+}
+
+export function allowNewBindingsForStylingContext(context: StylingContext): boolean {
+ return (context[StylingIndex.MasterFlagPosition] & StylingFlags.BindingAllocationLocked) === 0;
+}
+
+/**
+ * Adds in new binding values to a styling context.
+ *
+ * If a directive value is provided then all provided class/style binding names will
+ * reference the provided directive.
+ *
+ * @param context the existing styling context
+ * @param directiveRef the directive that the new bindings will reference
+ * @param classBindingNames an array of class binding names that will be added to the context
+ * @param styleBindingNames an array of style binding names that will be added to the context
+ * @param styleSanitizer an optional sanitizer that handle all sanitization on for each of
+ * the bindings added to the context. Note that if a directive is provided then the sanitizer
+ * instance will only be active if and when the directive updates the bindings that it owns.
+ */
+export function updateContextWithBindings(
+ context: StylingContext, directiveRef: any | null, classBindingNames?: string[] | null,
+ styleBindingNames?: string[] | null, styleSanitizer?: StyleSanitizeFn | null,
+ onlyProcessSingleClasses?: boolean) {
+ if (context[StylingIndex.MasterFlagPosition] & StylingFlags.BindingAllocationLocked) return;
+
+ // this means the context has already been patched with the directive's bindings
+ const directiveIndex = findOrPatchDirectiveIntoRegistry(context, directiveRef, styleSanitizer);
+ if (directiveIndex === -1) {
+ // this means the directive has already been patched in ... No point in doing anything
+ return;
+ }
+
+ // there are alot of variables being used below to track where in the context the new
+ // binding values will be placed. Because the context consists of multiple types of
+ // entries (single classes/styles and multi classes/styles) alot of the index positions
+ // need to be computed ahead of time and the context needs to be extended before the values
+ // are inserted in.
+ const singlePropOffsetValues = context[StylingIndex.SinglePropOffsetPositions];
+ const totalCurrentClassBindings =
+ singlePropOffsetValues[SinglePropOffsetValuesIndex.ClassesCountPosition];
+ const totalCurrentStyleBindings =
+ singlePropOffsetValues[SinglePropOffsetValuesIndex.StylesCountPosition];
+
+ const classesOffset = totalCurrentClassBindings * StylingIndex.Size;
+ const stylesOffset = totalCurrentStyleBindings * StylingIndex.Size;
+
+ const singleStylesStartIndex = StylingIndex.SingleStylesStartPosition;
+ let singleClassesStartIndex = singleStylesStartIndex + stylesOffset;
+ let multiStylesStartIndex = singleClassesStartIndex + classesOffset;
+ let multiClassesStartIndex = multiStylesStartIndex + stylesOffset;
+
+ // because we're inserting more bindings into the context, this means that the
+ // binding values need to be referenced the singlePropOffsetValues array so that
+ // the template/directive can easily find them inside of the `elementStyleProp`
+ // and the `elementClassProp` functions without iterating through the entire context.
+ // The first step to setting up these reference points is to mark how many bindings
+ // are being added. Even if these bindings already exist in the context, the directive
+ // or template code will still call them unknowingly. Therefore the total values need
+ // to be registered so that we know how many bindings are assigned to each directive.
+ const currentSinglePropsLength = singlePropOffsetValues.length;
+ singlePropOffsetValues.push(
+ styleBindingNames ? styleBindingNames.length : 0,
+ classBindingNames ? classBindingNames.length : 0);
+
+ // the code below will check to see if a new style binding already exists in the context
+ // if so then there is no point in inserting it into the context again. Whether or not it
+ // exists the styling offset code will now know exactly where it is
+ let insertionOffset = 0;
+ const filteredStyleBindingNames: string[] = [];
+ if (styleBindingNames && styleBindingNames.length) {
+ for (let i = 0; i < styleBindingNames.length; i++) {
+ const name = styleBindingNames[i];
+ let singlePropIndex =
+ getMatchingBindingIndex(context, name, singleStylesStartIndex, singleClassesStartIndex);
+ if (singlePropIndex == -1) {
+ singlePropIndex = singleClassesStartIndex + insertionOffset;
+ insertionOffset += StylingIndex.Size;
+ filteredStyleBindingNames.push(name);
+ }
+ singlePropOffsetValues.push(singlePropIndex);
+ }
+ }
+
+ // just like with the style binding loop above, the new class bindings get the same treatment...
+ const filteredClassBindingNames: string[] = [];
+ if (classBindingNames && classBindingNames.length) {
+ for (let i = 0; i < classBindingNames.length; i++) {
+ const name = classBindingNames[i];
+ let singlePropIndex =
+ getMatchingBindingIndex(context, name, singleClassesStartIndex, multiStylesStartIndex);
+ if (singlePropIndex == -1) {
+ singlePropIndex = multiStylesStartIndex + insertionOffset;
+ insertionOffset += StylingIndex.Size;
+ filteredClassBindingNames.push(name);
+ } else {
+ singlePropIndex += filteredStyleBindingNames.length * StylingIndex.Size;
+ }
+ singlePropOffsetValues.push(singlePropIndex);
+ }
+ }
+
+ // because new styles are being inserted, this means the existing collection of style offset
+ // index values are incorrect (they point to the wrong values). The code below will run through
+ // the entire offset array and update the existing set of index values to point to their new
+ // locations while taking the new binding values into consideration.
+ let i = SinglePropOffsetValuesIndex.ValueStartPosition;
+ if (filteredStyleBindingNames.length) {
+ while (i < currentSinglePropsLength) {
+ const totalStyles =
+ singlePropOffsetValues[i + SinglePropOffsetValuesIndex.StylesCountPosition];
+ const totalClasses =
+ singlePropOffsetValues[i + SinglePropOffsetValuesIndex.ClassesCountPosition];
+ if (totalClasses) {
+ const start = i + SinglePropOffsetValuesIndex.ValueStartPosition + totalStyles;
+ for (let j = start; j < start + totalClasses; j++) {
+ singlePropOffsetValues[j] += filteredStyleBindingNames.length * StylingIndex.Size;
+ }
+ }
+
+ const total = totalStyles + totalClasses;
+ i += SinglePropOffsetValuesIndex.ValueStartPosition + total;
+ }
+ }
+
+ const totalNewEntries = filteredClassBindingNames.length + filteredStyleBindingNames.length;
+
+ // in the event that there are new style values being inserted, all existing class and style
+ // bindings need to have their pointer values offsetted with the new amount of space that is
+ // used for the new style/class bindings.
+ for (let i = singleStylesStartIndex; i < context.length; i += StylingIndex.Size) {
+ const isMultiBased = i >= multiStylesStartIndex;
+ const isClassBased = i >= (isMultiBased ? multiClassesStartIndex : singleClassesStartIndex);
+ const flag = getPointers(context, i);
+ const staticIndex = getInitialIndex(flag);
+ let singleOrMultiIndex = getMultiOrSingleIndex(flag);
+ if (isMultiBased) {
+ singleOrMultiIndex +=
+ isClassBased ? (filteredStyleBindingNames.length * StylingIndex.Size) : 0;
+ } else {
+ singleOrMultiIndex += (totalNewEntries * StylingIndex.Size) +
+ ((isClassBased ? filteredStyleBindingNames.length : 0) * StylingIndex.Size);
+ }
+ setFlag(context, i, pointers(flag, staticIndex, singleOrMultiIndex));
+ }
+
+ // this is where we make space in the context for the new style bindings
+ for (let i = 0; i < filteredStyleBindingNames.length * StylingIndex.Size; i++) {
+ context.splice(multiClassesStartIndex, 0, null);
+ context.splice(singleClassesStartIndex, 0, null);
+ singleClassesStartIndex++;
+ multiStylesStartIndex++;
+ multiClassesStartIndex += 2; // both single + multi slots were inserted
+ }
+
+ // this is where we make space in the context for the new class bindings
+ for (let i = 0; i < filteredClassBindingNames.length * StylingIndex.Size; i++) {
+ context.splice(multiStylesStartIndex, 0, null);
+ context.push(null);
+ multiStylesStartIndex++;
+ multiClassesStartIndex++;
+ }
+
+ const initialClasses = context[StylingIndex.InitialClassValuesPosition];
+ const initialStyles = context[StylingIndex.InitialStyleValuesPosition];
+
+ // the code below will insert each new entry into the context and assign the appropriate
+ // flags and index values to them. It's important this runs at the end of this function
+ // because the context, property offset and index values have all been computed just before.
+ for (let i = 0; i < totalNewEntries; i++) {
+ const entryIsClassBased = i >= filteredStyleBindingNames.length;
+ const adjustedIndex = entryIsClassBased ? (i - filteredStyleBindingNames.length) : i;
+ const propName = entryIsClassBased ? filteredClassBindingNames[adjustedIndex] :
+ filteredStyleBindingNames[adjustedIndex];
+
+ let multiIndex, singleIndex;
+ if (entryIsClassBased) {
+ multiIndex = multiClassesStartIndex +
+ ((totalCurrentClassBindings + adjustedIndex) * StylingIndex.Size);
+ singleIndex = singleClassesStartIndex +
+ ((totalCurrentClassBindings + adjustedIndex) * StylingIndex.Size);
+ } else {
+ multiIndex =
+ multiStylesStartIndex + ((totalCurrentStyleBindings + adjustedIndex) * StylingIndex.Size);
+ singleIndex = singleStylesStartIndex +
+ ((totalCurrentStyleBindings + adjustedIndex) * StylingIndex.Size);
+ }
+
+ // if a property is not found in the initial style values list then it
+ // is ALWAYS added incase a follow-up directive introduces the same initial
+ // style/class value later on.
+ let initialValuesToLookup = entryIsClassBased ? initialClasses : initialStyles;
+ let indexForInitial = getInitialStylingValuesIndexOf(initialValuesToLookup, propName);
+ if (indexForInitial === -1) {
+ indexForInitial = initialValuesToLookup.length + InitialStylingValuesIndex.ValueOffset;
+ initialValuesToLookup.push(propName, entryIsClassBased ? false : null);
+ } else {
+ indexForInitial += InitialStylingValuesIndex.ValueOffset;
+ }
+
+ const initialFlag =
+ prepareInitialFlag(context, propName, entryIsClassBased, styleSanitizer || null);
+
+ setFlag(context, singleIndex, pointers(initialFlag, indexForInitial, multiIndex));
+ setProp(context, singleIndex, propName);
+ setValue(context, singleIndex, null);
+ setPlayerBuilderIndex(context, singleIndex, 0, directiveIndex);
+
+ setFlag(context, multiIndex, pointers(initialFlag, indexForInitial, singleIndex));
+ setProp(context, multiIndex, propName);
+ setValue(context, multiIndex, null);
+ setPlayerBuilderIndex(context, multiIndex, 0, directiveIndex);
+ }
+
+ // the total classes/style values are updated so the next time the context is patched
+ // additional style/class bindings from another directive then it knows exactly where
+ // to insert them in the context
+ singlePropOffsetValues[SinglePropOffsetValuesIndex.ClassesCountPosition] =
+ totalCurrentClassBindings + filteredClassBindingNames.length;
+ singlePropOffsetValues[SinglePropOffsetValuesIndex.StylesCountPosition] =
+ totalCurrentStyleBindings + filteredStyleBindingNames.length;
+
+ // there is no initial value flag for the master index since it doesn't
+ // reference an initial style value
+ const masterFlag = pointers(0, 0, multiStylesStartIndex) |
+ (onlyProcessSingleClasses ? StylingFlags.OnlyProcessSingleClasses : 0);
+ setFlag(context, StylingIndex.MasterFlagPosition, masterFlag);
+}
+
+/**
+ * Searches through the existing registry of directives
+ */
+function findOrPatchDirectiveIntoRegistry(
+ context: StylingContext, directiveRef: any, styleSanitizer?: StyleSanitizeFn | null) {
+ const directiveRefs = context[StylingIndex.DirectiveRegistryPosition];
+ const nextOffsetInsertionIndex = context[StylingIndex.SinglePropOffsetPositions].length;
+
+ let directiveIndex: number;
+ const detectedIndex = getDirectiveRegistryValuesIndexOf(directiveRefs, directiveRef);
+
+ if (detectedIndex === -1) {
+ directiveIndex = directiveRefs.length / DirectiveRegistryValuesIndex.Size;
+ directiveRefs.push(directiveRef, nextOffsetInsertionIndex, false, styleSanitizer || null);
+ } else {
+ const singlePropStartPosition =
+ detectedIndex + DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset;
+ if (directiveRefs[singlePropStartPosition] ! >= 0) {
+ // the directive has already been patched into the context
+ return -1;
+ }
+
+ directiveIndex = detectedIndex / DirectiveRegistryValuesIndex.Size;
+
+ // because the directive already existed this means that it was set during elementHostAttrs or
+ // elementStart which means that the binding values were not here. Therefore, the values below
+ // need to be applied so that single class and style properties can be assigned later.
+ const singlePropPositionIndex =
+ detectedIndex + DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset;
+ directiveRefs[singlePropPositionIndex] = nextOffsetInsertionIndex;
+
+ // the sanitizer is also apart of the binding process and will be used when bindings are
+ // applied.
+ const styleSanitizerIndex = detectedIndex + DirectiveRegistryValuesIndex.StyleSanitizerOffset;
+ directiveRefs[styleSanitizerIndex] = styleSanitizer || null;
+ }
+
+ return directiveIndex;
+}
+
+function getMatchingBindingIndex(
+ context: StylingContext, bindingName: string, start: number, end: number) {
+ for (let j = start; j < end; j += StylingIndex.Size) {
+ if (getProp(context, j) === bindingName) return j;
+ }
+ return -1;
+}
+
/**
* Sets and resolves all `multi` styling on an `StylingContext` so that they can be
- * applied to the element once `renderStyleAndClassBindings` is called.
+ * applied to the element once `renderStyling` is called.
*
* All missing styles/class (any values that are not provided in the new `styles`
* or `classes` params) will resolve to `null` within their respective positions
@@ -175,9 +461,11 @@ export function updateStylingMap(
context: StylingContext, classesInput: {[key: string]: any} | string |
BoundPlayerFactory| NO_CHANGE | null,
stylesInput?: {[key: string]: any} | BoundPlayerFactory| NO_CHANGE |
- null): void {
+ null,
+ directiveRef?: any): void {
stylesInput = stylesInput || null;
+ const directiveIndex = getDirectiveIndexFromRegistry(context, directiveRef || null);
const element = context[StylingIndex.ElementPosition] !as HTMLElement;
const classesPlayerBuilder = classesInput instanceof BoundPlayerFactory ?
new ClassAndStylePlayerBuilder(classesInput as any, element, BindingType.Class) :
@@ -192,15 +480,15 @@ export function updateStylingMap(
const stylesValue = stylesPlayerBuilder ? stylesInput !.value : stylesInput;
// early exit (this is what's done to avoid using ctx.bind() to cache the value)
const ignoreAllClassUpdates = limitToSingleClasses(context) || classesValue === NO_CHANGE ||
- classesValue === context[StylingIndex.PreviousOrCachedMultiClassValue];
+ classesValue === context[StylingIndex.CachedClassValueOrInitialClassString];
const ignoreAllStyleUpdates =
- stylesValue === NO_CHANGE || stylesValue === context[StylingIndex.PreviousMultiStyleValue];
+ stylesValue === NO_CHANGE || stylesValue === context[StylingIndex.CachedStyleValue];
if (ignoreAllClassUpdates && ignoreAllStyleUpdates) return;
- context[StylingIndex.PreviousOrCachedMultiClassValue] = classesValue;
- context[StylingIndex.PreviousMultiStyleValue] = stylesValue;
+ context[StylingIndex.CachedClassValueOrInitialClassString] = classesValue;
+ context[StylingIndex.CachedStyleValue] = stylesValue;
- let classNames: string[] = EMPTY_ARR;
+ let classNames: string[] = EMPTY_ARRAY;
let applyAllClasses = false;
let playerBuildersAreDirty = false;
@@ -229,16 +517,16 @@ export function updateStylingMap(
// since a classname string implies that all those classes are added
applyAllClasses = true;
} else {
- classNames = classesValue ? Object.keys(classesValue) : EMPTY_ARR;
+ classNames = classesValue ? Object.keys(classesValue) : EMPTY_ARRAY;
}
}
const classes = (classesValue || EMPTY_OBJ) as{[key: string]: any};
- const styleProps = stylesValue ? Object.keys(stylesValue) : EMPTY_ARR;
+ const styleProps = stylesValue ? Object.keys(stylesValue) : EMPTY_ARRAY;
const styles = stylesValue || EMPTY_OBJ;
const classesStartIndex = styleProps.length;
- const multiStartIndex = getMultiStartIndex(context);
+ let multiStartIndex = getMultiStartIndex(context);
let dirty = false;
let ctxIndex = multiStartIndex;
@@ -269,7 +557,7 @@ export function updateStylingMap(
if (prop === newProp) {
const value = getValue(context, ctxIndex);
const flag = getPointers(context, ctxIndex);
- setPlayerBuilderIndex(context, ctxIndex, playerBuilderIndex);
+ setPlayerBuilderIndex(context, ctxIndex, playerBuilderIndex, directiveIndex);
if (hasValueChanged(flag, value, newValue)) {
setValue(context, ctxIndex, newValue);
@@ -277,9 +565,13 @@ export function updateStylingMap(
const initialValue = getInitialValue(context, flag);
- // there is no point in setting this to dirty if the previously
- // rendered value was being referenced by the initial style (or null)
- if (hasValueChanged(flag, initialValue, newValue)) {
+ // SKIP IF INITIAL CHECK
+ // If the former `value` is `null` then it means that an initial value
+ // could be being rendered on screen. If that is the case then there is
+ // no point in updating the value incase it matches. In other words if the
+ // new value is the exact same as the previously rendered value (which
+ // happens to be the initial value) then do nothing.
+ if (value != null || hasValueChanged(flag, initialValue, newValue)) {
setDirty(context, ctxIndex, true);
dirty = true;
}
@@ -294,7 +586,9 @@ export function updateStylingMap(
if (hasValueChanged(flagToCompare, valueToCompare, newValue)) {
const initialValue = getInitialValue(context, flagToCompare);
setValue(context, ctxIndex, newValue);
- if (hasValueChanged(flagToCompare, initialValue, newValue)) {
+
+ // same if statement logic as above (look for SKIP IF INITIAL CHECK).
+ if (valueToCompare != null || hasValueChanged(flagToCompare, initialValue, newValue)) {
setDirty(context, ctxIndex, true);
playerBuildersAreDirty = playerBuildersAreDirty || !!playerBuilderIndex;
dirty = true;
@@ -302,10 +596,12 @@ export function updateStylingMap(
}
} else {
// we only care to do this if the insertion is in the middle
- const newFlag = prepareInitialFlag(newProp, isClassBased, getStyleSanitizer(context));
+ const newFlag = prepareInitialFlag(
+ context, newProp, isClassBased, getStyleSanitizer(context, directiveIndex));
playerBuildersAreDirty = playerBuildersAreDirty || !!playerBuilderIndex;
insertNewMultiProperty(
- context, ctxIndex, isClassBased, newProp, newFlag, newValue, playerBuilderIndex);
+ context, ctxIndex, isClassBased, newProp, newFlag, newValue, directiveIndex,
+ playerBuilderIndex);
dirty = true;
}
}
@@ -335,7 +631,7 @@ export function updateStylingMap(
// is a valid animation player instruction.
const playerBuilderIndex =
isClassBased ? classesPlayerBuilderIndex : stylesPlayerBuilderIndex;
- setPlayerBuilderIndex(context, ctxIndex, playerBuilderIndex);
+ setPlayerBuilderIndex(context, ctxIndex, playerBuilderIndex, directiveIndex);
dirty = true;
}
}
@@ -345,7 +641,7 @@ export function updateStylingMap(
// this means that there are left-over properties in the context that
// were not detected in the context during the loop above. In that
// case we want to add the new entries into the list
- const sanitizer = getStyleSanitizer(context);
+ const sanitizer = getStyleSanitizer(context, directiveIndex);
while (propIndex < propLimit) {
const isClassBased = propIndex >= classesStartIndex;
const processValue =
@@ -355,10 +651,12 @@ export function updateStylingMap(
const prop = isClassBased ? classNames[adjustedPropIndex] : styleProps[adjustedPropIndex];
const value: string|boolean =
isClassBased ? (applyAllClasses ? true : classes[prop]) : styles[prop];
- const flag = prepareInitialFlag(prop, isClassBased, sanitizer) | StylingFlags.Dirty;
+ const flag = prepareInitialFlag(context, prop, isClassBased, sanitizer) | StylingFlags.Dirty;
const playerBuilderIndex =
isClassBased ? classesPlayerBuilderIndex : stylesPlayerBuilderIndex;
- context.push(flag, prop, value, playerBuilderIndex);
+ const ctxIndex = context.length;
+ context.push(flag, prop, value, 0);
+ setPlayerBuilderIndex(context, ctxIndex, playerBuilderIndex, directiveIndex);
dirty = true;
}
propIndex++;
@@ -366,6 +664,7 @@ export function updateStylingMap(
if (dirty) {
setContextDirty(context, true);
+ setDirectiveDirty(context, directiveIndex, true);
}
if (playerBuildersAreDirty) {
@@ -374,8 +673,23 @@ export function updateStylingMap(
}
/**
- * Sets and resolves a single styling property/value on the provided `StylingContext` so
- * that they can be applied to the element once `renderStyleAndClassBindings` is called.
+ * This method will toggle the referenced CSS class (by the provided index)
+ * within the given context.
+ *
+ * @param context The styling context that will be updated with the
+ * newly provided class value.
+ * @param offset The index of the CSS class which is being updated.
+ * @param addOrRemove Whether or not to add or remove the CSS class
+ */
+export function updateClassProp(
+ context: StylingContext, offset: number, addOrRemove: boolean | BoundPlayerFactory,
+ directiveRef?: any): void {
+ _updateSingleStylingValue(context, offset, addOrRemove, true, directiveRef);
+}
+
+/**
+ * Sets and resolves a single style value on the provided `StylingContext` so
+ * that they can be applied to the element once `renderStyling` is called.
*
* Note that prop-level styling values are considered higher priority than any styling that
* has been applied using `updateStylingMap`, therefore, when styling values are rendered
@@ -384,19 +698,33 @@ export function updateStylingMap(
*
* @param context The styling context that will be updated with the
* newly provided style value.
- * @param index The index of the property which is being updated.
+ * @param offset The index of the property which is being updated.
* @param value The CSS style value that will be assigned
+ * @param directiveRef an optional reference to the directive responsible
+ * for this binding change. If present then style binding will only
+ * actualize if the directive has ownership over this binding
+ * (see styling.ts#directives for more information about the algorithm).
*/
export function updateStyleProp(
- context: StylingContext, index: number,
- input: string | boolean | null | BoundPlayerFactory): void {
- const singleIndex = StylingIndex.SingleStylesStartPosition + index * StylingIndex.Size;
+ context: StylingContext, offset: number,
+ input: string | boolean | null | BoundPlayerFactory,
+ directiveRef?: any): void {
+ _updateSingleStylingValue(context, offset, input, false, directiveRef);
+}
+
+function _updateSingleStylingValue(
+ context: StylingContext, offset: number,
+ input: string | boolean | null | BoundPlayerFactory, isClassBased: boolean,
+ directiveRef: any): void {
+ const directiveIndex = getDirectiveIndexFromRegistry(context, directiveRef || null);
+ const singleIndex = getSinglePropIndexValue(context, directiveIndex, offset, isClassBased);
const currValue = getValue(context, singleIndex);
const currFlag = getPointers(context, singleIndex);
+ const currDirective = getDirectiveIndexFromEntry(context, singleIndex);
const value: string|boolean|null = (input instanceof BoundPlayerFactory) ? input.value : input;
- // didn't change ... nothing to make a note of
- if (hasValueChanged(currFlag, currValue, value)) {
+ if (hasValueChanged(currFlag, currValue, value) &&
+ allowValueChange(currValue, value, currDirective, directiveIndex)) {
const isClassBased = (currFlag & StylingFlags.Class) === StylingFlags.Class;
const element = context[StylingIndex.ElementPosition] !as HTMLElement;
const playerBuilder = input instanceof BoundPlayerFactory ?
@@ -412,10 +740,19 @@ export function updateStyleProp(
if (hasPlayerBuilderChanged(context, playerBuilder, currPlayerIndex)) {
const newIndex = setPlayerBuilder(context, playerBuilder, currPlayerIndex);
playerBuilderIndex = playerBuilder ? newIndex : 0;
- setPlayerBuilderIndex(context, singleIndex, playerBuilderIndex);
playerBuildersAreDirty = true;
}
+ if (playerBuildersAreDirty || currDirective !== directiveIndex) {
+ setPlayerBuilderIndex(context, singleIndex, playerBuilderIndex, directiveIndex);
+ }
+
+ if (currDirective !== directiveIndex) {
+ const prop = getProp(context, singleIndex);
+ const sanitizer = getStyleSanitizer(context, directiveIndex);
+ setSanitizeFlag(context, singleIndex, (sanitizer && sanitizer(prop)) ? true : false);
+ }
+
// the value will always get updated (even if the dirty flag is skipped)
setValue(context, singleIndex, value);
const indexForMulti = getMultiOrSingleIndex(currFlag);
@@ -434,6 +771,7 @@ export function updateStyleProp(
setDirty(context, indexForMulti, multiDirty);
setDirty(context, singleIndex, singleDirty);
+ setDirectiveDirty(context, directiveIndex, true);
setContextDirty(context, true);
}
@@ -443,21 +781,6 @@ export function updateStyleProp(
}
}
-/**
- * This method will toggle the referenced CSS class (by the provided index)
- * within the given context.
- *
- * @param context The styling context that will be updated with the
- * newly provided class value.
- * @param index The index of the CSS class which is being updated.
- * @param addOrRemove Whether or not to add or remove the CSS class
- */
-export function updateClassProp(
- context: StylingContext, index: number,
- addOrRemove: boolean | BoundPlayerFactory): void {
- const adjustedIndex = index + context[StylingIndex.ClassOffsetPosition];
- updateStyleProp(context, adjustedIndex, addOrRemove);
-}
/**
* Renders all queued styling using a renderer onto the given element.
@@ -476,29 +799,41 @@ export function updateClassProp(
* to this key/value map instead of being renderered via the renderer.
* @param stylesStore if provided, the updated style values will be applied
* to this key/value map instead of being renderered via the renderer.
+ * @param directiveRef an optional directive that will be used to target which
+ * styling values are rendered. If left empty, only the bindings that are
+ * registered on the template will be rendered.
* @returns number the total amount of players that got queued for animation (if any)
*/
-export function renderStyleAndClassBindings(
+export function renderStyling(
context: StylingContext, renderer: Renderer3, rootOrView: RootContext | LView,
- isFirstRender: boolean, classesStore?: BindingStore | null,
- stylesStore?: BindingStore | null): number {
+ isFirstRender: boolean, classesStore?: BindingStore | null, stylesStore?: BindingStore | null,
+ directiveRef?: any): number {
let totalPlayersQueued = 0;
+ const targetDirectiveIndex = getDirectiveIndexFromRegistry(context, directiveRef || null);
- if (isContextDirty(context)) {
+ if (isContextDirty(context) && isDirectiveDirty(context, targetDirectiveIndex)) {
const flushPlayerBuilders: any =
context[StylingIndex.MasterFlagPosition] & StylingFlags.PlayerBuildersDirty;
const native = context[StylingIndex.ElementPosition] !;
const multiStartIndex = getMultiStartIndex(context);
- const styleSanitizer = getStyleSanitizer(context);
const onlySingleClasses = limitToSingleClasses(context);
+ let stillDirty = false;
for (let i = StylingIndex.SingleStylesStartPosition; i < context.length;
i += StylingIndex.Size) {
// there is no point in rendering styles that have not changed on screen
if (isDirty(context, i)) {
+ const flag = getPointers(context, i);
+ const directiveIndex = getDirectiveIndexFromEntry(context, i);
+ if (targetDirectiveIndex !== directiveIndex) {
+ stillDirty = true;
+ continue;
+ }
+
const prop = getProp(context, i);
const value = getValue(context, i);
- const flag = getPointers(context, i);
+ const styleSanitizer =
+ (flag & StylingFlags.Sanitize) ? getStyleSanitizer(context, directiveIndex) : null;
const playerBuilder = getPlayerBuilder(context, i);
const isClassBased = flag & StylingFlags.Class ? true : false;
const isInSingleRegion = i < multiStartIndex;
@@ -521,7 +856,9 @@ export function renderStyleAndClassBindings(
// note that this should always be a falsy check since `false` is used
// for both class and style comparisons (styles can't be false and false
// classes are turned off and should therefore defer to their initial values)
- if (!valueExists(valueToApply, isClassBased) && readInitialValue) {
+ // Note that we ignore class-based deferals because otherwise a class can never
+ // be removed in the case that it exists as true in the initial classes list...
+ if (!isClassBased && !valueExists(valueToApply, isClassBased) && readInitialValue) {
valueToApply = getInitialValue(context, flag);
}
@@ -535,9 +872,8 @@ export function renderStyleAndClassBindings(
setClass(
native, prop, valueToApply ? true : false, renderer, classesStore, playerBuilder);
} else {
- const sanitizer = (flag & StylingFlags.Sanitize) ? styleSanitizer : null;
setStyle(
- native, prop, valueToApply as string | null, renderer, sanitizer, stylesStore,
+ native, prop, valueToApply as string | null, renderer, styleSanitizer, stylesStore,
playerBuilder);
}
}
@@ -576,7 +912,9 @@ export function renderStyleAndClassBindings(
}
setContextPlayersDirty(context, false);
}
- setContextDirty(context, false);
+
+ setDirectiveDirty(context, targetDirectiveIndex, false);
+ setContextDirty(context, stillDirty);
}
return totalPlayersQueued;
@@ -607,6 +945,8 @@ export function setStyle(
playerBuilder.setValue(prop, value);
}
} else if (value) {
+ value = value.toString(); // opacity, z-index and flexbox all have number values which may not
+ // assign as numbers
ngDevMode && ngDevMode.rendererSetStyle++;
isProceduralRenderer(renderer) ?
renderer.setStyle(native, prop, value, RendererStyleFlags3.DashCase) :
@@ -652,6 +992,14 @@ function setClass(
}
}
+function setSanitizeFlag(context: StylingContext, index: number, sanitizeYes: boolean) {
+ if (sanitizeYes) {
+ (context[index] as number) |= StylingFlags.Sanitize;
+ } else {
+ (context[index] as number) &= ~StylingFlags.Sanitize;
+ }
+}
+
function setDirty(context: StylingContext, index: number, isDirtyYes: boolean) {
const adjustedIndex =
index >= StylingIndex.SingleStylesStartPosition ? (index + StylingIndex.FlagsOffset) : index;
@@ -668,7 +1016,7 @@ function isDirty(context: StylingContext, index: number): boolean {
return ((context[adjustedIndex] as number) & StylingFlags.Dirty) == StylingFlags.Dirty;
}
-export function isClassBased(context: StylingContext, index: number): boolean {
+export function isClassBasedValue(context: StylingContext, index: number): boolean {
const adjustedIndex =
index >= StylingIndex.SingleStylesStartPosition ? (index + StylingIndex.FlagsOffset) : index;
return ((context[adjustedIndex] as number) & StylingFlags.Class) == StylingFlags.Class;
@@ -685,9 +1033,12 @@ function pointers(configFlag: number, staticIndex: number, dynamicIndex: number)
(dynamicIndex << (StylingIndex.BitCountSize + StylingFlags.BitCountSize));
}
-function getInitialValue(context: StylingContext, flag: number): string|null {
+function getInitialValue(context: StylingContext, flag: number): string|boolean|null {
const index = getInitialIndex(flag);
- return context[StylingIndex.InitialStylesPosition][index] as null | string;
+ const entryIsClassBased = flag & StylingFlags.Class;
+ const initialValues = entryIsClassBased ? context[StylingIndex.InitialClassValuesPosition] :
+ context[StylingIndex.InitialStyleValuesPosition];
+ return initialValues[index];
}
function getInitialIndex(flag: number): number {
@@ -704,10 +1055,6 @@ function getMultiStartIndex(context: StylingContext): number {
return getMultiOrSingleIndex(context[StylingIndex.MasterFlagPosition]) as number;
}
-function getStyleSanitizer(context: StylingContext): StyleSanitizeFn|null {
- return context[StylingIndex.StyleSanitizerPosition];
-}
-
function setProp(context: StylingContext, index: number, prop: string) {
context[index + StylingIndex.PropertyOffset] = prop;
}
@@ -744,12 +1091,21 @@ function setPlayerBuilder(
return insertionIndex;
}
-function setPlayerBuilderIndex(context: StylingContext, index: number, playerBuilderIndex: number) {
- context[index + StylingIndex.PlayerBuilderIndexOffset] = playerBuilderIndex;
+export function directiveOwnerPointers(directiveIndex: number, playerIndex: number) {
+ return (playerIndex << DirectiveOwnerAndPlayerBuilderIndex.BitCountSize) | directiveIndex;
+}
+
+function setPlayerBuilderIndex(
+ context: StylingContext, index: number, playerBuilderIndex: number, directiveIndex: number) {
+ const value = directiveOwnerPointers(directiveIndex, playerBuilderIndex);
+ context[index + StylingIndex.PlayerBuilderIndexOffset] = value;
}
function getPlayerBuilderIndex(context: StylingContext, index: number): number {
- return (context[index + StylingIndex.PlayerBuilderIndexOffset] as number) || 0;
+ const flag = context[index + StylingIndex.PlayerBuilderIndexOffset] as number;
+ const playerBuilderIndex = (flag >> DirectiveOwnerAndPlayerBuilderIndex.BitCountSize) &
+ DirectiveOwnerAndPlayerBuilderIndex.BitMask;
+ return playerBuilderIndex;
}
function getPlayerBuilder(context: StylingContext, index: number): ClassAndStylePlayerBuilder|
@@ -842,12 +1198,14 @@ function swapMultiContextEntries(context: StylingContext, indexA: number, indexB
setValue(context, indexA, getValue(context, indexB));
setProp(context, indexA, getProp(context, indexB));
setFlag(context, indexA, getPointers(context, indexB));
- setPlayerBuilderIndex(context, indexA, getPlayerBuilderIndex(context, indexB));
+ const playerIndexA = getPlayerBuilderIndex(context, indexB);
+ const directiveIndexA = 0;
+ setPlayerBuilderIndex(context, indexA, playerIndexA, directiveIndexA);
setValue(context, indexB, tmpValue);
setProp(context, indexB, tmpProp);
setFlag(context, indexB, tmpFlag);
- setPlayerBuilderIndex(context, indexB, tmpPlayerBuilderIndex);
+ setPlayerBuilderIndex(context, indexB, tmpPlayerBuilderIndex, directiveIndexA);
}
function updateSinglePointerValues(context: StylingContext, indexStartPosition: number) {
@@ -858,7 +1216,7 @@ function updateSinglePointerValues(context: StylingContext, indexStartPosition:
const singleFlag = getPointers(context, singleIndex);
const initialIndexForSingle = getInitialIndex(singleFlag);
const flagValue = (isDirty(context, singleIndex) ? StylingFlags.Dirty : StylingFlags.None) |
- (isClassBased(context, singleIndex) ? StylingFlags.Class : StylingFlags.None) |
+ (isClassBasedValue(context, singleIndex) ? StylingFlags.Class : StylingFlags.None) |
(isSanitizable(context, singleIndex) ? StylingFlags.Sanitize : StylingFlags.None);
const updatedFlag = pointers(flagValue, initialIndexForSingle, i);
setFlag(context, singleIndex, updatedFlag);
@@ -868,13 +1226,14 @@ function updateSinglePointerValues(context: StylingContext, indexStartPosition:
function insertNewMultiProperty(
context: StylingContext, index: number, classBased: boolean, name: string, flag: number,
- value: string | boolean, playerIndex: number): void {
+ value: string | boolean, directiveIndex: number, playerIndex: number): void {
const doShift = index < context.length;
// prop does not exist in the list, add it in
context.splice(
index, 0, flag | StylingFlags.Dirty | (classBased ? StylingFlags.Class : StylingFlags.None),
- name, value, playerIndex);
+ name, value, 0);
+ setPlayerBuilderIndex(context, index, playerIndex, directiveIndex);
if (doShift) {
// because the value was inserted midway into the array then we
@@ -892,13 +1251,22 @@ function valueExists(value: string | null | boolean, isClassBased?: boolean) {
}
function prepareInitialFlag(
- name: string, isClassBased: boolean, sanitizer?: StyleSanitizeFn | null) {
- if (isClassBased) {
- return StylingFlags.Class;
- } else if (sanitizer && sanitizer(name)) {
- return StylingFlags.Sanitize;
+ context: StylingContext, prop: string, entryIsClassBased: boolean,
+ sanitizer?: StyleSanitizeFn | null) {
+ let flag = (sanitizer && sanitizer(prop)) ? StylingFlags.Sanitize : StylingFlags.None;
+
+ let initialIndex: number;
+ if (entryIsClassBased) {
+ flag |= StylingFlags.Class;
+ initialIndex =
+ getInitialStylingValuesIndexOf(context[StylingIndex.InitialClassValuesPosition], prop);
+ } else {
+ initialIndex =
+ getInitialStylingValuesIndexOf(context[StylingIndex.InitialStyleValuesPosition], prop);
}
- return StylingFlags.None;
+
+ initialIndex = initialIndex > 0 ? (initialIndex + InitialStylingValuesIndex.ValueOffset) : 0;
+ return pointers(flag, initialIndex, 0);
}
function hasValueChanged(
@@ -949,3 +1317,223 @@ export class ClassAndStylePlayerBuilder implements PlayerBuilder {
return undefined;
}
}
+
+/**
+ * Used to provide a summary of the state of the styling context.
+ *
+ * This is an internal interface that is only used inside of test tooling to
+ * help summarize what's going on within the styling context. None of this code
+ * is designed to be exported publicly and will, therefore, be tree-shaken away
+ * during runtime.
+ */
+export interface LogSummary {
+ name: string; //
+ staticIndex: number; //
+ dynamicIndex: number; //
+ value: number; //
+ flags: {
+ dirty: boolean; //
+ class: boolean; //
+ sanitize: boolean; //
+ playerBuildersDirty: boolean; //
+ onlyProcessSingleClasses: boolean; //
+ bindingAllocationLocked: boolean; //
+ };
+}
+
+/**
+ * This function is not designed to be used in production.
+ * It is a utility tool for debugging and testing and it
+ * will automatically be tree-shaken away during production.
+ */
+export function generateConfigSummary(source: number): LogSummary;
+export function generateConfigSummary(source: StylingContext): LogSummary;
+export function generateConfigSummary(source: StylingContext, index: number): LogSummary;
+export function generateConfigSummary(source: number | StylingContext, index?: number): LogSummary {
+ let flag, name = 'config value for ';
+ if (Array.isArray(source)) {
+ if (index) {
+ name += 'index: ' + index;
+ } else {
+ name += 'master config';
+ }
+ index = index || StylingIndex.MasterFlagPosition;
+ flag = source[index] as number;
+ } else {
+ flag = source;
+ name += 'index: ' + flag;
+ }
+ const dynamicIndex = getMultiOrSingleIndex(flag);
+ const staticIndex = getInitialIndex(flag);
+ return {
+ name,
+ staticIndex,
+ dynamicIndex,
+ value: flag,
+ flags: {
+ dirty: flag & StylingFlags.Dirty ? true : false,
+ class: flag & StylingFlags.Class ? true : false,
+ sanitize: flag & StylingFlags.Sanitize ? true : false,
+ playerBuildersDirty: flag & StylingFlags.PlayerBuildersDirty ? true : false,
+ onlyProcessSingleClasses: flag & StylingFlags.OnlyProcessSingleClasses ? true : false,
+ bindingAllocationLocked: flag & StylingFlags.BindingAllocationLocked ? true : false,
+ }
+ };
+}
+
+export function getDirectiveIndexFromEntry(context: StylingContext, index: number) {
+ const value = context[index + StylingIndex.PlayerBuilderIndexOffset] as number;
+ return value & DirectiveOwnerAndPlayerBuilderIndex.BitMask;
+}
+
+function getDirectiveIndexFromRegistry(context: StylingContext, directive: any) {
+ const index =
+ getDirectiveRegistryValuesIndexOf(context[StylingIndex.DirectiveRegistryPosition], directive);
+ ngDevMode &&
+ assertNotEqual(
+ index, -1,
+ `The provided directive ${directive} has not been allocated to the element\'s style/class bindings`);
+ return index > 0 ? index / DirectiveRegistryValuesIndex.Size : 0;
+ // return index / DirectiveRegistryValuesIndex.Size;
+}
+
+function getDirectiveRegistryValuesIndexOf(
+ directives: DirectiveRegistryValues, directive: {}): number {
+ for (let i = 0; i < directives.length; i += DirectiveRegistryValuesIndex.Size) {
+ if (directives[i] === directive) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+function getInitialStylingValuesIndexOf(keyValues: InitialStylingValues, key: string): number {
+ for (let i = InitialStylingValuesIndex.KeyValueStartPosition; i < keyValues.length;
+ i += InitialStylingValuesIndex.Size) {
+ if (keyValues[i] === key) return i;
+ }
+ return -1;
+}
+
+export function compareLogSummaries(a: LogSummary, b: LogSummary) {
+ const log: string[] = [];
+ const diffs: [string, any, any][] = [];
+ diffSummaryValues(diffs, 'staticIndex', 'staticIndex', a, b);
+ diffSummaryValues(diffs, 'dynamicIndex', 'dynamicIndex', a, b);
+ Object.keys(a.flags).forEach(
+ name => { diffSummaryValues(diffs, 'flags.' + name, name, a.flags, b.flags); });
+
+ if (diffs.length) {
+ log.push('Log Summaries for:');
+ log.push(' A: ' + a.name);
+ log.push(' B: ' + b.name);
+ log.push('\n Differ in the following way (A !== B):');
+ diffs.forEach(result => {
+ const [name, aVal, bVal] = result;
+ log.push(' => ' + name);
+ log.push(' => ' + aVal + ' !== ' + bVal + '\n');
+ });
+ }
+
+ return log;
+}
+
+function diffSummaryValues(result: any[], name: string, prop: string, a: any, b: any) {
+ const aVal = a[prop];
+ const bVal = b[prop];
+ if (aVal !== bVal) {
+ result.push([name, aVal, bVal]);
+ }
+}
+
+function getSinglePropIndexValue(
+ context: StylingContext, directiveIndex: number, offset: number, isClassBased: boolean) {
+ const singlePropOffsetRegistryIndex =
+ context[StylingIndex.DirectiveRegistryPosition]
+ [(directiveIndex * DirectiveRegistryValuesIndex.Size) +
+ DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset] as number;
+ const offsets = context[StylingIndex.SinglePropOffsetPositions];
+ const indexForOffset = singlePropOffsetRegistryIndex +
+ SinglePropOffsetValuesIndex.ValueStartPosition +
+ (isClassBased ?
+ offsets
+ [singlePropOffsetRegistryIndex + SinglePropOffsetValuesIndex.StylesCountPosition] :
+ 0) +
+ offset;
+ return offsets[indexForOffset];
+}
+
+function getStyleSanitizer(context: StylingContext, directiveIndex: number): StyleSanitizeFn|null {
+ const dirs = context[StylingIndex.DirectiveRegistryPosition];
+ const value = dirs
+ [directiveIndex * DirectiveRegistryValuesIndex.Size +
+ DirectiveRegistryValuesIndex.StyleSanitizerOffset] ||
+ dirs[DirectiveRegistryValuesIndex.StyleSanitizerOffset] || null;
+ return value as StyleSanitizeFn | null;
+}
+
+function isDirectiveDirty(context: StylingContext, directiveIndex: number): boolean {
+ const dirs = context[StylingIndex.DirectiveRegistryPosition];
+ return dirs
+ [directiveIndex * DirectiveRegistryValuesIndex.Size +
+ DirectiveRegistryValuesIndex.DirtyFlagOffset] as boolean;
+}
+
+function setDirectiveDirty(
+ context: StylingContext, directiveIndex: number, dirtyYes: boolean): void {
+ const dirs = context[StylingIndex.DirectiveRegistryPosition];
+ dirs
+ [directiveIndex * DirectiveRegistryValuesIndex.Size +
+ DirectiveRegistryValuesIndex.DirtyFlagOffset] = dirtyYes;
+}
+
+function allowValueChange(
+ currentValue: string | boolean | null, newValue: string | boolean | null,
+ currentDirectiveOwner: number, newDirectiveOwner: number) {
+ // the code below relies the importance of directive's being tied to their
+ // index value. The index values for each directive are derived from being
+ // registered into the styling context directive registry. The most important
+ // directive is the parent component directive (the template) and each directive
+ // that is added after is considered less important than the previous entry. This
+ // prioritization of directives enables the styling algorithm to decide if a style
+ // or class should be allowed to be updated/replaced incase an earlier directive
+ // already wrote to the exact same style-property or className value. In other words
+ // ... this decides what to do if and when there is a collision.
+ if (currentValue) {
+ if (newValue) {
+ // if a directive index is lower than it always has priority over the
+ // previous directive's value...
+ return newDirectiveOwner <= currentDirectiveOwner;
+ } else {
+ // only write a null value incase it's the same owner writing it.
+ // this avoids having a higher-priority directive write to null
+ // only to have a lesser-priority directive change right to a
+ // non-null value immediately afterwards.
+ return currentDirectiveOwner === newDirectiveOwner;
+ }
+ }
+ return true;
+}
+
+/**
+ * This function is only designed to be called for `[class]` bindings when
+ * `[ngClass]` (or something that uses `class` as an input) is present. Once
+ * directive host bindings fully work for `[class]` and `[style]` inputs
+ * then this can be deleted.
+ */
+export function getInitialClassNameValue(context: StylingContext): string {
+ let className = context[StylingIndex.CachedClassValueOrInitialClassString] as string;
+ if (className == null) {
+ className = '';
+ const initialClassValues = context[StylingIndex.InitialClassValuesPosition];
+ for (let i = InitialStylingValuesIndex.KeyValueStartPosition; i < initialClassValues.length;
+ i += InitialStylingValuesIndex.Size) {
+ const isPresent = initialClassValues[i + 1];
+ if (isPresent) {
+ className += (className.length ? ' ' : '') + initialClassValues[i];
+ }
+ }
+ context[StylingIndex.CachedClassValueOrInitialClassString] = className;
+ }
+ return className;
+}
diff --git a/packages/core/src/render3/styling/util.ts b/packages/core/src/render3/styling/util.ts
index 8a41dcffd5..16eb3d6e20 100644
--- a/packages/core/src/render3/styling/util.ts
+++ b/packages/core/src/render3/styling/util.ts
@@ -9,12 +9,13 @@ import '../ng_dev_mode';
import {StyleSanitizeFn} from '../../sanitization/style_sanitizer';
import {getLContext} from '../context_discovery';
-import {ACTIVE_INDEX, LContainer} from '../interfaces/container';
+import {LContainer} from '../interfaces/container';
import {LContext} from '../interfaces/context';
+import {AttributeMarker, TAttributes, TNode, TNodeFlags} from '../interfaces/node';
import {PlayState, Player, PlayerContext, PlayerIndex} from '../interfaces/player';
import {RElement} from '../interfaces/renderer';
-import {InitialStyles, StylingContext, StylingIndex} from '../interfaces/styling';
-import {FLAGS, HEADER_OFFSET, HOST, LView, RootContext} from '../interfaces/view';
+import {InitialStylingValues, StylingContext, StylingFlags, StylingIndex} from '../interfaces/styling';
+import {HEADER_OFFSET, HOST, LView, RootContext} from '../interfaces/view';
import {getTNode} from '../util';
import {CorePlayerHandler} from './core_player_handler';
@@ -23,16 +24,18 @@ const ANIMATION_PROP_PREFIX = '@';
export function createEmptyStylingContext(
element?: RElement | null, sanitizer?: StyleSanitizeFn | null,
- initialStylingValues?: InitialStyles): StylingContext {
+ initialStyles?: InitialStylingValues | null,
+ initialClasses?: InitialStylingValues | null): StylingContext {
return [
- null, // PlayerContext
- sanitizer || null, // StyleSanitizer
- initialStylingValues || [null], // InitialStyles
- 0, // MasterFlags
- 0, // ClassOffset
- element || null, // Element
- null, // PreviousMultiClassValue
- null // PreviousMultiStyleValue
+ 0, // MasterFlags
+ [null, -1, false, sanitizer || null], // DirectiveRefs
+ initialStyles || [null], // InitialStyles
+ initialClasses || [null], // InitialClasses
+ [0, 0], // SinglePropOffsets
+ element || null, // Element
+ null, // PreviousMultiClassValue
+ null, // PreviousMultiStyleValue
+ null, // PlayerContext
];
}
@@ -47,6 +50,9 @@ export function allocStylingContext(
// each instance gets a copy
const context = templateStyleContext.slice() as any as StylingContext;
context[StylingIndex.ElementPosition] = element;
+
+ // this will prevent any other directives from extending the context
+ context[StylingIndex.MasterFlagPosition] |= StylingFlags.BindingAllocationLocked;
return context;
}
@@ -89,8 +95,8 @@ export function getStylingContext(index: number, viewData: LView): StylingContex
export function isStylingContext(value: any): value is StylingContext {
// Not an LView or an LContainer
- return Array.isArray(value) && typeof value[FLAGS] !== 'number' &&
- typeof value[ACTIVE_INDEX] !== 'number';
+ return Array.isArray(value) && typeof value[StylingIndex.MasterFlagPosition] === 'number' &&
+ Array.isArray(value[StylingIndex.InitialStyleValuesPosition]);
}
export function isAnimationProp(name: string): boolean {
@@ -182,3 +188,15 @@ export function allocPlayerContext(data: StylingContext): PlayerContext {
export function throwInvalidRefError() {
throw new Error('Only elements that exist in an Angular application can be used for animations');
}
+
+export function hasStyling(attrs: TAttributes): boolean {
+ for (let i = 0; i < attrs.length; i++) {
+ const attr = attrs[i];
+ if (attr == AttributeMarker.Classes || attr == AttributeMarker.Styles) return true;
+ }
+ return false;
+}
+
+export function hasClassInput(tNode: TNode) {
+ return tNode.flags & TNodeFlags.hasClassInput ? true : false;
+}
diff --git a/packages/core/src/render3/util.ts b/packages/core/src/render3/util.ts
index 9cdd42d69b..c8c2d3132d 100644
--- a/packages/core/src/render3/util.ts
+++ b/packages/core/src/render3/util.ts
@@ -9,7 +9,7 @@
import {global} from '../util';
import {assertDataInRange, assertDefined, assertGreaterThan, assertLessThan} from './assert';
-import {ACTIVE_INDEX, LContainer} from './interfaces/container';
+import {ACTIVE_INDEX, LCONTAINER_LENGTH, LContainer} from './interfaces/container';
import {LContext, MONKEY_PATCH_KEY_NAME} from './interfaces/context';
import {ComponentDef, DirectiveDef} from './interfaces/definition';
import {NO_PARENT_INJECTOR, RelativeInjectorLocation, RelativeInjectorLocationFlags} from './interfaces/injector';
@@ -18,8 +18,6 @@ import {RComment, RElement, RText} from './interfaces/renderer';
import {StylingContext} from './interfaces/styling';
import {CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, LView, LViewFlags, PARENT, RootContext, TData, TVIEW, TView} from './interfaces/view';
-
-
/**
* Returns whether the values are different from a change detection stand point.
*
@@ -127,7 +125,7 @@ export function isComponentDef(def: DirectiveDef): def is ComponentDef
export function isLContainer(value: RElement | RComment | LContainer | StylingContext): boolean {
// Styling contexts are also arrays, but their first index contains an element node
- return Array.isArray(value) && typeof value[ACTIVE_INDEX] === 'number';
+ return Array.isArray(value) && value.length === LCONTAINER_LENGTH;
}
export function isRootView(target: LView): boolean {
diff --git a/packages/core/test/bundling/animation_world/index.ts b/packages/core/test/bundling/animation_world/index.ts
index 1d4cca61cb..001c8b3ea7 100644
--- a/packages/core/test/bundling/animation_world/index.ts
+++ b/packages/core/test/bundling/animation_world/index.ts
@@ -9,7 +9,29 @@
import '@angular/core/test/bundling/util/src/reflect_metadata';
import {CommonModule} from '@angular/common';
-import {Component, ElementRef, NgModule, ɵPlayState as PlayState, ɵPlayer as Player, ɵPlayerHandler as PlayerHandler, ɵaddPlayer as addPlayer, ɵbindPlayerFactory as bindPlayerFactory, ɵmarkDirty as markDirty, ɵrenderComponent as renderComponent} from '@angular/core';
+import {Component, Directive, ElementRef, HostBinding, NgModule, ɵPlayState as PlayState, ɵPlayer as Player, ɵPlayerHandler as PlayerHandler, ɵaddPlayer as addPlayer, ɵbindPlayerFactory as bindPlayerFactory, ɵmarkDirty as markDirty, ɵrenderComponent as renderComponent} from '@angular/core';
+
+@Directive({
+ selector: '[make-color-grey]',
+ exportAs: 'makeColorGrey',
+ host: {'style': 'font-family: Times New Roman;'}
+})
+class MakeColorGreyDirective {
+ @HostBinding('style.background-color') private _backgroundColor: string|null = null;
+ @HostBinding('style.color') private _textColor: string|null = null;
+
+ on() {
+ this._backgroundColor = 'grey';
+ this._textColor = 'black';
+ }
+
+ off() {
+ this._backgroundColor = null;
+ this._textColor = null;
+ }
+
+ toggle() { this._backgroundColor ? this.off() : this.on(); }
+}
@Component({
selector: 'animation-world',
@@ -20,21 +42,40 @@ import {Component, ElementRef, NgModule, ɵPlayState as PlayState, ɵPlayer as P