diff --git a/packages/core/src/render3/instructions/lview_debug.ts b/packages/core/src/render3/instructions/lview_debug.ts index 6490db105e..296767fe8a 100644 --- a/packages/core/src/render3/instructions/lview_debug.ts +++ b/packages/core/src/render3/instructions/lview_debug.ts @@ -18,7 +18,7 @@ import {PropertyAliases, TConstants, TContainerNode, TElementNode, TNode as ITNo import {SelectorFlags} from '../interfaces/projection'; import {TQueries} from '../interfaces/query'; import {RComment, RElement, RNode} from '../interfaces/renderer'; -import {TStylingContext} from '../interfaces/styling'; +import {TStylingContext, TStylingRange} from '../interfaces/styling'; import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_VIEW, ExpandoInstructions, FLAGS, HEADER_OFFSET, HOST, HookData, INJECTOR, LView, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, SANITIZER, TData, TVIEW, TView as ITView, TView, TViewType, T_HOST} from '../interfaces/view'; import {DebugNodeStyling, NodeStylingDebug} from '../styling/styling_debug'; import {attachDebugObject} from '../util/debug_utils'; @@ -177,6 +177,8 @@ export const TNodeConstructor = class TNode implements ITNode { public projection: number|(ITNode|RNode[])[]|null, // public styles: TStylingContext|null, // public classes: TStylingContext|null, // + public classBindings: TStylingRange, // + public styleBindings: TStylingRange, // ) {} get type_(): string { diff --git a/packages/core/src/render3/instructions/shared.ts b/packages/core/src/render3/instructions/shared.ts index 2f1e1cfb5e..e86d9138d4 100644 --- a/packages/core/src/render3/instructions/shared.ts +++ b/packages/core/src/render3/instructions/shared.ts @@ -815,6 +815,8 @@ export function createTNode( null, // projection: number|(ITNode|RNode[])[]|null null, // styles: TStylingContext|null null, // classes: TStylingContext|null + 0 as any, // classBindings: TStylingRange; + 0 as any, // styleBindings: TStylingRange; ) : { type: type, @@ -839,6 +841,8 @@ export function createTNode( projection: null, styles: null, classes: null, + classBindings: 0 as any, + styleBindings: 0 as any, }; } diff --git a/packages/core/src/render3/interfaces/node.ts b/packages/core/src/render3/interfaces/node.ts index bf52d5a1ec..4151d324b6 100644 --- a/packages/core/src/render3/interfaces/node.ts +++ b/packages/core/src/render3/interfaces/node.ts @@ -5,7 +5,7 @@ * 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 {StylingMapArray, TStylingContext} from '../interfaces/styling'; +import {StylingMapArray, TStylingContext, TStylingRange} from '../interfaces/styling'; import {CssSelector} from './projection'; import {RNode} from './renderer'; import {LView, TView} from './view'; @@ -599,6 +599,36 @@ export interface TNode { * will be placed into the initial styling slot in the newly created `TStylingContext`. */ classes: StylingMapArray|TStylingContext|null; + + /** + * Stores the head/tail index of the class bindings. + * + * - If no bindings, the head and tail will both be 0. + * - If there are template bindings, stores the head/tail of the class bindings in the template. + * - If no template bindings but there are host bindings, the head value will point to the last + * host binding for "class" (not the head of the linked list), tail will be 0. + * + * See: `style_binding_list.ts` for details. + * + * This is used by `insertTStylingBinding` to know where the next styling binding should be + * inserted so that they can be sorted in priority order. + */ + classBindings: TStylingRange; + + /** + * Stores the head/tail index of the class bindings. + * + * - If no bindings, the head and tail will both be 0. + * - If there are template bindings, stores the head/tail of the style bindings in the template. + * - If no template bindings but there are host bindings, the head value will point to the last + * host binding for "style" (not the head of the linked list), tail will be 0. + * + * See: `style_binding_list.ts` for details. + * + * This is used by `insertTStylingBinding` to know where the next styling binding should be + * inserted so that they can be sorted in priority order. + */ + styleBindings: TStylingRange; } /** Static data for an element */ diff --git a/packages/core/src/render3/interfaces/styling.ts b/packages/core/src/render3/interfaces/styling.ts index 04a88399ed..cba639d879 100644 --- a/packages/core/src/render3/interfaces/styling.ts +++ b/packages/core/src/render3/interfaces/styling.ts @@ -9,6 +9,7 @@ import {StyleSanitizeFn} from '../../sanitization/style_sanitizer'; import {TNodeFlags} from './node'; import {ProceduralRenderer3, RElement, Renderer3} from './renderer'; +import {SanitizerFn} from './sanitization'; import {LView} from './view'; @@ -471,3 +472,173 @@ export const enum StylingMapsSyncMode { * The styling algorithm code only needs access to `flags`. */ export interface TStylingNode { flags: TNodeFlags; } + + +/** + * Value stored in the `TData` which is needed to re-concatenate the styling. + * + * - `string`: Stores the property name. Used with `ɵɵstyleProp`/`ɵɵclassProp` instruction which + * don't have suffix or don't need sanitization. + */ +export type TStylingKey = string | TStylingSuffixKey | TStylingSanitizationKey | TStylingMapKey; + + +/** + * For performance reasons we want to make sure that all subclasses have the same shape object. + * + * See subclasses for implementation details. + */ +export interface TStylingKeyShape { + key: string|null; + extra: string|SanitizerFn|TStylingMapFn; +} + +/** + * Used in the case of `ɵɵstyleProp('width', exp, 'px')`. + */ +export interface TStylingSuffixKey extends TStylingKeyShape { + /// Stores the property key. + key: string; + /// Stores the property suffix. + extra: string; +} + +/** + * Used in the case of `ɵɵstyleProp('url', exp, styleSanitizationFn)`. + */ +export interface TStylingSanitizationKey extends TStylingKeyShape { + /// Stores the property key. + key: string; + /// Stores sanitization function. + extra: SanitizerFn; +} + +/** + * Used in the case of `ɵɵstyleMap()`/`ɵɵclassMap()`. + */ +export interface TStylingMapKey extends TStylingKeyShape { + /// There is no key + key: null; + /// Invoke this function to process the value (convert it into the result) + /// This is implemented this way so that the logic associated with `ɵɵstyleMap()`/`ɵɵclassMap()` + /// can be tree shaken away. Internally the function will break the `Map`/`Array` down into + /// parts and call `appendStyling` on parts. + /// + /// See: `CLASS_MAP_STYLING_KEY` and `STYLE_MAP_STYLING_KEY` for details. + extra: TStylingMapFn; +} + +/** + * Invoke this function to process the styling value which is non-primitive (Map/Array) + * This is implemented this way so that the logic associated with `ɵɵstyleMap()`/`ɵɵclassMap()` + * can be tree shaken away. Internally the function will break the `Map`/`Array` down into + * parts and call `appendStyling` on parts. + * + * See: `CLASS_MAP_STYLING_KEY` and `STYLE_MAP_STYLING_KEY` for details. + */ +export type TStylingMapFn = (text: string, value: any, hasPreviousDuplicate: boolean) => string; + +/** + * This is a branded number which contains previous and next index. + * + * When we come across styling instructions we need to store the `TStylingKey` in the correct + * order so that we can re-concatenate the styling value in the desired priority. + * + * The insertion can happen either at the: + * - end of template as in the case of coming across additional styling instruction in the template + * - in front of the template in the case of coming across additional instruction in the + * `hostBindings`. + * + * We use `TStylingRange` to store the previous and next index into the `TData` where the template + * bindings can be found. + * + * - bit 0 is used to mark that the previous index has a duplicate for current value. + * - bit 1 is used to mark that the next index has a duplicate for the current value. + * - bits 2-16 are used to encode the next/tail of the template. + * - bits 17-32 are used to encode the previous/head of template. + * + * NODE: *duplicate* false implies that it is statically known that this binding will not collide + * with other bindings and therefore there is no need to check other bindings. For example the + * bindings in `
` will never collide and will have + * their bits set accordingly. Previous duplicate means that we may need to check previous if the + * current binding is `null`. Next duplicate means that we may need to check next bindings if the + * current binding is not `null`. + * + * NOTE: `0` has special significance and represents `null` as in no additional pointer. + */ +export interface TStylingRange { __brand__: 'TStylingRange'; } + +/** + * Shift and masks constants for encoding two numbers into and duplicate info into a single number. + */ +export const enum StylingRange { + /// Number of bits to shift for the previous pointer + PREV_SHIFT = 18, + /// Previous pointer mask. + PREV_MASK = 0xFFFC0000, + + /// Number of bits to shift for the next pointer + NEXT_SHIFT = 2, + /// Next pointer mask. + NEXT_MASK = 0x0003FFC, + + /** + * This bit is set if the previous bindings contains a binding which could possibly cause a + * duplicate. For example: `
`, the `width` binding will + * have previous duplicate set. The implication is that if `width` binding becomes `null`, it is + * necessary to defer the value to `map.width`. (Because `width` overwrites `map.width`.) + */ + PREV_DUPLICATE = 0x02, + + /** + * This bit is set to if the next binding contains a binding which could possibly cause a + * duplicate. For example: `
`, the `map` binding will + * have next duplicate set. The implication is that if `map.width` binding becomes not `null`, it + * is necessary to defer the value to `width`. (Because `width` overwrites `map.width`.) + */ + NEXT_DUPLICATE = 0x01, +} + + +export function toTStylingRange(prev: number, next: number): TStylingRange { + return (prev << StylingRange.PREV_SHIFT | next << StylingRange.NEXT_SHIFT) as any; +} + +export function getTStylingRangePrev(tStylingRange: TStylingRange): number { + return (tStylingRange as any as number) >> StylingRange.PREV_SHIFT; +} + +export function getTStylingRangePrevDuplicate(tStylingRange: TStylingRange): boolean { + return ((tStylingRange as any as number) & StylingRange.PREV_DUPLICATE) == + StylingRange.PREV_DUPLICATE; +} + +export function setTStylingRangePrev( + tStylingRange: TStylingRange, previous: number): TStylingRange { + return ( + ((tStylingRange as any as number) & ~StylingRange.PREV_MASK) | + (previous << StylingRange.PREV_SHIFT)) as any; +} + +export function setTStylingRangePrevDuplicate(tStylingRange: TStylingRange): TStylingRange { + return ((tStylingRange as any as number) | StylingRange.PREV_DUPLICATE) as any; +} + +export function getTStylingRangeNext(tStylingRange: TStylingRange): number { + return ((tStylingRange as any as number) & StylingRange.NEXT_MASK) >> StylingRange.NEXT_SHIFT; +} + +export function setTStylingRangeNext(tStylingRange: TStylingRange, next: number): TStylingRange { + return ( + ((tStylingRange as any as number) & ~StylingRange.NEXT_MASK) | // + next << StylingRange.NEXT_SHIFT) as any; +} + +export function getTStylingRangeNextDuplicate(tStylingRange: TStylingRange): boolean { + return ((tStylingRange as any as number) & StylingRange.NEXT_DUPLICATE) === + StylingRange.NEXT_DUPLICATE; +} + +export function setTStylingRangeNextDuplicate(tStylingRange: TStylingRange): TStylingRange { + return ((tStylingRange as any as number) | StylingRange.NEXT_DUPLICATE) as any; +} diff --git a/packages/core/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts index c7468a3c5f..71976da4dc 100644 --- a/packages/core/src/render3/interfaces/view.ts +++ b/packages/core/src/render3/interfaces/view.ts @@ -15,10 +15,11 @@ import {Sanitizer} from '../../sanitization/sanitizer'; import {LContainer} from './container'; import {ComponentDef, ComponentTemplate, DirectiveDef, DirectiveDefList, HostBindingsFunction, PipeDef, PipeDefList, ViewQueriesFunction} from './definition'; import {I18nUpdateOpCodes, TI18n} from './i18n'; -import {TAttributes, TConstants, TElementNode, TNode, TViewNode} from './node'; +import {TConstants, TElementNode, TNode, TViewNode} from './node'; import {PlayerHandler} from './player'; import {LQueries, TQueries} from './query'; import {RElement, Renderer3, RendererFactory3} from './renderer'; +import {TStylingKey, TStylingRange} from './styling'; @@ -751,8 +752,8 @@ export type HookData = (number | (() => void))[]; * Injector bloom filters are also stored here. */ export type TData = - (TNode | PipeDef| DirectiveDef| ComponentDef| number | Type| - InjectionToken| TI18n | I18nUpdateOpCodes | null | string)[]; + (TNode | PipeDef| DirectiveDef| ComponentDef| number | TStylingRange | + TStylingKey | Type| InjectionToken| TI18n | I18nUpdateOpCodes | null | string)[]; // Note: This hack is necessary so we don't erroneously get a circular dependency // failure based on types. diff --git a/packages/core/src/render3/styling/class_differ.ts b/packages/core/src/render3/styling/class_differ.ts index 2c5cd26efa..df5d87644d 100644 --- a/packages/core/src/render3/styling/class_differ.ts +++ b/packages/core/src/render3/styling/class_differ.ts @@ -105,27 +105,71 @@ export function processClassToken( * @returns a new class-list which does not have `classToRemove` */ export function removeClass(className: string, classToRemove: string): string { + return toggleClass(className, classToRemove, false); +} + +/** + * Toggles a class in `className` string. + * + * @param className A string containing classes (whitespace separated) + * @param classToToggle A class name to remove or add to the `className` + * @param toggle Weather the resulting `className` should contain or not the `classToToggle` + * @returns a new class-list which does not have `classToRemove` + */ +export function toggleClass(className: string, classToToggle: string, toggle: boolean): string { + if (className === '') { + return toggle ? classToToggle : ''; + } let start = 0; let end = className.length; while (start < end) { - start = className.indexOf(classToRemove, start); + start = classIndexOf(className, classToToggle, start); if (start === -1) { - // we did not find anything, so just bail. + if (toggle === true) { + className = className === '' ? classToToggle : className + ' ' + classToToggle; + } break; } - const removeLength = classToRemove.length; - const hasLeadingWhiteSpace = start === 0 || className.charCodeAt(start - 1) <= CharCode.SPACE; - const hasTrailingWhiteSpace = start + removeLength === end || - className.charCodeAt(start + removeLength) <= CharCode.SPACE; - if (hasLeadingWhiteSpace && hasTrailingWhiteSpace) { + const removeLength = classToToggle.length; + if (toggle === true) { + // we found it and we should have it so just return + return className; + } else { // Cut out the class which should be removed. const endWhitespace = consumeWhitespace(className, start + removeLength, end); className = className.substring(0, start) + className.substring(endWhitespace, end); end = className.length; - } else { - // in this case we are only a substring of the actual class, move on. - start = start + removeLength; } } return className; +} + +/** + * Returns an index of `classToSearch` in `className` taking token boundaries into account. + * + * `classIndexOf('AB A', 'A', 0)` will be 3 (not 0 since `AB!==A`) + * + * @param className A string containing classes (whitespace separated) + * @param classToSearch A class name to locate + * @param startingIndex Starting location of search + * @returns an index of the located class (or -1 if not found) + */ +export function classIndexOf( + className: string, classToSearch: string, startingIndex: number): number { + let end = className.length; + while (true) { + const foundIndex = className.indexOf(classToSearch, startingIndex); + if (foundIndex === -1) return foundIndex; + if (foundIndex === 0 || className.charCodeAt(foundIndex - 1) <= CharCode.SPACE) { + // Ensure that it has leading whitespace + const removeLength = classToSearch.length; + if (foundIndex + removeLength === end || + className.charCodeAt(foundIndex + removeLength) <= CharCode.SPACE) { + // Ensure that it has trailing whitespace + return foundIndex; + } + } + // False positive, keep searching from where we left off. + startingIndex = foundIndex + 1; + } } \ No newline at end of file diff --git a/packages/core/src/render3/styling/style_binding_list.ts b/packages/core/src/render3/styling/style_binding_list.ts new file mode 100644 index 0000000000..0b43afca25 --- /dev/null +++ b/packages/core/src/render3/styling/style_binding_list.ts @@ -0,0 +1,563 @@ +/** +* @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 {stylePropNeedsSanitization, ɵɵsanitizeStyle} from '../../sanitization/sanitization'; +import {assertEqual, throwError} from '../../util/assert'; +import {TNode} from '../interfaces/node'; +import {SanitizerFn} from '../interfaces/sanitization'; +import {StylingMapArray, TStylingKey, TStylingMapKey, TStylingRange, getTStylingRangeNext, getTStylingRangePrev, getTStylingRangePrevDuplicate, setTStylingRangeNext, setTStylingRangeNextDuplicate, setTStylingRangePrev, setTStylingRangePrevDuplicate, toTStylingRange} from '../interfaces/styling'; +import {LView, TData, TVIEW} from '../interfaces/view'; +import {getLView} from '../state'; + +import {splitClassList, toggleClass} from './class_differ'; +import {StyleChangesMap, parseKeyValue, removeStyle} from './style_differ'; + + + +/** + * NOTE: The word `styling` is used interchangeably as style or class styling. + * + * This file contains code to link styling instructions together so that they can be replayed in + * priority order. The file exists because Ivy styling instruction execution order does not match + * that of the priority order. The purpose of this code is to create a linked list so that the + * instructions can be traversed in priority order when computing the styles. + * + * Assume we are dealing with the following code: + * ``` + * @Component({ + * template: ` + * ` + * }) + * class ExampleComponent { + * static ngComp = ... { + * ... + * // Compiler ensures that `ɵɵstyleProp` is after `ɵɵstyleMap` + * ɵɵstyleMap({color: '#001'}); + * ɵɵstyleProp('color', '#002'); + * ... + * } + * } + * + * @Directive({ + * selector: `[dir-style-color-1]', + * }) + * class Style1Directive { + * @HostBinding('style') style = {color: '#005'}; + * @HostBinding('style.color') color = '#006'; + * + * static ngDir = ... { + * ... + * // Compiler ensures that `ɵɵstyleProp` is after `ɵɵstyleMap` + * ɵɵstyleMap({color: '#005'}); + * ɵɵstyleProp('color', '#006'); + * ... + * } + * } + * + * @Directive({ + * selector: `[dir-style-color-2]', + * }) + * class Style2Directive { + * @HostBinding('style') style = {color: '#007'}; + * @HostBinding('style.color') color = '#008'; + * + * static ngDir = ... { + * ... + * // Compiler ensures that `ɵɵstyleProp` is after `ɵɵstyleMap` + * ɵɵstyleMap({color: '#007'}); + * ɵɵstyleProp('color', '#008'); + * ... + * } + * } + * + * @Directive({ + * selector: `my-cmp', + * }) + * class MyComponent { + * @HostBinding('style') style = {color: '#003'}; + * @HostBinding('style.color') color = '#004'; + * + * static ngComp = ... { + * ... + * // Compiler ensures that `ɵɵstyleProp` is after `ɵɵstyleMap` + * ɵɵstyleMap({color: '#003'}); + * ɵɵstyleProp('color', '#004'); + * ... + * } + * } + * ``` + * + * The Order of instruction execution is: + * + * NOTE: the comment binding location is for illustrative purposes only. + * + * ``` + * // Template: (ExampleComponent) + * ɵɵstyleMap({color: '#001'}); // Binding index: 10 + * ɵɵstyleProp('color', '#002'); // Binding index: 12 + * // MyComponent + * ɵɵstyleMap({color: '#003'}); // Binding index: 20 + * ɵɵstyleProp('color', '#004'); // Binding index: 22 + * // Style1Directive + * ɵɵstyleMap({color: '#005'}); // Binding index: 24 + * ɵɵstyleProp('color', '#006'); // Binding index: 26 + * // Style2Directive + * ɵɵstyleMap({color: '#007'}); // Binding index: 28 + * ɵɵstyleProp('color', '#008'); // Binding index: 30 + * ``` + * + * The correct priority order of concatenation is: + * + * ``` + * // MyComponent + * ɵɵstyleMap({color: '#003'}); // Binding index: 20 + * ɵɵstyleProp('color', '#004'); // Binding index: 22 + * // Style1Directive + * ɵɵstyleMap({color: '#005'}); // Binding index: 24 + * ɵɵstyleProp('color', '#006'); // Binding index: 26 + * // Style2Directive + * ɵɵstyleMap({color: '#007'}); // Binding index: 28 + * ɵɵstyleProp('color', '#008'); // Binding index: 30 + * // Template: (ExampleComponent) + * ɵɵstyleMap({color: '#001'}); // Binding index: 10 + * ɵɵstyleProp('color', '#002'); // Binding index: 12 + * ``` + * + * What color should be rendered? + * + * Once the items are correctly sorted in the list, the answer is simply the last item in the + * concatenation list which is `#002`. + * + * To do so we keep a linked list of all of the bindings which pertain to this element. + * Notice that the bindings are inserted in the order of execution, but the `TView.data` allows + * us to traverse them in the order of priority. + * + * |Idx|`TView.data`|`LView` | Notes + * |---|------------|-----------------|-------------- + * |...| | | + * |10 |`null` |`{color: '#001'}`| `ɵɵstyleMap('color', {color: '#001'})` + * |11 |`30 | 12` | ... | + * |12 |`color` |`'#002'` | `ɵɵstyleProp('color', '#002')` + * |13 |`10 | 0` | ... | + * |...| | | + * |20 |`null` |`{color: '#003'}`| `ɵɵstyleMap('color', {color: '#003'})` + * |21 |`0 | 22` | ... | + * |22 |`color` |`'#004'` | `ɵɵstyleProp('color', '#004')` + * |23 |`20 | 24` | ... | + * |24 |`null` |`{color: '#005'}`| `ɵɵstyleMap('color', {color: '#005'})` + * |25 |`22 | 26` | ... | + * |26 |`color` |`'#006'` | `ɵɵstyleProp('color', '#006')` + * |27 |`24 | 28` | ... | + * |28 |`null` |`{color: '#007'}`| `ɵɵstyleMap('color', {color: '#007'})` + * |29 |`26 | 30` | ... | + * |30 |`color` |`'#008'` | `ɵɵstyleProp('color', '#008')` + * |31 |`28 | 10` | ... | + * + * The above data structure allows us to re-concatenate the styling no matter which data binding + * changes. + * + * NOTE: in addition to keeping track of next/previous index the `TView.data` also stores prev/next + * duplicate bit. The duplicate bit if true says there either is a binding with the same name or + * there is a map (which may contain the name). This information is useful in knowing if other + * styles with higher priority need to be searched for overwrites. + * + * NOTE: See `should support example in 'tnode_linked_list.ts' documentation` in + * `tnode_linked_list_spec.ts` for working example. + */ + + +/** + * Insert new `tStyleValue` at `TData` and link existing style bindings such that we maintain linked + * list of styles and compute the duplicate flag. + * + * Note: this function is executed during `firstUpdatePass` only to populate the `TView.data`. + * + * The function works by keeping track of `tStylingRange` which contains two pointers pointing to + * the head/tail of the template portion of the styles. + * - if `isHost === false` (we are template) then insertion is at tail of `TStylingRange` + * - if `isHost === true` (we are host binding) then insertion is at head of `TStylingRange` + * + * @param tData The `TData` to insert into. + * @param tNode `TNode` associated with the styling element. + * @param tStylingKey See `TStylingKey`. + * @param index location of where `tStyleValue` should be stored (and linked into list.) + * @param isHostBinding `true` if the insertion is for a `hostBinding`. (insertion is in front of + * template.) + * @param isClassBinding True if the associated `tStylingKey` as a `class` styling. + * `tNode.classBindings` should be used (or `tNode.styleBindings` otherwise.) + */ +export function insertTStylingBinding( + tData: TData, tNode: TNode, tStylingKey: TStylingKey, index: number, isHostBinding: boolean, + isClassBinding: boolean): void { + ngDevMode && assertEqual( + getLView()[TVIEW].firstUpdatePass, true, + 'Should be called during \'firstUpdatePass` only.'); + let tBindings = isClassBinding ? tNode.classBindings : tNode.styleBindings; + let tmplHead = getTStylingRangePrev(tBindings); + let tmplTail = getTStylingRangeNext(tBindings); + + tData[index] = tStylingKey; + if (isHostBinding) { + // We are inserting host bindings + + // If we don't have template bindings then `tail` is 0. + const hasTemplateBindings = tmplTail !== 0; + // This is important to know because that means that the `head` can't point to the first + // template bindings (there are none.) Instead the head points to the tail of the template. + if (hasTemplateBindings) { + // template head's "prev" will point to last host binding or to 0 if no host bindings yet + const previousNode = getTStylingRangePrev(tData[tmplHead + 1] as TStylingRange); + tData[index + 1] = toTStylingRange(previousNode, tmplHead); + // if a host binding has already been registered, we need to update the next of that host + // binding to point to this one + if (previousNode !== 0) { + // We need to update the template-tail value to point to us. + tData[previousNode + 1] = + setTStylingRangeNext(tData[previousNode + 1] as TStylingRange, index); + } + // The "previous" of the template binding head should point to this host binding + tData[tmplHead + 1] = setTStylingRangePrev(tData[tmplHead + 1] as TStylingRange, index); + } else { + tData[index + 1] = toTStylingRange(tmplHead, 0); + // if a host binding has already been registered, we need to update the next of that host + // binding to point to this one + if (tmplHead !== 0) { + // We need to update the template-tail value to point to us. + tData[tmplHead + 1] = setTStylingRangeNext(tData[tmplHead + 1] as TStylingRange, index); + } + // if we don't have template, the head points to template-tail, and needs to be advanced. + tmplHead = index; + } + } else { + // We are inserting in template section. + // We need to set this binding's "previous" to the current template tail + tData[index + 1] = toTStylingRange(tmplTail, 0); + ngDevMode && assertEqual( + tmplHead !== 0 && tmplTail === 0, false, + 'Adding template bindings after hostBindings is not allowed.'); + if (tmplHead === 0) { + tmplHead = index; + } else { + // We need to update the previous value "next" to point to this binding + tData[tmplTail + 1] = setTStylingRangeNext(tData[tmplTail + 1] as TStylingRange, index); + } + tmplTail = index; + } + + // Now we need to update / compute the duplicates. + // Starting with our location search towards head (least priority) + markDuplicates(tData, tStylingKey, index, isClassBinding ? tNode.classes : tNode.styles, true); + markDuplicates(tData, tStylingKey, index, null, false); + + tBindings = toTStylingRange(tmplHead, tmplTail); + if (isClassBinding) { + tNode.classBindings = tBindings; + } else { + tNode.styleBindings = tBindings; + } +} + +/** + * Marks `TStyleValue`s as duplicates if another style binding in the list has the same + * `TStyleValue`. + * + * NOTE: this function is intended to be called twice once with `isPrevDir` set to `true` and once + * with it set to `false` to search both the previous as well as next items in the list. + * + * No duplicate case + * ``` + * [style.color] + * [style.width.px] <<- index + * [style.height.px] + * ``` + * + * In the above case adding `[style.width.px]` to the existing `[style.color]` produces no + * duplicates because `width` is not found in any other part of the linked list. + * + * Duplicate case + * ``` + * [style.color] + * [style.width.em] + * [style.width.px] <<- index + * ``` + * In the above case adding `[style.width.px]` will produce a duplicate with `[style.width.em]` + * because `width` is found in the chain. + * + * Map case 1 + * ``` + * [style.width.px] + * [style.color] + * [style] <<- index + * ``` + * In the above case adding `[style]` will produce a duplicate with any other bindings because + * `[style]` is a Map and as such is fully dynamic and could produce `color` or `width`. + * + * Map case 2 + * ``` + * [style] + * [style.width.px] + * [style.color] <<- index + * ``` + * In the above case adding `[style.color]` will produce a duplicate because there is already a + * `[style]` binding which is a Map and as such is fully dynamic and could produce `color` or + * `width`. + * + * NOTE: Once `[style]` (Map) is added into the system all things are mapped as duplicates. + * NOTE: We use `style` as example, but same logic is applied to `class`es as well. + * + * @param tData + * @param tStylingKey + * @param index + * @param staticValues + * @param isPrevDir + */ +function markDuplicates( + tData: TData, tStylingKey: TStylingKey, index: number, staticValues: StylingMapArray | null, + isPrevDir: boolean) { + const tStylingAtIndex = tData[index + 1] as TStylingRange; + const key: string|null = typeof tStylingKey === 'object' ? tStylingKey.key : tStylingKey; + const isMap = key === null; + let cursor = + isPrevDir ? getTStylingRangePrev(tStylingAtIndex) : getTStylingRangeNext(tStylingAtIndex); + let foundDuplicate = false; + // We keep iterating as long as we have a cursor + // AND either: We found what we are looking for, or we are a map in which case we have to + // continue searching even after we find what we were looking for since we are a wild card + // and everything needs to be flipped to duplicate. + while (cursor !== 0 && (foundDuplicate === false || isMap)) { + const tStylingValueAtCursor = tData[cursor] as TStylingKey; + const tStyleRangeAtCursor = tData[cursor + 1] as TStylingRange; + const keyAtCursor = typeof tStylingValueAtCursor === 'object' ? tStylingValueAtCursor.key : + tStylingValueAtCursor; + if (keyAtCursor === null || key == null || keyAtCursor === key) { + foundDuplicate = true; + tData[cursor + 1] = isPrevDir ? setTStylingRangeNextDuplicate(tStyleRangeAtCursor) : + setTStylingRangePrevDuplicate(tStyleRangeAtCursor); + } + cursor = isPrevDir ? getTStylingRangePrev(tStyleRangeAtCursor) : + getTStylingRangeNext(tStyleRangeAtCursor); + } + if (staticValues !== null && // If we have static values to search + !foundDuplicate // If we have duplicate don't bother since we are already marked as + // duplicate + ) { + if (isMap) { + // if we are a Map (and we have statics) we must assume duplicate + foundDuplicate = true; + } else { + for (let i = 1; foundDuplicate === false && i < staticValues.length; i = i + 2) { + if (staticValues[i] === key) { + foundDuplicate = true; + break; + } + } + } + } + if (foundDuplicate) { + tData[index + 1] = isPrevDir ? setTStylingRangePrevDuplicate(tStylingAtIndex) : + setTStylingRangeNextDuplicate(tStylingAtIndex); + } +} + +/** + * Computes the new styling value starting at `index` styling binding. + * + * @param tData `TData` containing the styling binding linked list. + * - `TData[index]` contains the binding name. + * - `TData[index + 1]` contains the `TStylingRange` a linked list of other bindings. + * @param tNode `TNode` containing the initial styling values. + * @param lView `LView` containing the styling values. + * - `LView[index]` contains the binding value. + * - `LView[index + 1]` contains the concatenated value up to this point. + * @param index the location in `TData`/`LView` where the styling search should start. + * @param isClassBinding `true` if binding to `className`; `false` when binding to `style`. + */ +export function flushStyleBinding( + tData: TData, tNode: TNode, lView: LView, index: number, isClassBinding: boolean): string { + const tStylingRangeAtIndex = tData[index + 1] as TStylingRange; + // When styling changes we don't have to start at the begging. Instead we start at the change + // value and look up the previous concatenation as a starting point going forward. + const lastUnchangedValueIndex = getTStylingRangePrev(tStylingRangeAtIndex); + let text = lastUnchangedValueIndex === 0 ? getStaticStylingValue(tNode, isClassBinding) : + lView[lastUnchangedValueIndex + 1] as string; + let cursor = index; + while (cursor !== 0) { + const value = lView[cursor]; + const key = tData[cursor] as TStylingKey; + const stylingRange = tData[cursor + 1] as TStylingRange; + lView[cursor + 1] = text = appendStyling( + text, key, value, null, getTStylingRangePrevDuplicate(stylingRange), isClassBinding); + cursor = getTStylingRangeNext(stylingRange); + } + return text; +} + +/** + * Retrieves the static value for styling. + * + * @param tNode + * @param isClassBinding + */ +function getStaticStylingValue(tNode: TNode, isClassBinding: Boolean) { + // TODO(misko): implement once we have more code integrated. + return ''; +} + +/** + * Append new styling to the currently concatenated styling text. + * + * This function concatenates the existing `className`/`cssText` text with the binding value. + * + * @param text Text to concatenate to. + * @param stylingKey `TStylingKey` holding the key (className or style property name). + * @param value The value for the key. + * - `isClassBinding === true` + * - `boolean` if `true` then add the key to the class list string. + * - `Array` add each string value to the class list string. + * - `Object` add object key to the class list string if the key value is truthy. + * - `isClassBinding === false` + * - `Array` Not supported. + * - `Object` add object key/value to the styles. + * @param sanitizer Optional sanitizer to use. If `null` the `stylingKey` sanitizer will be used. + * This is provided so that `ɵɵstyleMap()`/`ɵɵclassMap()` can recursively call + * `appendStyling` without having ta package the sanitizer into `TStylingSanitizationKey`. + * @param hasPreviousDuplicate determines if there is a chance of duplicate. + * - `true` the existing `text` should be searched for duplicates and if any found they + * should be removed. + * - `false` Fast path, just concatenate the strings. + * @param isClassBinding Determines if the `text` is `className` or `cssText`. + * @returns new styling string with the concatenated values. + */ +export function appendStyling( + text: string, stylingKey: TStylingKey, value: any, sanitizer: SanitizerFn | null, + hasPreviousDuplicate: boolean, isClassBinding: boolean): string { + let key: string; + let suffixOrSanitizer: string|SanitizerFn|undefined|null = sanitizer; + if (typeof stylingKey === 'object') { + if (stylingKey.key === null) { + return value != null ? stylingKey.extra(text, value, hasPreviousDuplicate) : text; + } else { + suffixOrSanitizer = stylingKey.extra; + key = stylingKey.key; + } + } else { + key = stylingKey; + } + if (isClassBinding) { + ngDevMode && assertEqual(typeof stylingKey === 'string', true, 'Expecting key to be string'); + if (hasPreviousDuplicate) { + text = toggleClass(text, stylingKey as string, !!value); + } else if (value) { + text = text === '' ? stylingKey as string : text + ' ' + stylingKey; + } + } else { + if (hasPreviousDuplicate) { + text = removeStyle(text, key); + } + const keyValue = + key + ': ' + (typeof suffixOrSanitizer === 'function' ? + suffixOrSanitizer(value) : + (suffixOrSanitizer == null ? value : value + suffixOrSanitizer)); + text = text === '' ? keyValue : text + '; ' + keyValue; + } + return text; +} + +/** + * `ɵɵclassMap()` inserts `CLASS_MAP_STYLING_KEY` as a key to the `insertTStylingBinding()`. + * + * The purpose of this key is to add class map abilities to the concatenation in a tree shakable + * way. If `ɵɵclassMap()` is not referenced than `CLASS_MAP_STYLING_KEY` will become eligible for + * tree shaking. + * + * This key supports: `strings`, `object` (as Map) and `Array`. In each case it is necessary to + * break the classes into parts and concatenate the parts into the `text`. The concatenation needs + * to be done in parts as each key is individually subject to overwrites. + */ +export const CLASS_MAP_STYLING_KEY: TStylingMapKey = { + key: null, + extra: (text: string, value: any, hasPreviousDuplicate: boolean): string => { + if (Array.isArray(value)) { + // We support Arrays + for (let i = 0; i < value.length; i++) { + text = appendStyling(text, value[i], true, null, hasPreviousDuplicate, true); + } + } else if (typeof value === 'object') { + // We support maps + for (let key in value) { + text = appendStyling(text, key, value[key], null, hasPreviousDuplicate, true); + } + } else if (typeof value === 'string') { + // We support strings + if (hasPreviousDuplicate) { + // We need to parse and process it. + const changes = new Map(); + splitClassList(value, changes, false); + changes.forEach((_, key) => text = appendStyling(text, key, true, null, true, true)); + } else { + // No duplicates, just append it. + text = text === '' ? value : text + ' ' + value; + } + } else { + // All other cases are not supported. + ngDevMode && throwError('Unsupported value for class binding: ' + value); + } + return text; + } +}; + +/** + * `ɵɵstyleMap()` inserts `STYLE_MAP_STYLING_KEY` as a key to the `insertTStylingBinding()`. + * + * The purpose of this key is to add style map abilities to the concatenation in a tree shakable + * way. If `ɵɵstyleMap()` is not referenced than `STYLE_MAP_STYLING_KEY` will become eligible for + * tree shaking. (`STYLE_MAP_STYLING_KEY` also pulls in the sanitizer as `ɵɵstyleMap()` could have + * a sanitizable property.) + * + * This key supports: `strings`, and `object` (as Map). In each case it is necessary to + * break the style into parts and concatenate the parts into the `text`. The concatenation needs + * to be done in parts as each key is individually subject to overwrites. + */ +export const STYLE_MAP_STYLING_KEY: TStylingMapKey = { + key: null, + extra: (text: string, value: any, hasPreviousDuplicate: boolean): string => { + if (Array.isArray(value)) { + // We don't support Arrays + ngDevMode && throwError('Style bindings do not support array bindings: ' + value); + } else if (typeof value === 'object') { + // We support maps + for (let key in value) { + text = appendStyling( + text, key, value[key], stylePropNeedsSanitization(key) ? ɵɵsanitizeStyle : null, + hasPreviousDuplicate, false); + } + } else if (typeof value === 'string') { + // We support strings + if (hasPreviousDuplicate) { + // We need to parse and process it. + const changes: StyleChangesMap = + new Map(); + parseKeyValue(value, changes, false); + changes.forEach( + (value, key) => text = appendStyling( + text, key, value.old, stylePropNeedsSanitization(key) ? ɵɵsanitizeStyle : null, + true, false)); + } else { + // No duplicates, just append it. + text = text === '' ? value : text + '; ' + value; + } + } else { + // All other cases are not supported. + ngDevMode && throwError('Unsupported value for style binding: ' + value); + } + return text; + } +}; diff --git a/packages/core/src/render3/styling/styling_debug.ts b/packages/core/src/render3/styling/styling_debug.ts index ae87ca6e93..d901b91659 100644 --- a/packages/core/src/render3/styling/styling_debug.ts +++ b/packages/core/src/render3/styling/styling_debug.ts @@ -6,9 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ import {StyleSanitizeFn} from '../../sanitization/style_sanitizer'; -import {TNodeFlags} from '../interfaces/node'; +import {TNode, TNodeFlags} from '../interfaces/node'; import {RElement} from '../interfaces/renderer'; -import {ApplyStylingFn, LStylingData, TStylingContext, TStylingContextIndex, TStylingNode} from '../interfaces/styling'; +import {ApplyStylingFn, LStylingData, TStylingContext, TStylingContextIndex, TStylingNode, TStylingRange, getTStylingRangePrev} from '../interfaces/styling'; +import {TData} from '../interfaces/view'; import {getCurrentStyleSanitizer} from '../state'; import {attachDebugObject} from '../util/debug_utils'; import {MAP_BASED_ENTRY_PROP_NAME, TEMPLATE_DIRECTIVE_INDEX, allowDirectStyling as _allowDirectStyling, getBindingValue, getDefaultValue, getGuardMask, getProp, getPropValuesStartPosition, getValue, getValuesCount, hasConfig, isSanitizationRequired, isStylingContext, normalizeIntoStylingMap, setValue} from '../util/styling_utils'; @@ -495,3 +496,21 @@ function buildConfig(tNode: TStylingNode, isClassBased: boolean): DebugStylingCo allowDirectStyling, // }; } + + +/** + * Find the head of the styling binding linked list. + */ +export function getStylingBindingHead(tData: TData, tNode: TNode, isClassBinding: boolean): number { + let index = getTStylingRangePrev(isClassBinding ? tNode.classBindings : tNode.styleBindings); + while (true) { + const tStylingRange = tData[index + 1] as TStylingRange; + const prev = getTStylingRangePrev(tStylingRange); + if (prev === 0) { + // found head exit. + return index; + } else { + index = prev; + } + } +} \ No newline at end of file diff --git a/packages/core/src/sanitization/sanitization.ts b/packages/core/src/sanitization/sanitization.ts index ba64c67a6a..1108eee5f5 100644 --- a/packages/core/src/sanitization/sanitization.ts +++ b/packages/core/src/sanitization/sanitization.ts @@ -15,7 +15,7 @@ import {BypassType, allowSanitizationBypassAndThrow, unwrapSafeValue} from './by import {_sanitizeHtml as _sanitizeHtml} from './html_sanitizer'; import {Sanitizer} from './sanitizer'; import {SecurityContext} from './security'; -import {StyleSanitizeFn, StyleSanitizeMode, _sanitizeStyle as _sanitizeStyle} from './style_sanitizer'; +import {StyleSanitizeFn, StyleSanitizeMode, _sanitizeStyle} from './style_sanitizer'; import {_sanitizeUrl as _sanitizeUrl} from './url_sanitizer'; @@ -190,9 +190,7 @@ export const ɵɵdefaultStyleSanitizer = mode = mode || StyleSanitizeMode.ValidateAndSanitize; let doSanitizeValue = true; if (mode & StyleSanitizeMode.ValidateProperty) { - doSanitizeValue = prop === 'background-image' || prop === 'background' || - prop === 'border-image' || prop === 'filter' || prop === 'list-style' || - prop === 'list-style-image' || prop === 'clip-path'; + doSanitizeValue = stylePropNeedsSanitization(prop); } if (mode & StyleSanitizeMode.SanitizeOnly) { @@ -202,6 +200,12 @@ export const ɵɵdefaultStyleSanitizer = } } as StyleSanitizeFn); +export function stylePropNeedsSanitization(prop: string): boolean { + return prop === 'background-image' || prop === 'background' || prop === 'border-image' || + prop === 'filter' || prop === 'list-style' || prop === 'list-style-image' || + prop === 'clip-path'; +} + export function validateAgainstEventProperties(name: string) { if (name.toLowerCase().startsWith('on')) { const msg = `Binding to event property '${name}' is disallowed for security reasons, ` + diff --git a/packages/core/test/render3/styling_next/class_differ_spec.ts b/packages/core/test/render3/styling_next/class_differ_spec.ts index 3ee3f8086f..7f91bfabcd 100644 --- a/packages/core/test/render3/styling_next/class_differ_spec.ts +++ b/packages/core/test/render3/styling_next/class_differ_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {computeClassChanges, removeClass, splitClassList} from '../../../src/render3/styling/class_differ'; +import {classIndexOf, computeClassChanges, removeClass, splitClassList, toggleClass} from '../../../src/render3/styling/class_differ'; describe('class differ', () => { describe('computeClassChanges', () => { @@ -98,6 +98,35 @@ describe('class differ', () => { expect(removeClass('ABC', 'BC')).toEqual('ABC'); }); }); + + describe('removeClass', () => { + it('should toggle a class', () => { + expect(toggleClass('', 'B', false)).toEqual(''); + expect(toggleClass('', 'B', true)).toEqual('B'); + expect(toggleClass('A B C', 'B', true)).toEqual('A B C'); + expect(toggleClass('A C', 'B', true)).toEqual('A C B'); + expect(toggleClass('A B C', 'B', false)).toEqual('A C'); + expect(toggleClass('A B B C', 'B', false)).toEqual('A C'); + expect(toggleClass('A B B C', 'B', true)).toEqual('A B B C'); + }); + }); + + describe('classIndexOf', () => { + it('should match simple case', () => { + expect(classIndexOf('A', 'A', 0)).toEqual(0); + expect(classIndexOf('AA', 'A', 0)).toEqual(-1); + expect(classIndexOf('_A_', 'A', 0)).toEqual(-1); + expect(classIndexOf('_ A_', 'A', 0)).toEqual(-1); + expect(classIndexOf('_ A _', 'A', 0)).toEqual(2); + }); + + it('should not match on partial matches', () => { + expect(classIndexOf('ABC AB', 'AB', 0)).toEqual(4); + expect(classIndexOf('AB ABC', 'AB', 1)).toEqual(-1); + expect(classIndexOf('ABC BC', 'BC', 0)).toEqual(4); + expect(classIndexOf('BC ABC', 'BB', 1)).toEqual(-1); + }); + }); }); export function sortedForEach(map: Map, fn: (value: V, key: string) => void): void { diff --git a/packages/core/test/render3/styling_next/style_binding_list_spec.ts b/packages/core/test/render3/styling_next/style_binding_list_spec.ts new file mode 100644 index 0000000000..335034e4af --- /dev/null +++ b/packages/core/test/render3/styling_next/style_binding_list_spec.ts @@ -0,0 +1,658 @@ +/** + * @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 {createTNode} from '@angular/core/src/render3/instructions/shared'; +import {TNode, TNodeType} from '@angular/core/src/render3/interfaces/node'; +import {TStylingKey, TStylingRange, getTStylingRangeNext, getTStylingRangeNextDuplicate, getTStylingRangePrev, getTStylingRangePrevDuplicate} from '@angular/core/src/render3/interfaces/styling'; +import {LView, TData} from '@angular/core/src/render3/interfaces/view'; +import {enterView, leaveView} from '@angular/core/src/render3/state'; +import {CLASS_MAP_STYLING_KEY, STYLE_MAP_STYLING_KEY, appendStyling, flushStyleBinding, insertTStylingBinding} from '@angular/core/src/render3/styling/style_binding_list'; +import {getStylingBindingHead} from '@angular/core/src/render3/styling/styling_debug'; +import {newArray} from '@angular/core/src/util/array_utils'; + +describe('TNode styling linked list', () => { + const mockFirstUpdatePassLView: LView = [null, {firstUpdatePass: true}] as any; + beforeEach(() => enterView(mockFirstUpdatePassLView, null)); + afterEach(() => leaveView()); + describe('insertTStylingBinding', () => { + it('should append template only', () => { + const tNode = createTNode(null !, null !, TNodeType.Element, 0, '', null); + const tData: TData = [null, null]; + + insertTStylingBinding(tData, tNode, 'tmpl1', 2, false, true); + expectRange(tNode.classBindings).toEqual([2, 2]); + expectTData(tData).toEqual([ + null, null, // 0 + 'tmpl1', [false, 0, false, 0], // 2 + ]); + + insertTStylingBinding(tData, tNode, 'tmpl2', 4, false, true); + expectRange(tNode.classBindings).toEqual([2, 4]); + expectTData(tData).toEqual([ + null, null, // 0 + 'tmpl1', [false, 0, false, 4], // 2 + 'tmpl2', [false, 2, false, 0], // 4 + ]); + + insertTStylingBinding(tData, tNode, 'host1', 6, true, true); + expectRange(tNode.classBindings).toEqual([2, 4]); + expectTData(tData).toEqual([ + null, null, // 0 + 'tmpl1', [false, 6, false, 4], // 2 + 'tmpl2', [false, 2, false, 0], // 4 + 'host1', [false, 0, false, 2], // 6 + ]); + + insertTStylingBinding(tData, tNode, 'host2', 8, true, true); + expectRange(tNode.classBindings).toEqual([2, 4]); + expectTData(tData).toEqual([ + null, null, // 0 + 'tmpl1', [false, 8, false, 4], // 2 + 'tmpl2', [false, 2, false, 0], // 4 + 'host1', [false, 0, false, 8], // 6 + 'host2', [false, 6, false, 2], // 8 + ]); + }); + + it('should append host only', () => { + const tData: TData = [null, null]; + const tNode = createTNode(null !, null !, TNodeType.Element, 0, '', null); + + insertTStylingBinding(tData, tNode, 'host1', 2, true, true); + expectRange(tNode.classBindings).toEqual([2, 0 /* no template binding */]); + expectTData(tData).toEqual([ + null, null, // 0 + 'host1', [false, 0, false, 0], // 2 + ]); + + insertTStylingBinding(tData, tNode, 'host2', 4, true, true); + expectRange(tNode.classBindings).toEqual([4, 0 /* no template binding */]); + expectTData(tData).toEqual([ + null, null, // 0 + 'host1', [false, 0, false, 4], // 2 + 'host2', [false, 2, false, 0], // 4 + ]); + }); + + it('should append template and host', () => { + const tNode = createTNode(null !, null !, TNodeType.Element, 0, '', null); + const tData: TData = [null, null]; + + insertTStylingBinding(tData, tNode, 'tmpl1', 2, false, true); + expectRange(tNode.classBindings).toEqual([2, 2]); + expectTData(tData).toEqual([ + null, null, // 0 + 'tmpl1', [false, 0, false, 0], // 2 + ]); + + insertTStylingBinding(tData, tNode, 'host1', 4, true, true); + expectRange(tNode.classBindings).toEqual([2, 2]); + expectTData(tData).toEqual([ + null, null, // 0 + 'tmpl1', [false, 4, false, 0], // 2 + 'host1', [false, 0, false, 2], // 4 + ]); + }); + + it('should support example in \'tnode_linked_list.ts\' documentation', () => { + // See: `tnode_linked_list.ts` file description for this example. + // Template: (ExampleComponent) + // ɵɵstyleMap({color: '#001'}); // Binding index: 10 + // ɵɵstyleProp('color', '#002'); // Binding index: 12 + // MyComponent + // ɵɵstyleMap({color: '#003'}); // Binding index: 20 + // ɵɵstyleProp('color', '#004'); // Binding index: 22 + // Style1Directive + // ɵɵstyleMap({color: '#005'}); // Binding index: 24 + // ɵɵstyleProp('color', '#006'); // Binding index: 26 + // Style2Directive + // ɵɵstyleMap({color: '#007'}); // Binding index: 28 + // ɵɵstyleProp('color', '#008'); // Binding index: 30 + + const tNode = createTNode(null !, null !, TNodeType.Element, 0, '', null); + const tData: TData = newArray(32, null); + const STYLE = STYLE_MAP_STYLING_KEY; + + insertTStylingBinding(tData, tNode, STYLE, 10, false, false); + expectRange(tNode.styleBindings).toEqual([10, 10]); + expectTData(tData).toEqual([ + ...empty_0_through_9, // + STYLE, [false, 0, false, 0], // 10 - Template: ɵɵstyleMap({color: '#001'}); + null, null, // 12 + ...empty_14_through_19, // 14-19 + null, null, // 20 + null, null, // 22 + null, null, // 24 + null, null, // 26 + null, null, // 28 + null, null, // 30 + ]); + expectPriorityOrder(tData, tNode, false).toEqual([ + [10, null, false, false], // 10 - Template: ɵɵstyleMap({color: '#001'}); + ]); + + + insertTStylingBinding(tData, tNode, 'color', 12, false, false); + expectRange(tNode.styleBindings).toEqual([10, 12]); + expectTData(tData).toEqual([ + ...empty_0_through_9, // + STYLE, [false, 0, false, 12], // 10 - Template: ɵɵstyleMap({color: '#001'}); + 'color', [false, 10, false, 0], // 12 - Template: ɵɵstyleProp('color', '#002'}); + ...empty_14_through_19, // 14-19 + null, null, // 20 + null, null, // 22 + null, null, // 24 + null, null, // 26 + null, null, // 28 + null, null, // 30 + ]); + expectPriorityOrder(tData, tNode, false).toEqual([ + [10, null, false, true], // 10 - Template: ɵɵstyleMap({color: '#001'}); + [12, 'color', true, false], // 12 - Template: ɵɵstyleProp('color', '#002'}); + ]); + + insertTStylingBinding(tData, tNode, STYLE, 20, true, false); + expectRange(tNode.styleBindings).toEqual([10, 12]); + expectTData(tData).toEqual([ + ...empty_0_through_9, // + STYLE, [false, 20, false, 12], // 10 - Template: ɵɵstyleMap({color: '#001'}); + 'color', [false, 10, false, 0], // 12 - Template: ɵɵstyleProp('color', '#002'}); + ...empty_14_through_19, // 14-19 + STYLE, [false, 0, false, 10], // 20 - MyComponent: ɵɵstyleMap({color: '#003'}); + null, null, // 22 + null, null, // 24 + null, null, // 26 + null, null, // 28 + null, null, // 30 + ]); + expectPriorityOrder(tData, tNode, false).toEqual([ + [20, null, false, true], // 20 - MyComponent: ɵɵstyleMap({color: '#003'}); + [10, null, true, true], // 10 - Template: ɵɵstyleMap({color: '#001'}); + [12, 'color', true, false], // 12 - Template: ɵɵstyleProp('color', '#002'}); + ]); + + insertTStylingBinding(tData, tNode, 'color', 22, true, false); + expectRange(tNode.styleBindings).toEqual([10, 12]); + expectTData(tData).toEqual([ + ...empty_0_through_9, // 00-09 + STYLE, [false, 22, false, 12], // 10 - Template: ɵɵstyleMap({color: '#001'}); + 'color', [false, 10, false, 0], // 12 - Template: ɵɵstyleProp('color', '#002'}); + ...empty_14_through_19, // 14-19 + STYLE, [false, 0, false, 22], // 20 - MyComponent: ɵɵstyleMap({color: '#003'}); + 'color', [false, 20, false, 10], // 22 - MyComponent: ɵɵstyleProp('color', '#004'}); + null, null, // 24 + null, null, // 26 + null, null, // 28 + null, null, // 30 + ]); + expectPriorityOrder(tData, tNode, false).toEqual([ + [20, null, false, true], // 20 - MyComponent: ɵɵstyleMap({color: '#003'}); + [22, 'color', true, true], // 22 - MyComponent: ɵɵstyleProp('color', '#004'}); + [10, null, true, true], // 10 - Template: ɵɵstyleMap({color: '#001'}); + [12, 'color', true, false], // 12 - Template: ɵɵstyleProp('color', '#002'}); + ]); + + insertTStylingBinding(tData, tNode, STYLE, 24, true, false); + expectRange(tNode.styleBindings).toEqual([10, 12]); + expectTData(tData).toEqual([ + ...empty_0_through_9, // + STYLE, [false, 24, false, 12], // 10 - Template: ɵɵstyleMap({color: '#001'}); + 'color', [false, 10, false, 0], // 12 - Template: ɵɵstyleProp('color', '#002'}); + ...empty_14_through_19, // 14-19 + STYLE, [false, 0, false, 22], // 20 - MyComponent: ɵɵstyleMap({color: '#003'}); + 'color', [false, 20, false, 24], // 22 - MyComponent: ɵɵstyleProp('color', '#004'}); + STYLE, [false, 22, false, 10], // 24 - Style1Directive: ɵɵstyleMap({color: '#003'}); + null, null, // 26 + null, null, // 28 + null, null, // 30 + ]); + expectPriorityOrder(tData, tNode, false).toEqual([ + [20, null, false, true], // 20 - MyComponent: ɵɵstyleMap({color: '#003'}); + [22, 'color', true, true], // 22 - MyComponent: ɵɵstyleProp('color', '#004'}); + [24, null, true, true], // 24 - Style1Directive: ɵɵstyleMap({color: '#003'}); + [10, null, true, true], // 10 - Template: ɵɵstyleMap({color: '#001'}); + [12, 'color', true, false], // 12 - Template: ɵɵstyleProp('color', '#002'}); + ]); + + insertTStylingBinding(tData, tNode, 'color', 26, true, false); + expectRange(tNode.styleBindings).toEqual([10, 12]); + expectTData(tData).toEqual([ + ...empty_0_through_9, // 00-09 + STYLE, [false, 26, false, 12], // 10 - Template: ɵɵstyleMap({color: '#001'}); + 'color', [false, 10, false, 0], // 12 - Template: ɵɵstyleProp('color', '#002'}); + ...empty_14_through_19, // 14-19 + STYLE, [false, 0, false, 22], // 20 - MyComponent: ɵɵstyleMap({color: '#003'}); + 'color', [false, 20, false, 24], // 22 - MyComponent: ɵɵstyleProp('color', '#004'}); + STYLE, [false, 22, false, 26], // 24 - Style1Directive: ɵɵstyleMap({color: '#005'}); + 'color', [false, 24, false, 10], // 26 - Style1Directive: ɵɵstyleProp('color', '#006'}); + null, null, // 28 + null, null, // 30 + ]); + expectPriorityOrder(tData, tNode, false).toEqual([ + [20, null, false, true], // 20 - MyComponent: ɵɵstyleMap({color: '#003'}); + [22, 'color', true, true], // 22 - MyComponent: ɵɵstyleProp('color', '#004'}); + [24, null, true, true], // 24 - Style1Directive: ɵɵstyleMap({color: '#003'}); + [26, 'color', true, true], // 26 - Style1Directive: ɵɵstyleProp('color', '#006'}); + [10, null, true, true], // 10 - Template: ɵɵstyleMap({color: '#001'}); + [12, 'color', true, false], // 12 - Template: ɵɵstyleProp('color', '#002'}); + ]); + + + insertTStylingBinding(tData, tNode, STYLE, 28, true, false); + expectRange(tNode.styleBindings).toEqual([10, 12]); + expectTData(tData).toEqual([ + ...empty_0_through_9, // + STYLE, [false, 28, false, 12], // 10 - Template: ɵɵstyleMap({color: '#001'}); + 'color', [false, 10, false, 0], // 12 - Template: ɵɵstyleProp('color', '#002'}); + ...empty_14_through_19, // 14-19 + STYLE, [false, 0, false, 22], // 20 - MyComponent: ɵɵstyleMap({color: '#003'}); + 'color', [false, 20, false, 24], // 22 - MyComponent: ɵɵstyleProp('color', '#004'}); + STYLE, [false, 22, false, 26], // 24 - Style1Directive: ɵɵstyleMap({color: '#005'}); + 'color', [false, 24, false, 28], // 26 - Style1Directive: ɵɵstyleProp('color', '#006'}); + STYLE, [false, 26, false, 10], // 28 - Style2Directive: ɵɵstyleMap({color: '#007'}); + null, null, // 30 + ]); + expectPriorityOrder(tData, tNode, false).toEqual([ + [20, null, false, true], // 20 - MyComponent: ɵɵstyleMap({color: '#003'}); + [22, 'color', true, true], // 22 - MyComponent: ɵɵstyleProp('color', '#004'}); + [24, null, true, true], // 24 - Style1Directive: ɵɵstyleMap({color: '#003'}); + [26, 'color', true, true], // 26 - Style1Directive: ɵɵstyleProp('color', '#006'}); + [28, null, true, true], // 28 - Style2Directive: ɵɵstyleMap({color: '#007'}); + [10, null, true, true], // 10 - Template: ɵɵstyleMap({color: '#001'}); + [12, 'color', true, false], // 12 - Template: ɵɵstyleProp('color', '#002'}); + ]); + + insertTStylingBinding(tData, tNode, 'color', 30, true, false); + expectRange(tNode.styleBindings).toEqual([10, 12]); + expectTData(tData).toEqual([ + ...empty_0_through_9, // 00-09 + STYLE, [false, 30, false, 12], // 10 - Template: ɵɵstyleMap({color: '#001'}); + 'color', [false, 10, false, 0], // 12 - Template: ɵɵstyleProp('color', '#002'}); + ...empty_14_through_19, // 14-19 + STYLE, [false, 0, false, 22], // 20 - MyComponent: ɵɵstyleMap({color: '#003'}); + 'color', [false, 20, false, 24], // 22 - MyComponent: ɵɵstyleProp('color', '#004'}); + STYLE, [false, 22, false, 26], // 24 - Style1Directive: ɵɵstyleMap({color: '#005'}); + 'color', [false, 24, false, 28], // 26 - Style1Directive: ɵɵstyleProp('color', '#006'}); + STYLE, [false, 26, false, 30], // 28 - Style2Directive: ɵɵstyleMap({color: '#007'}); + 'color', [false, 28, false, 10], // 30 - Style2Directive: ɵɵstyleProp('color', '#008'}); + ]); + expectPriorityOrder(tData, tNode, false).toEqual([ + [20, null, false, true], // 20 - MyComponent: ɵɵstyleMap({color: '#003'}); + [22, 'color', true, true], // 22 - MyComponent: ɵɵstyleProp('color', '#004'}); + [24, null, true, true], // 24 - Style1Directive: ɵɵstyleMap({color: '#005'}); + [26, 'color', true, true], // 26 - Style1Directive: ɵɵstyleProp('color', '#006'}); + [28, null, true, true], // 28 - Style2Directive: ɵɵstyleMap({color: '#007'}); + [30, 'color', true, true], // 30 - Style2Directive: ɵɵstyleProp('color', '#008'}); + [10, null, true, true], // 10 - Template: ɵɵstyleMap({color: '#001'}); + [12, 'color', true, false], // 12 - Template: ɵɵstyleProp('color', '#002'}); + ]); + }); + + }); + + describe('markDuplicates', () => { + it('should not mark items as duplicate if names don\'t match', () => { + const tNode = createTNode(null !, null !, TNodeType.Element, 0, '', null); + const tData: TData = [null, null]; + insertTStylingBinding(tData, tNode, 'color', 2, false, false); + expectPriorityOrder(tData, tNode, false).toEqual([ + // PREV, NEXT + [2, 'color', false, false], + ]); + + insertTStylingBinding(tData, tNode, 'width', 4, false, false); + expectPriorityOrder(tData, tNode, false).toEqual([ + // PREV, NEXT + [2, 'color', false, false], + [4, 'width', false, false], + ]); + + insertTStylingBinding(tData, tNode, 'height', 6, true, false); + expectPriorityOrder(tData, tNode, false).toEqual([ + // PREV, NEXT + [6, 'height', false, false], + [2, 'color', false, false], + [4, 'width', false, false], + ]); + }); + + it('should mark items as duplicate if names match', () => { + const tNode = createTNode(null !, null !, TNodeType.Element, 0, '', null); + const tData: TData = [null, null]; + insertTStylingBinding(tData, tNode, 'color', 2, false, false); + expectPriorityOrder(tData, tNode, false).toEqual([ + // PREV, NEXT + [2, 'color', false, false], + ]); + insertTStylingBinding(tData, tNode, 'color', 4, false, false); + expectPriorityOrder(tData, tNode, false).toEqual([ + // PREV, NEXT + [2, 'color', false, true], + [4, 'color', true, false], + ]); + + insertTStylingBinding(tData, tNode, 'height', 6, true, false); + expectPriorityOrder(tData, tNode, false).toEqual([ + // PREV, NEXT + [6, 'height', false, false], + [2, 'color', false, true], + [4, 'color', true, false], + ]); + }); + + it('should treat maps as matching all', () => { + const tNode = createTNode(null !, null !, TNodeType.Element, 0, '', null); + const tData: TData = [null, null]; + insertTStylingBinding(tData, tNode, 'color', 2, false, false); + insertTStylingBinding(tData, tNode, 'height', 4, true, false); + expectPriorityOrder(tData, tNode, false).toEqual([ + // PREV, NEXT + [4, 'height', false, false], + [2, 'color', false, false], + ]); + + insertTStylingBinding(tData, tNode, STYLE_MAP_STYLING_KEY /*Map*/, 6, true, false); + expectPriorityOrder(tData, tNode, false).toEqual([ + // PREV, NEXT + [4, 'height', false, true], + [6, null, true, true], + [2, 'color', true, false], + ]); + }); + + it('should mark all things after map as duplicate', () => { + const tNode = createTNode(null !, null !, TNodeType.Element, 0, '', null); + const tData: TData = [null, null]; + insertTStylingBinding(tData, tNode, STYLE_MAP_STYLING_KEY, 2, false, false); + insertTStylingBinding(tData, tNode, 'height', 4, false, false); + insertTStylingBinding(tData, tNode, 'color', 6, true, false); + expectPriorityOrder(tData, tNode, false).toEqual([ + // PREV, NEXT + [6, 'color', false, true], + [2, null, true, true], + [4, 'height', true, false], + ]); + }); + + it('should mark duplicate on complex objects like width.px', () => { + const tNode = createTNode(null !, null !, TNodeType.Element, 0, '', null); + const tData: TData = [null, null]; + insertTStylingBinding(tData, tNode, 'width', 2, false, false); + insertTStylingBinding(tData, tNode, {key: 'height', extra: 'px'}, 4, false, false); + expectPriorityOrder(tData, tNode, false).toEqual([ + // PREV, NEXT + [2, 'width', false, false], + [4, 'height', false, false], + ]); + insertTStylingBinding(tData, tNode, {key: 'height', extra: 'em'}, 6, false, false); + expectPriorityOrder(tData, tNode, false).toEqual([ + // PREV, NEXT + [2, 'width', false, false], + [4, 'height', false, true], + [6, 'height', true, false], + ]); + insertTStylingBinding(tData, tNode, 'width', 8, false, false); + expectPriorityOrder(tData, tNode, false).toEqual([ + // PREV, NEXT + [2, 'width', false, true], + [4, 'height', false, true], + [6, 'height', true, false], + [8, 'width', true, false], + ]); + }); + + it('should mark duplicate on static fields', () => { + const tNode = createTNode(null !, null !, TNodeType.Element, 0, '', null); + tNode.styles = [null, 'color', 'blue']; + const tData: TData = [null, null]; + insertTStylingBinding(tData, tNode, 'width', 2, false, false); + expectPriorityOrder(tData, tNode, false).toEqual([ + // PREV, NEXT + [2, 'width', false, false], + ]); + + insertTStylingBinding(tData, tNode, 'color', 4, false, false); + expectPriorityOrder(tData, tNode, false).toEqual([ + // PREV, NEXT + [2, 'width', false, false], + [4, 'color', true, false], + ]); + + insertTStylingBinding(tData, tNode, STYLE_MAP_STYLING_KEY, 6, false, false); + expectPriorityOrder(tData, tNode, false).toEqual([ + // PREV, NEXT + [2, 'width', false, true], + [4, 'color', true, true], + [6, null, true, false], + ]); + }); + }); + + describe('styleBindingFlush', () => { + it('should write basic value', () => { + const fixture = new StylingFixture([['color']], false); + fixture.setBinding(0, 'red'); + expect(fixture.flush(0)).toEqual('color: red'); + }); + + it('should chain values and allow update mid list', () => { + const fixture = new StylingFixture([['color', {key: 'width', extra: 'px'}]], false); + fixture.setBinding(0, 'red'); + fixture.setBinding(1, '100'); + expect(fixture.flush(0)).toEqual('color: red; width: 100px'); + + fixture.setBinding(0, 'blue'); + fixture.setBinding(1, '200'); + expect(fixture.flush(1)).toEqual('color: red; width: 200px'); + expect(fixture.flush(0)).toEqual('color: blue; width: 200px'); + }); + + it('should remove duplicates', () => { + const fixture = new StylingFixture([['color', 'color']], false); + fixture.setBinding(0, 'red'); + fixture.setBinding(1, 'blue'); + expect(fixture.flush(0)).toEqual('color: blue'); + }); + + }); + + describe('appendStyling', () => { + it('should append simple style', () => { + expect(appendStyling('', 'color', 'red', null, false, false)).toEqual('color: red'); + expect(appendStyling('', 'color', 'red', null, true, false)).toEqual('color: red'); + expect(appendStyling('', 'color', 'red', null, false, true)).toEqual('color'); + expect(appendStyling('', 'color', 'red', null, true, true)).toEqual('color'); + expect(appendStyling('', 'color', true, null, true, true)).toEqual('color'); + expect(appendStyling('', 'color', false, null, true, true)).toEqual(''); + expect(appendStyling('', 'color', 0, null, true, true)).toEqual(''); + expect(appendStyling('', 'color', '', null, true, true)).toEqual(''); + }); + + it('should append simple style with suffix', () => { + expect(appendStyling('', {key: 'width', extra: 'px'}, 100, null, false, false)) + .toEqual('width: 100px'); + }); + + it('should append simple style with sanitizer', () => { + expect( + appendStyling('', {key: 'width', extra: (v: any) => `-${v}-`}, 100, null, false, false)) + .toEqual('width: -100-'); + }); + + it('should append class/style', () => { + expect(appendStyling('color: white', 'color', 'red', null, false, false)) + .toEqual('color: white; color: red'); + expect(appendStyling('MY-CLASS', 'color', true, null, false, true)).toEqual('MY-CLASS color'); + expect(appendStyling('MY-CLASS', 'color', false, null, true, true)).toEqual('MY-CLASS'); + }); + + it('should remove existing', () => { + expect(appendStyling('color: white', 'color', 'blue', null, true, false)) + .toEqual('color: blue'); + expect(appendStyling('A YES B', 'YES', false, null, true, true)).toEqual('A B'); + }); + + it('should support maps/arrays for classes', () => { + expect(appendStyling('', CLASS_MAP_STYLING_KEY, {A: true, B: false}, null, true, true)) + .toEqual('A'); + expect(appendStyling('A B C', CLASS_MAP_STYLING_KEY, {A: true, B: false}, null, true, true)) + .toEqual('A C'); + expect(appendStyling('', CLASS_MAP_STYLING_KEY, ['A', 'B'], null, true, true)).toEqual('A B'); + expect(appendStyling('A B C', CLASS_MAP_STYLING_KEY, ['A', 'B'], null, true, true)) + .toEqual('A B C'); + }); + + it('should support maps for styles', () => { + expect(appendStyling('', STYLE_MAP_STYLING_KEY, {A: 'a', B: 'b'}, null, true, false)) + .toEqual('A: a; B: b'); + expect(appendStyling( + 'A:_; B:_; C:_', STYLE_MAP_STYLING_KEY, {A: 'a', B: 'b'}, null, true, false)) + .toEqual('C:_; A: a; B: b'); + }); + + it('should support strings for classes', () => { + expect(appendStyling('', CLASS_MAP_STYLING_KEY, 'A B', null, true, true)).toEqual('A B'); + expect(appendStyling('A B C', CLASS_MAP_STYLING_KEY, 'A B', null, false, true)) + .toEqual('A B C A B'); + expect(appendStyling('A B C', CLASS_MAP_STYLING_KEY, 'A B', null, true, true)) + .toEqual('A B C'); + }); + + it('should support strings for styles', () => { + expect(appendStyling('A:a;B:b', STYLE_MAP_STYLING_KEY, 'A : a ; B : b', null, false, false)) + .toEqual('A:a;B:b; A : a ; B : b'); + expect( + appendStyling('A:_; B:_; C:_', STYLE_MAP_STYLING_KEY, 'A : a ; B : b', null, true, false)) + .toEqual('C:_; A: a; B: b'); + }); + + it('should throw no arrays for styles', () => { + expect(() => appendStyling('', STYLE_MAP_STYLING_KEY, ['A', 'a'], null, true, false)) + .toThrow(); + }); + + describe('style sanitization', () => { + it('should sanitize properties', () => { + // Verify map + expect(appendStyling( + '', STYLE_MAP_STYLING_KEY, { + 'background-image': 'url(javascript:evil())', + 'background': 'url(javascript:evil())', + 'border-image': 'url(javascript:evil())', + 'filter': 'url(javascript:evil())', + 'list-style': 'url(javascript:evil())', + 'list-style-image': 'url(javascript:evil())', + 'clip-path': 'url(javascript:evil())', + 'width': 'url(javascript:evil())', // should not sanitize + }, + null, true, false)) + .toEqual( + 'background-image: unsafe; ' + + 'background: unsafe; ' + + 'border-image: unsafe; ' + + 'filter: unsafe; ' + + 'list-style: unsafe; ' + + 'list-style-image: unsafe; ' + + 'clip-path: unsafe; ' + + 'width: url(javascript:evil())'); + // verify string + expect(appendStyling( + '', STYLE_MAP_STYLING_KEY, + 'background-image: url(javascript:evil());' + + 'background: url(javascript:evil());' + + 'border-image: url(javascript:evil());' + + 'filter: url(javascript:evil());' + + 'list-style: url(javascript:evil());' + + 'list-style-image: url(javascript:evil());' + + 'clip-path: url(javascript:evil());' + + 'width: url(javascript:evil())' // should not sanitize + , + null, true, false)) + .toEqual( + 'background-image: unsafe; ' + + 'background: unsafe; ' + + 'border-image: unsafe; ' + + 'filter: unsafe; ' + + 'list-style: unsafe; ' + + 'list-style-image: unsafe; ' + + 'clip-path: unsafe; ' + + 'width: url(javascript:evil())'); + }); + }); + }); +}); + +const empty_0_through_9 = [null, null, null, null, null, null, null, null, null, null]; +const empty_14_through_19 = [null, null, null, null, null, null]; + +function expectRange(tStylingRange: TStylingRange) { + return expect([ + getTStylingRangePrev(tStylingRange), // + getTStylingRangeNext(tStylingRange), // + ]); +} + +function expectTData(tData: TData) { + return expect(tData.map((tStylingRange: any) => { + return typeof tStylingRange === 'number' ? + [ + false, + getTStylingRangePrev(tStylingRange as any), // + false, + getTStylingRangeNext(tStylingRange as any), // + ] : + tStylingRange; + })); +} + +function expectPriorityOrder(tData: TData, tNode: TNode, isClassBinding: boolean) { + // first find head. + let index = getStylingBindingHead(tData, tNode, isClassBinding); + const indexes: [number, string | null, boolean, boolean][] = []; + while (index !== 0) { + let key = tData[index] as TStylingKey | null; + if (key !== null && typeof key === 'object') { + key = key.key; + } + const tStylingRange = tData[index + 1] as TStylingRange; + indexes.push([ + index, // + key as string, // + getTStylingRangePrevDuplicate(tStylingRange), // + getTStylingRangeNextDuplicate(tStylingRange), // + ]); + index = getTStylingRangeNext(tStylingRange); + } + return expect(indexes); +} + +class StylingFixture { + tData: TData = [null, null]; + lView: LView = [null, null !] as any; + tNode: TNode = createTNode(null !, null !, TNodeType.Element, 0, '', null); + constructor(bindingSources: TStylingKey[][], public isClassBinding: boolean) { + let bindingIndex = this.tData.length; + for (let i = 0; i < bindingSources.length; i++) { + const bindings = bindingSources[i]; + for (let j = 0; j < bindings.length; j++) { + const binding = bindings[j]; + insertTStylingBinding( + this.tData, this.tNode, binding, bindingIndex, i === 0, isClassBinding); + this.lView.push(null, null); + bindingIndex += 2; + } + } + } + + setBinding(index: number, value: any) { this.lView[index * 2 + 2] = value; } + + flush(index: number): string { + return flushStyleBinding( + this.tData, this.tNode, this.lView, index * 2 + 2, this.isClassBinding); + } +} \ No newline at end of file