refactor(ivy): add `insertTStylingBinding` to keep track of bindings. (#34004)

This adds `insertTStyleValue` but does not hook it up to anything yet.

The purpose of this function is to create a linked-list of styling
related bindings. The bindings can be traversed during flush.

The linked list also keeps track of duplicates. This is important
for binding to know if it needs to check other styles for reconciliation.

PR Close #34004
This commit is contained in:
Miško Hevery 2019-12-12 13:03:20 -08:00
parent 76698d38f7
commit 94504ff5c8
11 changed files with 1547 additions and 22 deletions

View File

@ -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 {

View File

@ -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,
};
}

View File

@ -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 */

View File

@ -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 `<div [style.color]="exp" [style.width]="exp">` 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: `<div [style]="map" [style.width]="width">`, 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: `<div [style]="map" [style.width]="width">`, 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;
}

View File

@ -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<any>| DirectiveDef<any>| ComponentDef<any>| number | Type<any>|
InjectionToken<any>| TI18n | I18nUpdateOpCodes | null | string)[];
(TNode | PipeDef<any>| DirectiveDef<any>| ComponentDef<any>| number | TStylingRange |
TStylingKey | Type<any>| InjectionToken<any>| TI18n | I18nUpdateOpCodes | null | string)[];
// Note: This hack is necessary so we don't erroneously get a circular dependency
// failure based on types.

View File

@ -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;
}
}

View File

@ -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: `
* <my-cmp [style]=" {color: '#001'} "
* [style.color]=" #002 "
* dir-style-color-1
* dir-style-color-2> `
* })
* 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<string, boolean|null>();
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<string, {old: string | null, new: string | null}>();
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;
}
};

View File

@ -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;
}
}
}

View File

@ -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, ` +

View File

@ -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<V>(map: Map<string, V>, fn: (value: V, key: string) => void): void {

View File

@ -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);
}
}