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