refactor(ivy): evaluate prop-based styling bindings with a new algorithm (#30469)

This is the first refactor PR designed to change how styling bindings
(i.e. `[style]` and `[class]`) behave in Ivy. Instead of having a heavy
element-by-element context be generated for each element, this new
refactor aims to use a single context for each `tNode` element that is
examined and iterated over when styling values are to be applied to the
element.

This patch brings this new functionality to prop-based bindings such as
`[style.prop]` and `[class.name]`.

PR Close #30469
This commit is contained in:
Matias Niemelä 2019-05-08 16:30:28 -07:00 committed by Jason Aden
parent 848e53efd0
commit f03475cac8
24 changed files with 1993 additions and 26 deletions

View File

@ -21,7 +21,7 @@
"master": {
"uncompressed": {
"runtime": 1440,
"main": 146225,
"main": 147764,
"polyfills": 43567
}
}

View File

@ -13,7 +13,6 @@ import {BindingForm, convertPropertyBinding} from '../../compiler_util/expressio
import {ConstantPool, DefinitionKind} from '../../constant_pool';
import * as core from '../../core';
import {AST, ParsedEvent, ParsedEventType, ParsedProperty} from '../../expression_parser/ast';
import {LifecycleHooks} from '../../lifecycle_reflector';
import {DEFAULT_INTERPOLATION_CONFIG} from '../../ml_parser/interpolation_config';
import * as o from '../../output/output_ast';
import {ParseError, ParseSourceSpan, typeSourceSpan} from '../../parse_util';
@ -725,6 +724,7 @@ function createHostBindingsFunction(
// the update block of a component/directive templateFn/hostBindingsFn so that the bindings
// are evaluated and updated for the element.
styleBuilder.buildUpdateLevelInstructions(getValueConverter()).forEach(instruction => {
totalHostVarsCount += instruction.allocateBindingSlots;
updateStatements.push(createStylingStmt(instruction, bindingContext, bindingFn));
});
}

View File

@ -15,6 +15,7 @@ import * as t from '../r3_ast';
import {Identifiers as R3} from '../r3_identifiers';
import {parse as parseStyle} from './style_parser';
import {compilerIsNewStylingInUse} from './styling_state';
import {ValueConverter} from './template';
const IMPORTANT_FLAG = '!important';
@ -389,6 +390,11 @@ export class StylingBuilder {
const bindingIndex: number = mapIndex.get(input.name !) !;
const value = input.value.visit(valueConverter);
totalBindingSlotsRequired += (value instanceof Interpolation) ? value.expressions.length : 0;
if (compilerIsNewStylingInUse()) {
// the old implementation does not reserve slot values for
// binding entries. The new one does.
totalBindingSlotsRequired++;
}
return {
sourceSpan: input.sourceSpan,
allocateBindingSlots: totalBindingSlotsRequired, reference,

View File

@ -0,0 +1,37 @@
/**
* @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
*/
/**
* A temporary enum of states that inform the core whether or not
* to defer all styling instruction calls to the old or new
* styling implementation.
*/
export const enum CompilerStylingMode {
UseOld = 0,
UseBothOldAndNew = 1,
UseNew = 2,
}
let _stylingMode = 0;
/**
* Temporary function used to inform the existing styling algorithm
* code to delegate all styling instruction calls to the new refactored
* styling code.
*/
export function compilerSetStylingMode(mode: CompilerStylingMode) {
_stylingMode = mode;
}
export function compilerIsNewStylingInUse() {
return _stylingMode > CompilerStylingMode.UseOld;
}
export function compilerAllowOldStyling() {
return _stylingMode < CompilerStylingMode.UseNew;
}

View File

@ -15,11 +15,10 @@ import {LQueries} from './interfaces/query';
import {RComment, RElement} from './interfaces/renderer';
import {StylingContext} from './interfaces/styling';
import {BINDING_INDEX, CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTENT_QUERIES, CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, INJECTOR, LView, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, SANITIZER, TVIEW, T_HOST} from './interfaces/view';
import {getTNode, unwrapRNode} from './util/view_utils';
function attachDebugObject(obj: any, debug: any) {
Object.defineProperty(obj, 'debug', {value: debug, enumerable: false});
}
import {runtimeIsNewStylingInUse} from './styling_next/state';
import {DebugStyling as DebugNewStyling, NodeStylingDebug} from './styling_next/styling_debug';
import {attachDebugObject} from './util/debug_utils';
import {getTNode, isStylingContext, unwrapRNode} from './util/view_utils';
/*
* This file contains conditionally attached classes which provide human readable (debug) level
@ -171,6 +170,8 @@ export class LViewDebug {
export interface DebugNode {
html: string|null;
native: Node;
styles: DebugNewStyling|null;
classes: DebugNewStyling|null;
nodes: DebugNode[]|null;
component: LViewDebug|null;
}
@ -188,12 +189,21 @@ export function toDebugNodes(tNode: TNode | null, lView: LView): DebugNode[]|nul
while (tNodeCursor) {
const rawValue = lView[tNode.index];
const native = unwrapRNode(rawValue);
const componentLViewDebug = toDebug(readLViewValue(rawValue));
const componentLViewDebug =
isStylingContext(rawValue) ? null : toDebug(readLViewValue(rawValue));
let styles: DebugNewStyling|null = null;
let classes: DebugNewStyling|null = null;
if (runtimeIsNewStylingInUse()) {
styles = tNode.newStyles ? new NodeStylingDebug(tNode.newStyles, lView) : null;
classes = tNode.newClasses ? new NodeStylingDebug(tNode.newClasses, lView) : null;
}
debugNodes.push({
html: toHtml(native),
native: native as any,
native: native as any, styles, classes,
nodes: toDebugNodes(tNode.child, lView),
component: componentLViewDebug
component: componentLViewDebug,
});
tNodeCursor = tNodeCursor.next;
}

View File

@ -21,6 +21,8 @@ import {applyOnCreateInstructions} from '../node_util';
import {decreaseElementDepthCount, getElementDepthCount, getIsParent, getLView, getPreviousOrParentTNode, getSelectedIndex, increaseElementDepthCount, setIsParent, setPreviousOrParentTNode} from '../state';
import {getInitialClassNameValue, getInitialStyleStringValue, initializeStaticContext, patchContextWithStaticAttrs, renderInitialClasses, renderInitialStyles} from '../styling/class_and_style_bindings';
import {getStylingContextFromLView, hasClassInput, hasStyleInput} from '../styling/util';
import {registerInitialStylingIntoContext} from '../styling_next/instructions';
import {runtimeIsNewStylingInUse} from '../styling_next/state';
import {NO_CHANGE} from '../tokens';
import {attrsStylingIndexOf, setUpAttributes} from '../util/attrs_utils';
import {renderStringify} from '../util/misc_utils';
@ -63,8 +65,9 @@ export function ΔelementStart(
let initialStylesIndex = 0;
let initialClassesIndex = 0;
let lastAttrIndex = -1;
if (attrs) {
const lastAttrIndex = setUpAttributes(native, attrs);
lastAttrIndex = setUpAttributes(native, attrs);
// it's important to only prepare styling-related datastructures once for a given
// tNode and not each time an element is created. Also, the styling code is designed
@ -116,6 +119,10 @@ export function ΔelementStart(
renderInitialStyles(native, tNode.stylingTemplate, renderer, initialStylesIndex);
}
if (runtimeIsNewStylingInUse() && lastAttrIndex >= 0) {
registerInitialStylingIntoContext(tNode, attrs as TAttributes, lastAttrIndex);
}
const currentQueries = lView[QUERIES];
if (currentQueries) {
currentQueries.addNode(tNode);

View File

@ -777,6 +777,10 @@ export function createTNode(
stylingTemplate: null,
projection: null,
onElementCreationFns: null,
// TODO (matsko): rename this to `styles` once the old styling impl is gone
newStyles: null,
// TODO (matsko): rename this to `classes` once the old styling impl is gone
newClasses: null,
};
}

View File

@ -17,6 +17,9 @@ import {BoundPlayerFactory} from '../styling/player_factory';
import {DEFAULT_TEMPLATE_DIRECTIVE_INDEX} from '../styling/shared';
import {getCachedStylingContext, setCachedStylingContext} from '../styling/state';
import {allocateOrUpdateDirectiveIntoContext, createEmptyStylingContext, forceClassesAsString, forceStylesAsString, getStylingContextFromLView, hasClassInput, hasStyleInput} from '../styling/util';
import {classProp as newClassProp, styleProp as newStyleProp, stylingApply as newStylingApply, stylingInit as newStylingInit} from '../styling_next/instructions';
import {runtimeAllowOldStyling, runtimeIsNewStylingInUse} from '../styling_next/state';
import {getBindingNameFromIndex} from '../styling_next/util';
import {NO_CHANGE} from '../tokens';
import {renderStringify} from '../util/misc_utils';
import {getRootContext} from '../util/view_traversal_utils';
@ -73,6 +76,13 @@ export function Δstyling(
const directiveStylingIndex = getActiveDirectiveStylingIndex();
if (directiveStylingIndex) {
// this is temporary hack to get the existing styling instructions to
// play ball with the new refactored implementation.
// TODO (matsko): remove this once the old implementation is not needed.
if (runtimeIsNewStylingInUse()) {
newStylingInit();
}
// despite the binding being applied in a queue (below), the allocation
// of the directive into the context happens right away. The reason for
// this is to retain the ordering of the directives (which is important
@ -81,7 +91,7 @@ export function Δstyling(
const fns = tNode.onElementCreationFns = tNode.onElementCreationFns || [];
fns.push(() => {
initstyling(
initStyling(
tNode, classBindingNames, styleBindingNames, styleSanitizer, directiveStylingIndex);
registerHostDirective(tNode.stylingTemplate !, directiveStylingIndex);
});
@ -92,13 +102,13 @@ export function Δstyling(
// components) then they will be applied at the end of the `elementEnd`
// instruction (because directives are created first before styling is
// executed for a new element).
initstyling(
initStyling(
tNode, classBindingNames, styleBindingNames, styleSanitizer,
DEFAULT_TEMPLATE_DIRECTIVE_INDEX);
}
}
function initstyling(
function initStyling(
tNode: TNode, classBindingNames: string[] | null | undefined,
styleBindingNames: string[] | null | undefined,
styleSanitizer: StyleSanitizeFn | null | undefined, directiveStylingIndex: number): void {
@ -148,6 +158,15 @@ export function ΔstyleProp(
updatestyleProp(
stylingContext, styleIndex, valueToAdd, DEFAULT_TEMPLATE_DIRECTIVE_INDEX, forceOverride);
}
if (runtimeIsNewStylingInUse()) {
const prop = getBindingNameFromIndex(stylingContext, styleIndex, directiveStylingIndex, false);
// the reason why we cast the value as `boolean` is
// because the new styling refactor does not yet support
// sanitization or animation players.
newStyleProp(prop, value as string | number, suffix);
}
}
function resolveStylePropValue(
@ -206,6 +225,15 @@ export function ΔclassProp(
updateclassProp(
stylingContext, classIndex, input, DEFAULT_TEMPLATE_DIRECTIVE_INDEX, forceOverride);
}
if (runtimeIsNewStylingInUse()) {
const prop = getBindingNameFromIndex(stylingContext, classIndex, directiveStylingIndex, true);
// the reason why we cast the value as `boolean` is
// because the new styling refactor does not yet support
// sanitization or animation players.
newClassProp(prop, input as boolean);
}
}
@ -324,11 +352,14 @@ export function ΔstylingApply(): void {
const renderer = tNode.type === TNodeType.Element ? lView[RENDERER] : null;
const isFirstRender = (lView[FLAGS] & LViewFlags.FirstLViewPass) !== 0;
const stylingContext = getStylingContext(index, lView);
const totalPlayersQueued = renderStyling(
stylingContext, renderer, lView, isFirstRender, null, null, directiveStylingIndex);
if (totalPlayersQueued > 0) {
const rootContext = getRootContext(lView);
scheduleTick(rootContext, RootContextFlags.FlushPlayers);
if (runtimeAllowOldStyling()) {
const totalPlayersQueued = renderStyling(
stylingContext, renderer, lView, isFirstRender, null, null, directiveStylingIndex);
if (totalPlayersQueued > 0) {
const rootContext = getRootContext(lView);
scheduleTick(rootContext, RootContextFlags.FlushPlayers);
}
}
// because select(n) may not run between every instruction, the cached styling
@ -339,6 +370,10 @@ export function ΔstylingApply(): void {
// cleared because there is no code in Angular that applies more styling code after a
// styling flush has occurred. Note that this will be fixed once FW-1254 lands.
setCachedStylingContext(null);
if (runtimeIsNewStylingInUse()) {
newStylingApply();
}
}
export function getActiveDirectiveStylingIndex() {

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 {TStylingContext} from '../styling_next/interfaces';
import {CssSelector} from './projection';
import {RNode} from './renderer';
import {StylingContext} from './styling';
@ -438,6 +438,10 @@ export interface TNode {
* with functions each time the creation block is called.
*/
onElementCreationFns: Function[]|null;
// TODO (matsko): rename this to `styles` once the old styling impl is gone
newStyles: TStylingContext|null;
// TODO (matsko): rename this to `classes` once the old styling impl is gone
newClasses: TStylingContext|null;
}
/** Static data for an element */

View File

@ -6,10 +6,11 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ΔdefineInjectable, ΔdefineInjector,} from '../../di/interface/defs';
import {Δinject} from '../../di/injector_compatibility';
import * as r3 from '../index';
import {ΔdefineInjectable, ΔdefineInjector} from '../../di/interface/defs';
import * as sanitization from '../../sanitization/sanitization';
import * as r3 from '../index';
/**

View File

@ -245,6 +245,27 @@ export function adjustActiveDirectiveSuperClassDepthPosition(delta: number) {
Math.max(activeDirectiveSuperClassHeight, activeDirectiveSuperClassDepthPosition);
}
/**
* Returns he current depth of the super/sub class inheritance chain.
*
* This will return how many inherited directive/component classes
* exist in the current chain.
*
* ```typescript
* @Directive({ selector: '[super-dir]' })
* class SuperDir {}
*
* @Directive({ selector: '[sub-dir]' })
* class SubDir extends SuperDir {}
*
* // if `<div sub-dir>` is used then the super class height is `1`
* // if `<div super-dir>` is used then the super class height is `0`
* ```
*/
export function getActiveDirectiveSuperClassHeight() {
return activeDirectiveSuperClassHeight;
}
/**
* Returns the current super class (reverse inheritance) depth for a directive.
*

View File

@ -1636,7 +1636,7 @@ function diffSummaryValues(result: any[], name: string, prop: string, a: any, b:
}
}
function getSinglePropIndexValue(
export function getSinglePropIndexValue(
context: StylingContext, directiveIndex: number, offset: number, isClassBased: boolean) {
const singlePropOffsetRegistryIndex =
context[StylingIndex.DirectiveRegistryPosition]

View File

@ -0,0 +1,356 @@
/**
* @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 {ProceduralRenderer3, RElement, Renderer3, RendererStyleFlags3, isProceduralRenderer} from '../interfaces/renderer';
import {ApplyStylingFn, StylingBindingData, TStylingContext, TStylingContextIndex} from './interfaces';
import {allowStylingFlush, getGuardMask, getProp, getValue, getValuesCount, isContextLocked, lockContext} from './util';
/**
* This file contains the core logic for styling in Angular.
*
* All styling bindings (i.e. `[style]`, `[style.prop]`, `[class]` and `[class.name]`)
* will have their values be applied through the logic in this file.
*
* When a binding is encountered (e.g. `<div [style.width]="w">`) then
* the binding data will be populated into a `TStylingContext` data-structure.
* There is only one `TStylingContext` per `TNode` and each element instance
* will update its style/class binding values in concert with the styling
* context.
*
* To learn more about the algorithm see `TStylingContext`.
*/
// the values below are global to all styling code below. Each value
// will either increment or mutate each time a styling instruction is
// executed. Do not modify the values below.
let currentStyleIndex = 0;
let currentClassIndex = 0;
let stylesBitMask = 0;
let classesBitMask = 0;
let deferredBindingQueue: (TStylingContext | number | string | null)[] = [];
const DEFAULT_BINDING_VALUE = null;
const DEFAULT_SIZE_VALUE = 1;
const DEFAULT_MASK_VALUE = 0;
export const DEFAULT_BINDING_INDEX_VALUE = -1;
export const BIT_MASK_APPLY_ALL = -1;
/**
* Visits a class-based binding and updates the new value (if changed).
*
* This function is called each time a class-based styling instruction
* is executed. It's important that it's always called (even if the value
* has not changed) so that the inner counter index value is incremented.
* This way, each instruction is always guaranteed to get the same counter
* state each time its called (which then allows the `TStylingContext`
* and the bit mask values to be in sync).
*/
export function updateClassBinding(
context: TStylingContext, data: StylingBindingData, prop: string, bindingIndex: number,
value: boolean | null | undefined, deferRegistration: boolean): void {
const index = currentClassIndex++;
if (updateBindingData(context, data, index, prop, bindingIndex, value, deferRegistration)) {
classesBitMask |= 1 << index;
}
}
/**
* Visits a style-based binding and updates the new value (if changed).
*
* This function is called each time a style-based styling instruction
* is executed. It's important that it's always called (even if the value
* has not changed) so that the inner counter index value is incremented.
* This way, each instruction is always guaranteed to get the same counter
* state each time its called (which then allows the `TStylingContext`
* and the bit mask values to be in sync).
*/
export function updateStyleBinding(
context: TStylingContext, data: StylingBindingData, prop: string, bindingIndex: number,
value: String | string | number | null | undefined, deferRegistration: boolean): void {
const index = currentStyleIndex++;
if (updateBindingData(context, data, index, prop, bindingIndex, value, deferRegistration)) {
stylesBitMask |= 1 << index;
}
}
function updateBindingData(
context: TStylingContext, data: StylingBindingData, counterIndex: number, prop: string,
bindingIndex: number, value: string | String | number | boolean | null | undefined,
deferRegistration?: boolean): boolean {
if (!isContextLocked(context)) {
if (deferRegistration) {
deferBindingRegistration(context, counterIndex, prop, bindingIndex);
} else {
deferredBindingQueue.length && flushDeferredBindings();
// this will only happen during the first update pass of the
// context. The reason why we can't use `tNode.firstTemplatePass`
// here is because its not guaranteed to be true when the first
// update pass is executed (remember that all styling instructions
// are run in the update phase, and, as a result, are no more
// styling instructions that are run in the creation phase).
registerBinding(context, counterIndex, prop, bindingIndex);
}
}
if (data[bindingIndex] !== value) {
data[bindingIndex] = value;
return true;
}
return false;
}
/**
* Schedules a binding registration to be run at a later point.
*
* The reasoning for this feature is to ensure that styling
* bindings are registered in the correct order for when
* directives/components have a super/sub class inheritance
* chains. Each directive's styling bindings must be
* registered into the context in reverse order. Therefore all
* bindings will be buffered in reverse order and then applied
* after the inheritance chain exits.
*/
function deferBindingRegistration(
context: TStylingContext, counterIndex: number, prop: string, bindingIndex: number) {
deferredBindingQueue.splice(0, 0, context, counterIndex, prop, bindingIndex);
}
/**
* Flushes the collection of deferred bindings and causes each entry
* to be registered into the context.
*/
function flushDeferredBindings() {
let i = 0;
while (i < deferredBindingQueue.length) {
const context = deferredBindingQueue[i++] as TStylingContext;
const count = deferredBindingQueue[i++] as number;
const prop = deferredBindingQueue[i++] as string;
const bindingIndex = deferredBindingQueue[i++] as number | null;
registerBinding(context, count, prop, bindingIndex);
}
deferredBindingQueue.length = 0;
}
/**
* Registers the provided binding (prop + bindingIndex) into the context.
*
* This function is shared between bindings that are assigned immediately
* (via `updateBindingData`) and at a deferred stage. When called, it will
* figure out exactly where to place the binding data in the context.
*
* It is needed because it will either update or insert a styling property
* into the context at the correct spot.
*
* When called, one of two things will happen:
*
* 1) If the property already exists in the context then it will just add
* the provided `bindingValue` to the end of the binding sources region
* for that particular property.
*
* - If the binding value is a number then it will be added as a new
* binding index source next to the other binding sources for the property.
*
* - Otherwise, if the binding value is a string/boolean/null type then it will
* replace the default value for the property if the default value is `null`.
*
* 2) If the property does not exist then it will be inserted into the context.
* The styling context relies on all properties being stored in alphabetical
* order, so it knows exactly where to store it.
*
* When inserted, a default `null` value is created for the property which exists
* as the default value for the binding. If the bindingValue property is inserted
* and it is either a string, number or null value then that will replace the default
* value.
*/
export function registerBinding(
context: TStylingContext, countId: number, prop: string,
bindingValue: number | null | string | boolean) {
let i = TStylingContextIndex.ValuesStartPosition;
let found = false;
while (i < context.length) {
const valuesCount = getValuesCount(context, i);
const p = getProp(context, i);
found = prop <= p;
if (found) {
// all style/class bindings are sorted by property name
if (prop < p) {
allocateNewContextEntry(context, i, prop);
}
addBindingIntoContext(context, i, bindingValue, countId);
break;
}
i += TStylingContextIndex.BindingsStartOffset + valuesCount;
}
if (!found) {
allocateNewContextEntry(context, context.length, prop);
addBindingIntoContext(context, i, bindingValue, countId);
}
}
function allocateNewContextEntry(context: TStylingContext, index: number, prop: string) {
context.splice(index, 0, DEFAULT_MASK_VALUE, DEFAULT_SIZE_VALUE, prop, DEFAULT_BINDING_VALUE);
}
/**
* Inserts a new binding value into a styling property tuple in the `TStylingContext`.
*
* A bindingValue is inserted into a context during the first update pass
* of a template or host bindings function. When this occurs, two things
* happen:
*
* - If the bindingValue value is a number then it is treated as a bindingIndex
* value (a index in the `LView`) and it will be inserted next to the other
* binding index entries.
*
* - Otherwise the binding value will update the default value for the property
* and this will only happen if the default value is `null`.
*/
function addBindingIntoContext(
context: TStylingContext, index: number, bindingValue: number | string | boolean | null,
countId: number) {
const valuesCount = getValuesCount(context, index);
// -1 is used because we want the last value that's in the list (not the next slot)
const lastValueIndex = index + TStylingContextIndex.BindingsStartOffset + valuesCount - 1;
if (typeof bindingValue === 'number') {
context.splice(lastValueIndex, 0, bindingValue);
(context[index + TStylingContextIndex.ValuesCountOffset] as number)++;
(context[index + TStylingContextIndex.MaskOffset] as number) |= 1 << countId;
} else if (typeof bindingValue === 'string' && context[lastValueIndex] == null) {
context[lastValueIndex] = bindingValue;
}
}
/**
* Applies all class entries in the provided context to the provided element.
*/
export function applyClasses(
renderer: Renderer3 | ProceduralRenderer3 | null, data: StylingBindingData,
context: TStylingContext, element: RElement, directiveIndex: number) {
if (allowStylingFlush(context, directiveIndex)) {
const isFirstPass = isContextLocked(context);
isFirstPass && lockContext(context);
applyStyling(context, renderer, element, data, classesBitMask, setClass, isFirstPass);
currentClassIndex = 0;
classesBitMask = 0;
}
}
/**
* Applies all style entries in the provided context to the provided element.
*/
export function applyStyles(
renderer: Renderer3 | ProceduralRenderer3 | null, data: StylingBindingData,
context: TStylingContext, element: RElement, directiveIndex: number) {
if (allowStylingFlush(context, directiveIndex)) {
const isFirstPass = isContextLocked(context);
isFirstPass && lockContext(context);
applyStyling(context, renderer, element, data, stylesBitMask, setStyle, isFirstPass);
currentStyleIndex = 0;
stylesBitMask = 0;
}
}
/**
* Runs through the provided styling context and applies each value to
* the provided element (via the renderer) if one or more values are present.
*
* Note that this function is not designed to be called in isolation (use
* `applyClasses` and `applyStyles` to actually apply styling values).
*/
export function applyStyling(
context: TStylingContext, renderer: Renderer3 | ProceduralRenderer3 | null, element: RElement,
bindingData: StylingBindingData, bitMask: number, applyStylingFn: ApplyStylingFn,
forceApplyDefaultValues?: boolean) {
deferredBindingQueue.length && flushDeferredBindings();
if (bitMask) {
let processAllEntries = bitMask === BIT_MASK_APPLY_ALL;
let i = TStylingContextIndex.ValuesStartPosition;
while (i < context.length) {
const valuesCount = getValuesCount(context, i);
const guardMask = getGuardMask(context, i);
// the guard mask value is non-zero if and when
// there are binding values present for the property.
// If there are ONLY static values (i.e. `style="prop:val")
// then the guard value will stay as zero.
const processEntry =
processAllEntries || (guardMask ? (bitMask & guardMask) : forceApplyDefaultValues);
if (processEntry) {
const prop = getProp(context, i);
const limit = valuesCount - 1;
for (let j = 0; j <= limit; j++) {
const isFinalValue = j === limit;
const bindingValue = getValue(context, i, j);
const bindingIndex =
isFinalValue ? DEFAULT_BINDING_INDEX_VALUE : (bindingValue as number);
const valueToApply: string|null = isFinalValue ? bindingValue : bindingData[bindingIndex];
if (isValueDefined(valueToApply) || isFinalValue) {
applyStylingFn(renderer, element, prop, valueToApply, bindingIndex);
break;
}
}
}
i += TStylingContextIndex.BindingsStartOffset + valuesCount;
}
}
}
function isValueDefined(value: any) {
// the reason why null is compared against is because
// a CSS class value that is set to `false` must be
// respected (otherwise it would be treated as falsy).
// Empty string values are because developers usually
// set a value to an empty string to remove it.
return value != null && value !== '';
}
/**
* Assigns a style value to a style property for the given element.
*/
const setStyle: ApplyStylingFn =
(renderer: Renderer3 | null, native: any, prop: string, value: string | null) => {
if (value) {
// opacity, z-index and flexbox all have number values
// and these need to be converted into strings so that
// they can be assigned properly.
value = value.toString();
ngDevMode && ngDevMode.rendererSetStyle++;
renderer && isProceduralRenderer(renderer) ?
renderer.setStyle(native, prop, value, RendererStyleFlags3.DashCase) :
native.style.setProperty(prop, value);
} else {
ngDevMode && ngDevMode.rendererRemoveStyle++;
renderer && isProceduralRenderer(renderer) ?
renderer.removeStyle(native, prop, RendererStyleFlags3.DashCase) :
native.style.removeProperty(prop);
}
};
/**
* Adds/removes the provided className value to the provided element.
*/
const setClass: ApplyStylingFn =
(renderer: Renderer3 | null, native: any, className: string, value: any) => {
if (className !== '') {
if (value) {
ngDevMode && ngDevMode.rendererAddClass++;
renderer && isProceduralRenderer(renderer) ? renderer.addClass(native, className) :
native.classList.add(className);
} else {
ngDevMode && ngDevMode.rendererRemoveClass++;
renderer && isProceduralRenderer(renderer) ? renderer.removeClass(native, className) :
native.classList.remove(className);
}
}
};

View File

@ -0,0 +1,211 @@
/**
* @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 {LContainer} from '../interfaces/container';
import {AttributeMarker, TAttributes, TNode, TNodeType} from '../interfaces/node';
import {RElement} from '../interfaces/renderer';
import {StylingContext as OldStylingContext, StylingIndex as OldStylingIndex} from '../interfaces/styling';
import {BINDING_INDEX, HEADER_OFFSET, HOST, LView, RENDERER} from '../interfaces/view';
import {getActiveDirectiveId, getActiveDirectiveSuperClassDepth, getActiveDirectiveSuperClassHeight, getLView, getSelectedIndex} from '../state';
import {getTNode, isStylingContext as isOldStylingContext} from '../util/view_utils';
import {applyClasses, applyStyles, registerBinding, updateClassBinding, updateStyleBinding} from './bindings';
import {TStylingContext} from './interfaces';
import {attachStylingDebugObject} from './styling_debug';
import {allocStylingContext, updateContextDirectiveIndex} from './util';
/**
* This file contains the core logic for how styling instructions are processed in Angular.
*
* To learn more about the algorithm see `TStylingContext`.
*/
/**
* Temporary function to bridge styling functionality between this new
* refactor (which is here inside of `styling_next/`) and the old
* implementation (which lives inside of `styling/`).
*
* This function is executed during the creation block of an element.
* Because the existing styling implementation issues a call to the
* `styling()` instruction, this instruction will also get run. The
* central idea here is that the directive index values are bound
* into the context. The directive index is temporary and is only
* required until the `select(n)` instruction is fully functional.
*/
export function stylingInit() {
const lView = getLView();
const index = getSelectedIndex();
const tNode = getTNode(index, lView);
updateLastDirectiveIndex(tNode, getActiveDirectiveStylingIndex());
}
/**
* Mirror implementation of the `styleProp()` instruction (found in `instructions/styling.ts`).
*/
export function styleProp(
prop: string, value: string | number | String | null, suffix?: string | null): void {
const index = getSelectedIndex();
const lView = getLView();
const bindingIndex = lView[BINDING_INDEX]++;
const tNode = getTNode(index, lView);
const tContext = getStylesContext(tNode);
const defer = getActiveDirectiveSuperClassHeight() > 0;
updateStyleBinding(tContext, lView, prop, bindingIndex, value, defer);
}
/**
* Mirror implementation of the `classProp()` instruction (found in `instructions/styling.ts`).
*/
export function classProp(className: string, value: boolean | null): void {
const index = getSelectedIndex();
const lView = getLView();
const bindingIndex = lView[BINDING_INDEX]++;
const tNode = getTNode(index, lView);
const tContext = getClassesContext(tNode);
const defer = getActiveDirectiveSuperClassHeight() > 0;
updateClassBinding(tContext, lView, className, bindingIndex, value, defer);
}
/**
* Temporary function to bridge styling functionality between this new
* refactor (which is here inside of `styling_next/`) and the old
* implementation (which lives inside of `styling/`).
*
* The new styling refactor ensures that styling flushing is called
* automatically when a template function exits or a follow-up element
* is visited (i.e. when `select(n)` is called). Because the `select(n)`
* instruction is not fully implemented yet (it doesn't actually execute
* host binding instruction code at the right time), this means that a
* styling apply function is still needed.
*
* This function is a mirror implementation of the `stylingApply()`
* instruction (found in `instructions/styling.ts`).
*/
export function stylingApply() {
const index = getSelectedIndex();
const lView = getLView();
const tNode = getTNode(index, lView);
const renderer = getRenderer(tNode, lView);
const native = getNativeFromLView(index, lView);
const directiveIndex = getActiveDirectiveStylingIndex();
applyClasses(renderer, lView, getClassesContext(tNode), native, directiveIndex);
applyStyles(renderer, lView, getStylesContext(tNode), native, directiveIndex);
}
function getStylesContext(tNode: TNode): TStylingContext {
return getContext(tNode, false);
}
function getClassesContext(tNode: TNode): TStylingContext {
return getContext(tNode, true);
}
/**
* Returns/instantiates a styling context from/to a `tNode` instance.
*/
function getContext(tNode: TNode, isClassBased: boolean) {
let context = isClassBased ? tNode.newClasses : tNode.newStyles;
if (!context) {
context = allocStylingContext();
if (ngDevMode) {
attachStylingDebugObject(context);
}
if (isClassBased) {
tNode.newClasses = context;
} else {
tNode.newStyles = context;
}
}
return context;
}
/**
* Temporary function to bridge styling functionality between this new
* refactor (which is here inside of `styling_next/`) and the old
* implementation (which lives inside of `styling/`).
*
* The purpose of this function is to traverse through the LView data
* for a specific element index and return the native node. Because the
* current implementation relies on there being a styling context array,
* the code below will need to loop through these array values until it
* gets a native element node.
*
* Note that this code is temporary and will disappear once the new
* styling refactor lands in its entirety.
*/
function getNativeFromLView(index: number, viewData: LView): RElement {
let storageIndex = index + HEADER_OFFSET;
let slotValue: LContainer|LView|OldStylingContext|RElement = viewData[storageIndex];
let wrapper: LContainer|LView|OldStylingContext = viewData;
while (Array.isArray(slotValue)) {
wrapper = slotValue;
slotValue = slotValue[HOST] as LView | OldStylingContext | RElement;
}
if (isOldStylingContext(wrapper)) {
return wrapper[OldStylingIndex.ElementPosition] as RElement;
} else {
return slotValue;
}
}
function getRenderer(tNode: TNode, lView: LView) {
return tNode.type === TNodeType.Element ? lView[RENDERER] : null;
}
/**
* Searches and assigns provided all static style/class entries (found in the `attrs` value)
* and registers them in their respective styling contexts.
*/
export function registerInitialStylingIntoContext(
tNode: TNode, attrs: TAttributes, startIndex: number) {
let classesContext !: TStylingContext;
let stylesContext !: TStylingContext;
let mode = -1;
for (let i = startIndex; i < attrs.length; i++) {
const attr = attrs[i];
if (typeof attr == 'number') {
mode = attr;
} else if (mode == AttributeMarker.Classes) {
classesContext = classesContext || getClassesContext(tNode);
registerBinding(classesContext, -1, attr as string, true);
} else if (mode == AttributeMarker.Styles) {
stylesContext = stylesContext || getStylesContext(tNode);
registerBinding(stylesContext, -1, attr as string, attrs[++i] as string);
}
}
}
/**
* Mirror implementation of the same function found in `instructions/styling.ts`.
*/
export function getActiveDirectiveStylingIndex(): number {
// whenever a directive's hostBindings function is called a uniqueId value
// is assigned. Normally this is enough to help distinguish one directive
// from another for the styling context, but there are situations where a
// sub-class directive could inherit and assign styling in concert with a
// parent directive. To help the styling code distinguish between a parent
// sub-classed directive the inheritance depth is taken into account as well.
return getActiveDirectiveId() + getActiveDirectiveSuperClassDepth();
}
/**
* Temporary function that will update the max directive index value in
* both the classes and styles contexts present on the provided `tNode`.
*
* This code is only used because the `select(n)` code functionality is not
* yet 100% functional. The `select(n)` instruction cannot yet evaluate host
* bindings function code in sync with the associated template function code.
* For this reason the styling algorithm needs to track the last directive index
* value so that it knows exactly when to render styling to the element since
* `stylingApply()` is called multiple times per CD (`stylingApply` will be
* removed once `select(n)` is fixed).
*/
function updateLastDirectiveIndex(tNode: TNode, directiveIndex: number) {
updateContextDirectiveIndex(getClassesContext(tNode), directiveIndex);
updateContextDirectiveIndex(getStylesContext(tNode), directiveIndex);
}

View File

@ -0,0 +1,239 @@
/**
* @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 {ProceduralRenderer3, RElement, Renderer3} from '../interfaces/renderer';
import {LView} from '../interfaces/view';
/**
* A static-level representation of all style or class bindings/values
* associated with a `TNode`.
*
* The `TStylingContext` unites all template styling bindings (i.e.
* `[class]` and `[style]` bindings) as well as all host-level
* styling bindings (for components and directives) together into
* a single manifest. It is used each time there are one or more
* styling bindings present for an element.
*
* The styling context is stored on a `TNode` on and there are
* two instances of it: one for classes and another for styles.
*
* ```typescript
* tNode.styles = [ ... a context only for styles ... ];
* tNode.classes = [ ... a context only for classes ... ];
* ```
*
* Due to the fact the the `TStylingContext` is stored on a `TNode`
* this means that all data within the context is static. Instead of
* storing actual styling binding values, the lView binding index values
* are stored within the context. (static nature means it is more compact.)
*
* ```typescript
* // <div [class.active]="c" // lView binding index = 20
* // [style.width]="x" // lView binding index = 21
* // [style.height]="y"> // lView binding index = 22
* tNode.stylesContext = [
* 0, // the context config value
*
* 0b001, // guard mask for width
* 2, // total entries for width
* 'width', // the property name
* 21, // the binding location for the "x" binding in the lView
* null,
*
* 0b010, // guard mask for height
* 2, // total entries for height
* 'height', // the property name
* 22, // the binding location for the "y" binding in the lView
* null,
* ];
*
* tNode.classesContext = [
* 0, // the context config value
*
* 0b001, // guard mask for active
* 2, // total entries for active
* 'active', // the property name
* 20, // the binding location for the "c" binding in the lView
* null,
* ];
* ```
*
* Entry value present in an entry (called a tuple) within the
* styling context is as follows:
*
* ```typescript
* context = [
* CONFIG, // the styling context config value
* //...
* guardMask,
* totalEntries,
* propName,
* bindingIndices...,
* defaultValue
* ];
* ```
*
* Below is a breakdown of each value:
*
* - **guardMask**:
* A numeric value where each bit represents a binding index
* location. Each binding index location is assigned based on
* a local counter value that increments each time an instruction
* is called:
*
* ```
* <div [style.width]="x" // binding index = 21 (counter index = 0)
* [style.height]="y"> // binding index = 22 (counter index = 1)
* ```
*
* In the example code above, if the `width` value where to change
* then the first bit in the local bit mask value would be flipped
* (and the second bit for when `height`).
*
* If and when there are more than 32 binding sources in the context
* (more than 32 `[style/class]` bindings) then the bit masking will
* overflow and we are left with a situation where a `-1` value will
* represent the bit mask. Due to the way that JavaScript handles
* negative values, when the bit mask is `-1` then all bits within
* that value will be automatically flipped (this is a quick and
* efficient way to flip all bits on the mask when a special kind
* of caching scenario occurs or when there are more than 32 bindings).
*
* - **totalEntries**:
* Each property present in the contains various binding sources of
* where the styling data could come from. This includes template
* level bindings, directive/component host bindings as well as the
* default value (or static value) all writing to the same property.
* This value depicts how many binding source entries exist for the
* property.
*
* The reason why the totalEntries value is needed is because the
* styling context is dynamic in size and it's not possible
* for the flushing or update algorithms to know when and where
* a property starts and ends without it.
*
* - **propName**:
* The CSS property name or class name (e.g `width` or `active`).
*
* - **bindingIndices...**:
* A series of numeric binding values that reflect where in the
* lView to find the style/class values associated with the property.
* Each value is in order in terms of priority (templates are first,
* then directives and then components). When the context is flushed
* and the style/class values are applied to the element (this happens
* inside of the `stylingApply` instruction) then the flushing code
* will keep checking each binding index against the associated lView
* to find the first style/class value that is non-null.
*
* - **defaultValue**:
* This is the default that will always be applied to the element if
* and when all other binding sources return a result that is null.
* Usually this value is null but it can also be a static value that
* is intercepted when the tNode is first constructured (e.g.
* `<div style="width:200px">` has a default value of `200px` for
* the `width` property).
*
* Each time a new binding is encountered it is registered into the
* context. The context then is continually updated until the first
* styling apply call has been called (this is triggered by the
* `stylingApply()` instruction for the active element).
*
* # How Styles/Classes are Applied
* Each time a styling instruction (e.g. `[class.name]`, `[style.prop]`,
* etc...) is executed, the associated `lView` for the view is updated
* at the current binding location. Also, when this happens, a local
* counter value is incremented. If the binding value has changed then
* a local `bitMask` variable is updated with the specific bit based
* on the counter value.
*
* Below is a lightweight example of what happens when a single style
* property is updated (i.e. `<div [style.prop]="val">`):
*
* ```typescript
* function updateStyleProp(prop: string, value: string) {
* const lView = getLView();
* const bindingIndex = BINDING_INDEX++;
* const indexForStyle = localStylesCounter++;
* if (lView[bindingIndex] !== value) {
* lView[bindingIndex] = value;
* localBitMaskForStyles |= 1 << indexForStyle;
* }
* }
* ```
*
* Once all the styling instructions have been evaluated, then the styling
* context(s) are flushed to the element. When this happens, the context will
* be iterated over (property by property) and each binding source will be
* examined and the first non-null value will be applied to the element.
*
*/
export interface TStylingContext extends Array<number|string|number|boolean|null> {
[TStylingContextIndex.ConfigPosition]: TStylingConfigFlags;
/* Temporary value used to track directive index entries until
the old styling code is fully removed. The reason why this
is required is to figure out which directive is last and,
when encountered, trigger a styling flush to happen */
[TStylingContextIndex.MaxDirectiveIndexPosition]: number;
}
/**
* A series of flags used to configure the config value present within a
* `TStylingContext` value.
*/
export const enum TStylingConfigFlags {
/**
* The initial state of the styling context config
*/
Initial = 0b0,
/**
* A flag which marks the context as being locked.
*
* The styling context is constructed across an element template
* function as well as any associated hostBindings functions. When
* this occurs, the context itself is open to mutation and only once
* it has been flushed once then it will be locked for good (no extra
* bindings can be added to it).
*/
Locked = 0b1,
}
/**
* An index of position and offset values used to natigate the `TStylingContext`.
*/
export const enum TStylingContextIndex {
ConfigPosition = 0,
MaxDirectiveIndexPosition = 1,
ValuesStartPosition = 2,
// each tuple entry in the context
// (mask, count, prop, ...bindings||default-value)
MaskOffset = 0,
ValuesCountOffset = 1,
PropOffset = 2,
BindingsStartOffset = 3,
}
/**
* A function used to apply or remove styling from an element for a given property.
*/
export interface ApplyStylingFn {
(renderer: Renderer3|ProceduralRenderer3|null, element: RElement, prop: string,
value: string|null, bindingIndex: number): void;
}
/**
* Runtime data type that is used to store binding data referenced from the `TStylingContext`.
*
* Because `LView` is just an array with data, there is no reason to
* special case `LView` everywhere in the styling algorithm. By allowing
* this data type to be an array that contains various scalar data types,
* an instance of `LView` doesn't need to be constructed for tests.
*/
export type StylingBindingData = LView | (string | number | boolean)[];

View File

@ -0,0 +1,37 @@
/**
* @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
*/
/**
* A temporary enum of states that inform the core whether or not
* to defer all styling instruction calls to the old or new
* styling implementation.
*/
export const enum RuntimeStylingMode {
UseOld = 0,
UseBothOldAndNew = 1,
UseNew = 2,
}
let _stylingMode = 0;
/**
* Temporary function used to inform the existing styling algorithm
* code to delegate all styling instruction calls to the new refactored
* styling code.
*/
export function runtimeSetStylingMode(mode: RuntimeStylingMode) {
_stylingMode = mode;
}
export function runtimeIsNewStylingInUse() {
return _stylingMode > RuntimeStylingMode.UseOld;
}
export function runtimeAllowOldStyling() {
return _stylingMode < RuntimeStylingMode.UseNew;
}

View File

@ -0,0 +1,210 @@
/**
* @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 {RElement} from '../interfaces/renderer';
import {attachDebugObject} from '../util/debug_utils';
import {BIT_MASK_APPLY_ALL, DEFAULT_BINDING_INDEX_VALUE, applyStyling} from './bindings';
import {StylingBindingData, TStylingContext, TStylingContextIndex} from './interfaces';
import {getDefaultValue, getGuardMask, getProp, getValuesCount, isContextLocked} from './util';
/**
* A debug/testing-oriented summary of a styling entry.
*
* A value such as this is generated as an artifact of the `DebugStyling`
* summary.
*/
export interface StylingSummary {
/** The style/class property that the summary is attached to */
prop: string;
/** The last applied value for the style/class property */
value: string|null;
/** The binding index of the last applied style/class property */
bindingIndex: number|null;
/** Every binding source that is writing the style/class property represented in this tuple */
sourceValues: {value: string | number | null, bindingIndex: number|null}[];
}
/**
* A debug/testing-oriented summary of all styling entries for a `DebugNode` instance.
*/
export interface DebugStyling {
/** The associated TStylingContext instance */
context: TStylingContext;
/**
* A summarization of each style/class property
* present in the context.
*/
summary: {[key: string]: StylingSummary}|null;
/**
* A key/value map of all styling properties and their
* runtime values.
*/
values: {[key: string]: string | number | null | boolean};
}
/**
* A debug/testing-oriented summary of all styling entries within a `TStylingContext`.
*/
export interface TStylingTupleSummary {
/** The property (style or class property) that this tuple represents */
prop: string;
/** The total amount of styling entries apart of this tuple */
valuesCount: number;
/**
* The bit guard mask that is used to compare and protect against
* styling changes when and styling bindings update
*/
guardMask: number;
/**
* The default value that will be applied if any bindings are falsy.
*/
defaultValue: string|boolean|null;
/**
* All bindingIndex sources that have been registered for this style.
*/
sources: (number|null|string)[];
}
/**
* Instantiates and attaches an instance of `TStylingContextDebug` to the provided context.
*/
export function attachStylingDebugObject(context: TStylingContext) {
const debug = new TStylingContextDebug(context);
attachDebugObject(context, debug);
return debug;
}
/**
* A human-readable debug summary of the styling data present within `TStylingContext`.
*
* This class is designed to be used within testing code or when an
* application has `ngDevMode` activated.
*/
class TStylingContextDebug {
constructor(public readonly context: TStylingContext) {}
get isLocked() { return isContextLocked(this.context); }
/**
* Returns a detailed summary of each styling entry in the context.
*
* See `TStylingTupleSummary`.
*/
get entries(): {[prop: string]: TStylingTupleSummary} {
const context = this.context;
const entries: {[prop: string]: TStylingTupleSummary} = {};
const start = TStylingContextIndex.ValuesStartPosition;
let i = start;
while (i < context.length) {
const prop = getProp(context, i);
const guardMask = getGuardMask(context, i);
const valuesCount = getValuesCount(context, i);
const defaultValue = getDefaultValue(context, i);
const bindingsStartPosition = i + TStylingContextIndex.BindingsStartOffset;
const sources: (number | string | null)[] = [];
for (let j = 0; j < valuesCount; j++) {
sources.push(context[bindingsStartPosition + j] as number | string | null);
}
entries[prop] = {prop, guardMask, valuesCount, defaultValue, sources};
i += TStylingContextIndex.BindingsStartOffset + valuesCount;
}
return entries;
}
}
/**
* A human-readable debug summary of the styling data present for a `DebugNode` instance.
*
* This class is designed to be used within testing code or when an
* application has `ngDevMode` activated.
*/
export class NodeStylingDebug implements DebugStyling {
private _contextDebug: TStylingContextDebug;
constructor(public context: TStylingContext, private _data: StylingBindingData) {
this._contextDebug = (this.context as any).debug as any;
}
/**
* Returns a detailed summary of each styling entry in the context and
* what their runtime representation is.
*
* See `StylingSummary`.
*/
get summary(): {[key: string]: StylingSummary} {
const contextEntries = this._contextDebug.entries;
const finalValues: {[key: string]: {value: string, bindingIndex: number}} = {};
this._mapValues((prop: string, value: any, bindingIndex: number) => {
finalValues[prop] = {value, bindingIndex};
});
const entries: {[key: string]: StylingSummary} = {};
const values = this.values;
const props = Object.keys(values);
for (let i = 0; i < props.length; i++) {
const prop = props[i];
const contextEntry = contextEntries[prop];
const sourceValues = contextEntry.sources.map(v => {
let value: string|number|null;
let bindingIndex: number|null;
if (typeof v === 'number') {
value = this._data[v];
bindingIndex = v;
} else {
value = v;
bindingIndex = null;
}
return {bindingIndex, value};
});
const finalValue = finalValues[prop] !;
let bindingIndex: number|null = finalValue.bindingIndex;
bindingIndex = bindingIndex === DEFAULT_BINDING_INDEX_VALUE ? null : bindingIndex;
entries[prop] = {prop, value: finalValue.value, bindingIndex, sourceValues};
}
return entries;
}
/**
* Returns a key/value map of all the styles/classes that were last applied to the element.
*/
get values(): {[key: string]: any} {
const entries: {[key: string]: any} = {};
this._mapValues((prop: string, value: any) => { entries[prop] = value; });
return entries;
}
private _mapValues(fn: (prop: string, value: any, bindingIndex: number) => any) {
// there is no need to store/track an element instance. The
// element is only used when the styling algorithm attempts to
// style the value (and we mock out the stylingApplyFn anyway).
const mockElement = {} as any;
const mapFn =
(renderer: any, element: RElement, prop: string, value: any, bindingIndex: number) => {
fn(prop, value, bindingIndex);
};
applyStyling(this.context, null, mockElement, this._data, BIT_MASK_APPLY_ALL, mapFn);
}
}

View File

@ -0,0 +1,82 @@
/**
* @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 {StylingContext} from '../interfaces/styling';
import {getProp as getOldProp, getSinglePropIndexValue as getOldSinglePropIndexValue} from '../styling/class_and_style_bindings';
import {TStylingConfigFlags, TStylingContext, TStylingContextIndex} from './interfaces';
/**
* Creates a new instance of the `TStylingContext`.
*/
export function allocStylingContext(): TStylingContext {
return [TStylingConfigFlags.Initial, 0];
}
/**
* Temporary function that allows for a string-based property name to be
* obtained from an index-based property identifier.
*
* This function will be removed once the new styling refactor code (which
* lives inside of `render3/styling_next/`) replaces the existing styling
* implementation.
*/
export function getBindingNameFromIndex(
stylingContext: StylingContext, offset: number, directiveIndex: number, isClassBased: boolean) {
const singleIndex =
getOldSinglePropIndexValue(stylingContext, directiveIndex, offset, isClassBased);
return getOldProp(stylingContext, singleIndex);
}
export function updateContextDirectiveIndex(context: TStylingContext, index: number) {
context[TStylingContextIndex.MaxDirectiveIndexPosition] = index;
}
function getConfig(context: TStylingContext) {
return context[TStylingContextIndex.ConfigPosition];
}
export function setConfig(context: TStylingContext, value: number) {
context[TStylingContextIndex.ConfigPosition] = value;
}
export function getProp(context: TStylingContext, index: number) {
return context[index + TStylingContextIndex.PropOffset] as string;
}
export function getGuardMask(context: TStylingContext, index: number) {
return context[index + TStylingContextIndex.MaskOffset] as number;
}
export function getValuesCount(context: TStylingContext, index: number) {
return context[index + TStylingContextIndex.ValuesCountOffset] as number;
}
export function getValue(context: TStylingContext, index: number, offset: number) {
return context[index + TStylingContextIndex.BindingsStartOffset + offset] as number | string;
}
export function getDefaultValue(context: TStylingContext, index: number): string|boolean|null {
const valuesCount = getValuesCount(context, index);
return context[index + TStylingContextIndex.BindingsStartOffset + valuesCount - 1] as string |
boolean | null;
}
/**
* Temporary function which determines whether or not a context is
* allowed to be flushed based on the provided directive index.
*/
export function allowStylingFlush(context: TStylingContext, index: number) {
return index === context[TStylingContextIndex.MaxDirectiveIndexPosition];
}
export function lockContext(context: TStylingContext) {
setConfig(context, getConfig(context) | TStylingConfigFlags.Locked);
}
export function isContextLocked(context: TStylingContext): boolean {
return (getConfig(context) & TStylingConfigFlags.Locked) > 0;
}

View File

@ -0,0 +1,10 @@
/**
* @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
*/
export function attachDebugObject(obj: any, debug: any) {
Object.defineProperty(obj, 'debug', {value: debug, enumerable: false});
}

View File

@ -0,0 +1,330 @@
/**
* @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 {CompilerStylingMode, compilerSetStylingMode} from '@angular/compiler/src/render3/view/styling_state';
import {Component, Directive, HostBinding, Input} from '@angular/core';
import {DebugNode, LViewDebug, toDebug} from '@angular/core/src/render3/debug';
import {RuntimeStylingMode, runtimeSetStylingMode} from '@angular/core/src/render3/styling_next/state';
import {loadLContextFromNode} from '@angular/core/src/render3/util/discovery_utils';
import {TestBed} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/src/matchers';
import {onlyInIvy} from '@angular/private/testing';
describe('new styling integration', () => {
beforeEach(() => {
runtimeSetStylingMode(RuntimeStylingMode.UseNew);
compilerSetStylingMode(CompilerStylingMode.UseNew);
});
afterEach(() => {
runtimeSetStylingMode(RuntimeStylingMode.UseOld);
compilerSetStylingMode(CompilerStylingMode.UseOld);
});
onlyInIvy('ivy resolves styling across directives, components and templates in unison')
.it('should apply single property styles/classes to the element and default to any static styling values',
() => {
@Component({
template: `
<div [style.width]="w"
[style.height]="h"
[style.opacity]="o"
style="width:200px; height:200px;"
[class.abc]="abc"
[class.xyz]="xyz"></div>
`
})
class Cmp {
w: string|null = '100px';
h: string|null = '100px';
o: string|null = '0.5';
abc = true;
xyz = false;
}
TestBed.configureTestingModule({declarations: [Cmp]});
const fixture = TestBed.createComponent(Cmp);
fixture.detectChanges();
const element = fixture.nativeElement.querySelector('div');
expect(element.style.width).toEqual('100px');
expect(element.style.height).toEqual('100px');
expect(element.style.opacity).toEqual('0.5');
expect(element.classList.contains('abc')).toBeTruthy();
expect(element.classList.contains('xyz')).toBeFalsy();
fixture.componentInstance.w = null;
fixture.componentInstance.h = null;
fixture.componentInstance.o = null;
fixture.componentInstance.abc = false;
fixture.componentInstance.xyz = true;
fixture.detectChanges();
expect(element.style.width).toEqual('200px');
expect(element.style.height).toEqual('200px');
expect(element.style.opacity).toBeFalsy();
expect(element.classList.contains('abc')).toBeFalsy();
expect(element.classList.contains('xyz')).toBeTruthy();
});
onlyInIvy('ivy resolves styling across directives, components and templates in unison')
.it('should apply single style/class across the template and directive host bindings', () => {
@Directive({selector: '[dir-that-sets-width]'})
class DirThatSetsWidthDirective {
@Input('dir-that-sets-width') @HostBinding('style.width') public width: string = '';
}
@Directive({selector: '[another-dir-that-sets-width]', host: {'[style.width]': 'width'}})
class AnotherDirThatSetsWidthDirective {
@Input('another-dir-that-sets-width') public width: string = '';
}
@Component({
template: `
<div [style.width]="w0"
[dir-that-sets-width]="w1"
[another-dir-that-sets-width]="w2">
`
})
class Cmp {
w0: string|null = null;
w1: string|null = null;
w2: string|null = null;
}
TestBed.configureTestingModule(
{declarations: [Cmp, DirThatSetsWidthDirective, AnotherDirThatSetsWidthDirective]});
const fixture = TestBed.createComponent(Cmp);
fixture.componentInstance.w0 = '100px';
fixture.componentInstance.w1 = '200px';
fixture.componentInstance.w2 = '300px';
fixture.detectChanges();
const element = fixture.nativeElement.querySelector('div');
expect(element.style.width).toEqual('100px');
fixture.componentInstance.w0 = null;
fixture.detectChanges();
expect(element.style.width).toEqual('200px');
fixture.componentInstance.w1 = null;
fixture.detectChanges();
expect(element.style.width).toEqual('300px');
fixture.componentInstance.w2 = null;
fixture.detectChanges();
expect(element.style.width).toBeFalsy();
fixture.componentInstance.w2 = '400px';
fixture.detectChanges();
expect(element.style.width).toEqual('400px');
fixture.componentInstance.w1 = '500px';
fixture.componentInstance.w0 = '600px';
fixture.detectChanges();
expect(element.style.width).toEqual('600px');
});
onlyInIvy('ivy resolves styling across directives, components and templates in unison')
.it('should combine all styling across the template, directive and component host bindings',
() => {
@Directive({selector: '[dir-with-styling]'})
class DirWithStyling {
@HostBinding('style.color') public color = 'red';
@HostBinding('style.font-size') public fontSize = '100px';
@HostBinding('class.dir') public dirClass = true;
}
@Component({selector: 'comp-with-styling'})
class CompWithStyling {
@HostBinding('style.width') public width = '900px';
@HostBinding('style.height') public height = '900px';
@HostBinding('class.comp') public compClass = true;
}
@Component({
template: `
<comp-with-styling
[style.opacity]="opacity"
[style.width]="width"
[class.tpl]="tplClass"
dir-with-styling>...</comp-with-styling>
`
})
class Cmp {
opacity: string|null = '0.5';
width: string|null = 'auto';
tplClass = true;
}
TestBed.configureTestingModule({declarations: [Cmp, DirWithStyling, CompWithStyling]});
const fixture = TestBed.createComponent(Cmp);
fixture.detectChanges();
const element = fixture.nativeElement.querySelector('comp-with-styling');
const node = getDebugNode(element) !;
const styles = node.styles !;
const classes = node.classes !;
expect(styles.values).toEqual({
'color': 'red',
'width': 'auto',
'opacity': '0.5',
'height': '900px',
'font-size': '100px'
});
expect(classes.values).toEqual({
'dir': true,
'comp': true,
'tpl': true,
});
fixture.componentInstance.width = null;
fixture.componentInstance.opacity = null;
fixture.componentInstance.tplClass = false;
fixture.detectChanges();
expect(styles.values).toEqual({
'color': 'red',
'width': '900px',
'opacity': null,
'height': '900px',
'font-size': '100px'
});
expect(classes.values).toEqual({
'dir': true,
'comp': true,
'tpl': false,
});
});
onlyInIvy('ivy resolves styling across directives, components and templates in unison')
.it('should properly apply styling across sub and super class directive host bindings',
() => {
@Directive({selector: '[super-class-dir]'})
class SuperClassDirective {
@HostBinding('style.width') public w1 = '100px';
}
@Component({selector: '[sub-class-dir]'})
class SubClassDirective extends SuperClassDirective {
@HostBinding('style.width') public w2 = '200px';
}
@Component({
template: `
<div sub-class-dir [style.width]="w3"></div>
`
})
class Cmp {
w3: string|null = '300px';
}
TestBed.configureTestingModule(
{declarations: [Cmp, SuperClassDirective, SubClassDirective]});
const fixture = TestBed.createComponent(Cmp);
fixture.detectChanges();
const element = fixture.nativeElement.querySelector('div');
const node = getDebugNode(element) !;
const styles = node.styles !;
expect(styles.values).toEqual({
'width': '300px',
});
fixture.componentInstance.w3 = null;
fixture.detectChanges();
expect(styles.values).toEqual({
'width': '200px',
});
});
onlyInIvy('only ivy has style debugging support')
.it('should support situations where there are more than 32 bindings', () => {
const TOTAL_BINDINGS = 34;
let bindingsHTML = '';
let bindingsArr: any[] = [];
for (let i = 0; i < TOTAL_BINDINGS; i++) {
bindingsHTML += `[style.prop${i}]="bindings[${i}]" `;
bindingsArr.push(null);
}
@Component({template: `<div ${bindingsHTML}></div>`})
class Cmp {
bindings = bindingsArr;
updateBindings(value: string) {
for (let i = 0; i < TOTAL_BINDINGS; i++) {
this.bindings[i] = value + i;
}
}
}
TestBed.configureTestingModule({declarations: [Cmp]});
const fixture = TestBed.createComponent(Cmp);
let testValue = 'initial';
fixture.componentInstance.updateBindings('initial');
fixture.detectChanges();
const element = fixture.nativeElement.querySelector('div');
const node = getDebugNode(element) !;
const styles = node.styles !;
let values = styles.values;
let props = Object.keys(values);
expect(props.length).toEqual(TOTAL_BINDINGS);
for (let i = 0; i < props.length; i++) {
const prop = props[i];
const value = values[prop] as string;
const num = value.substr(testValue.length);
expect(value).toEqual(`initial${num}`);
}
testValue = 'final';
fixture.componentInstance.updateBindings('final');
fixture.detectChanges();
values = styles.values;
props = Object.keys(values);
expect(props.length).toEqual(TOTAL_BINDINGS);
for (let i = 0; i < props.length; i++) {
const prop = props[i];
const value = values[prop] as string;
const num = value.substr(testValue.length);
expect(value).toEqual(`final${num}`);
}
});
});
function getDebugNode(element: Node): DebugNode|null {
const lContext = loadLContextFromNode(element);
const lViewDebug = toDebug(lContext.lView) as LViewDebug;
const debugNodes = lViewDebug.nodes || [];
for (let i = 0; i < debugNodes.length; i++) {
const n = debugNodes[i];
if (n.native === element) {
return n;
}
}
return null;
}

View File

@ -32,6 +32,15 @@
{
"name": "DECLARATION_VIEW"
},
{
"name": "DEFAULT_BINDING_VALUE"
},
{
"name": "DEFAULT_MASK_VALUE"
},
{
"name": "DEFAULT_SIZE_VALUE"
},
{
"name": "DEFAULT_TEMPLATE_DIRECTIVE_INDEX"
},
@ -161,6 +170,12 @@
{
"name": "_selectedIndex"
},
{
"name": "_stylingMode"
},
{
"name": "addBindingIntoContext"
},
{
"name": "addComponentLogic"
},
@ -173,6 +188,12 @@
{
"name": "allocStylingContext"
},
{
"name": "allocStylingContext"
},
{
"name": "allocateNewContextEntry"
},
{
"name": "allocateOrUpdateDirectiveIntoContext"
},
@ -311,6 +332,9 @@
{
"name": "getCheckNoChangesMode"
},
{
"name": "getClassesContext"
},
{
"name": "getClosureSafeProperty"
},
@ -323,6 +347,9 @@
{
"name": "getContainerRenderParent"
},
{
"name": "getContext"
},
{
"name": "getDirectiveDef"
},
@ -398,6 +425,9 @@
{
"name": "getPreviousOrParentTNode"
},
{
"name": "getProp"
},
{
"name": "getRenderFlags"
},
@ -413,12 +443,18 @@
{
"name": "getSelectedIndex"
},
{
"name": "getStylesContext"
},
{
"name": "getStylingContextFromLView"
},
{
"name": "getTNode"
},
{
"name": "getValuesCount"
},
{
"name": "hasClassInput"
},
@ -575,6 +611,12 @@
{
"name": "refreshDynamicEmbeddedViews"
},
{
"name": "registerBinding"
},
{
"name": "registerInitialStylingIntoContext"
},
{
"name": "registerPostOrderHooks"
},
@ -608,6 +650,9 @@
{
"name": "resolveDirectives"
},
{
"name": "runtimeIsNewStylingInUse"
},
{
"name": "saveNameToExportMap"
},

View File

@ -11,6 +11,9 @@
{
"name": "BINDING_INDEX"
},
{
"name": "BIT_MASK_APPLY_ALL"
},
{
"name": "BLOOM_MASK"
},
@ -47,6 +50,18 @@
{
"name": "DECLARATION_VIEW"
},
{
"name": "DEFAULT_BINDING_INDEX_VALUE"
},
{
"name": "DEFAULT_BINDING_VALUE"
},
{
"name": "DEFAULT_MASK_VALUE"
},
{
"name": "DEFAULT_SIZE_VALUE"
},
{
"name": "DEFAULT_TEMPLATE_DIRECTIVE_INDEX"
},
@ -377,6 +392,9 @@
{
"name": "_selectedIndex"
},
{
"name": "_stylingMode"
},
{
"name": "_symbolIterator"
},
@ -389,6 +407,9 @@
{
"name": "activeDirectiveSuperClassHeight"
},
{
"name": "addBindingIntoContext"
},
{
"name": "addComponentLogic"
},
@ -413,21 +434,39 @@
{
"name": "allocStylingContext"
},
{
"name": "allocStylingContext"
},
{
"name": "allocateNewContextEntry"
},
{
"name": "allocateOrUpdateDirectiveIntoContext"
},
{
"name": "allowFlush"
},
{
"name": "allowStylingFlush"
},
{
"name": "allowValueChange"
},
{
"name": "appendChild"
},
{
"name": "applyClasses"
},
{
"name": "applyOnCreateInstructions"
},
{
"name": "applyStyles"
},
{
"name": "applyStyling"
},
{
"name": "assertTemplate"
},
@ -476,6 +515,12 @@
{
"name": "checkView"
},
{
"name": "classProp"
},
{
"name": "classesBitMask"
},
{
"name": "cleanUpView"
},
@ -542,6 +587,9 @@
{
"name": "createViewBlueprint"
},
{
"name": "currentClassIndex"
},
{
"name": "decreaseElementDepthCount"
},
@ -551,6 +599,12 @@
{
"name": "defaultScheduler"
},
{
"name": "deferBindingRegistration"
},
{
"name": "deferredBindingQueue"
},
{
"name": "destroyLView"
},
@ -638,6 +692,9 @@
{
"name": "findViaComponent"
},
{
"name": "flushDeferredBindings"
},
{
"name": "flushQueue"
},
@ -659,12 +716,21 @@
{
"name": "getActiveDirectiveStylingIndex"
},
{
"name": "getActiveDirectiveStylingIndex"
},
{
"name": "getActiveDirectiveSuperClassDepth"
},
{
"name": "getActiveDirectiveSuperClassHeight"
},
{
"name": "getBeforeNodeForView"
},
{
"name": "getBindingNameFromIndex"
},
{
"name": "getBindingsEnabled"
},
@ -674,6 +740,9 @@
{
"name": "getCheckNoChangesMode"
},
{
"name": "getClassesContext"
},
{
"name": "getCleanup"
},
@ -689,9 +758,15 @@
{
"name": "getComponentViewByInstance"
},
{
"name": "getConfig"
},
{
"name": "getContainerRenderParent"
},
{
"name": "getContext"
},
{
"name": "getContextLView"
},
@ -713,6 +788,9 @@
{
"name": "getGlobal"
},
{
"name": "getGuardMask"
},
{
"name": "getHighestElementOrICUContainer"
},
@ -776,6 +854,9 @@
{
"name": "getNativeByTNode"
},
{
"name": "getNativeFromLView"
},
{
"name": "getNodeInjectable"
},
@ -833,12 +914,18 @@
{
"name": "getProp"
},
{
"name": "getProp"
},
{
"name": "getRenderFlags"
},
{
"name": "getRenderParent"
},
{
"name": "getRenderer"
},
{
"name": "getRootContext"
},
@ -854,6 +941,9 @@
{
"name": "getStyleSanitizer"
},
{
"name": "getStylesContext"
},
{
"name": "getStylingContext"
},
@ -878,6 +968,12 @@
{
"name": "getValue"
},
{
"name": "getValue"
},
{
"name": "getValuesCount"
},
{
"name": "handleError"
},
@ -920,15 +1016,15 @@
{
"name": "initNodeFlags"
},
{
"name": "initStyling"
},
{
"name": "initializeStaticContext"
},
{
"name": "initializeTNodeInputs"
},
{
"name": "initstyling"
},
{
"name": "injectElementRef"
},
@ -980,6 +1076,9 @@
{
"name": "isContextDirty"
},
{
"name": "isContextLocked"
},
{
"name": "isCreationMode"
},
@ -1031,6 +1130,9 @@
{
"name": "isStylingContext"
},
{
"name": "isValueDefined"
},
{
"name": "iterateListLike"
},
@ -1049,6 +1151,9 @@
{
"name": "locateHostElement"
},
{
"name": "lockContext"
},
{
"name": "looseIdentical"
},
@ -1145,9 +1250,15 @@
{
"name": "refreshDynamicEmbeddedViews"
},
{
"name": "registerBinding"
},
{
"name": "registerHostDirective"
},
{
"name": "registerInitialStylingIntoContext"
},
{
"name": "registerMultiMapEntry"
},
@ -1199,6 +1310,12 @@
{
"name": "resolveForwardRef"
},
{
"name": "runtimeAllowOldStyling"
},
{
"name": "runtimeIsNewStylingInUse"
},
{
"name": "saveNameToExportMap"
},
@ -1229,6 +1346,12 @@
{
"name": "setClass"
},
{
"name": "setClass"
},
{
"name": "setConfig"
},
{
"name": "setContextDirty"
},
@ -1289,6 +1412,9 @@
{
"name": "setStyle"
},
{
"name": "setStyle"
},
{
"name": "setTNodeAndViewData"
},
@ -1313,9 +1439,18 @@
{
"name": "stringifyForError"
},
{
"name": "stylesBitMask"
},
{
"name": "stylingApply"
},
{
"name": "stylingContext"
},
{
"name": "stylingInit"
},
{
"name": "syncViewWithBlueprint"
},
@ -1331,12 +1466,24 @@
{
"name": "unwrapRNode"
},
{
"name": "updateBindingData"
},
{
"name": "updateClassBinding"
},
{
"name": "updateClassProp"
},
{
"name": "updateContextDirectiveIndex"
},
{
"name": "updateContextWithBindings"
},
{
"name": "updateLastDirectiveIndex"
},
{
"name": "updateSingleStylingValue"
},

View File

@ -0,0 +1,93 @@
/**
* @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 {registerBinding} from '@angular/core/src/render3/styling_next/bindings';
import {attachStylingDebugObject} from '@angular/core/src/render3/styling_next/styling_debug';
import {allocStylingContext} from '../../../src/render3/styling_next/util';
describe('styling context', () => {
it('should register a series of entries into the context', () => {
const debug = makeContextWithDebug();
const context = debug.context;
expect(debug.entries).toEqual({});
registerBinding(context, 0, 'width', '100px');
expect(debug.entries['width']).toEqual({
prop: 'width',
valuesCount: 1,
guardMask: buildGuardMask(),
defaultValue: '100px',
sources: ['100px'],
});
registerBinding(context, 1, 'width', 20);
expect(debug.entries['width']).toEqual({
prop: 'width',
valuesCount: 2,
guardMask: buildGuardMask(1),
defaultValue: '100px',
sources: [20, '100px'],
});
registerBinding(context, 2, 'height', 10);
registerBinding(context, 3, 'height', 15);
expect(debug.entries['height']).toEqual({
prop: 'height',
valuesCount: 3,
guardMask: buildGuardMask(2, 3),
defaultValue: null,
sources: [10, 15, null],
});
});
it('should overwrite a default value for an entry only if it is non-null', () => {
const debug = makeContextWithDebug();
const context = debug.context;
expect(debug.entries).toEqual({});
registerBinding(context, 0, 'width', null);
expect(debug.entries['width']).toEqual({
prop: 'width',
valuesCount: 1,
guardMask: buildGuardMask(),
defaultValue: null,
sources: [null]
});
registerBinding(context, 0, 'width', '100px');
expect(debug.entries['width']).toEqual({
prop: 'width',
valuesCount: 1,
guardMask: buildGuardMask(),
defaultValue: '100px',
sources: ['100px']
});
registerBinding(context, 0, 'width', '200px');
expect(debug.entries['width']).toEqual({
prop: 'width',
valuesCount: 1,
guardMask: buildGuardMask(),
defaultValue: '100px',
sources: ['100px']
});
});
});
function makeContextWithDebug() {
const ctx = allocStylingContext();
return attachStylingDebugObject(ctx);
}
function buildGuardMask(...bindingIndices: number[]) {
let mask = 0;
for (let i = 0; i < bindingIndices.length; i++) {
mask |= 1 << bindingIndices[i];
}
return mask;
}

View File

@ -0,0 +1,82 @@
/**
* @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 {registerBinding} from '@angular/core/src/render3/styling_next/bindings';
import {NodeStylingDebug, attachStylingDebugObject} from '@angular/core/src/render3/styling_next/styling_debug';
import {allocStylingContext} from '@angular/core/src/render3/styling_next/util';
describe('styling debugging tools', () => {
describe('NodeStylingDebug', () => {
it('should list out each of the values in the context paired together with the provided data',
() => {
const debug = makeContextWithDebug();
const context = debug.context;
const data: any[] = [];
const d = new NodeStylingDebug(context, data);
registerBinding(context, 0, 'width', null);
expect(d.summary).toEqual({
width: {
prop: 'width',
value: null,
bindingIndex: null,
sourceValues: [{value: null, bindingIndex: null}],
},
});
registerBinding(context, 0, 'width', '100px');
expect(d.summary).toEqual({
width: {
prop: 'width',
value: '100px',
bindingIndex: null,
sourceValues: [
{bindingIndex: null, value: '100px'},
],
},
});
const someBindingIndex1 = 1;
data[someBindingIndex1] = '200px';
registerBinding(context, 0, 'width', someBindingIndex1);
expect(d.summary).toEqual({
width: {
prop: 'width',
value: '200px',
bindingIndex: someBindingIndex1,
sourceValues: [
{bindingIndex: someBindingIndex1, value: '200px'},
{bindingIndex: null, value: '100px'},
],
},
});
const someBindingIndex2 = 2;
data[someBindingIndex2] = '500px';
registerBinding(context, 0, 'width', someBindingIndex2);
expect(d.summary).toEqual({
width: {
prop: 'width',
value: '200px',
bindingIndex: someBindingIndex1,
sourceValues: [
{bindingIndex: someBindingIndex1, value: '200px'},
{bindingIndex: someBindingIndex2, value: '500px'},
{bindingIndex: null, value: '100px'},
],
},
});
});
});
});
function makeContextWithDebug() {
const ctx = allocStylingContext();
return attachStylingDebugObject(ctx);
}