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:
parent
848e53efd0
commit
f03475cac8
|
@ -21,7 +21,7 @@
|
|||
"master": {
|
||||
"uncompressed": {
|
||||
"runtime": 1440,
|
||||
"main": 146225,
|
||||
"main": 147764,
|
||||
"polyfills": 43567
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
}
|
|
@ -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)[];
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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});
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
Loading…
Reference in New Issue