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:

committed by
Jason Aden

parent
848e53efd0
commit
f03475cac8
@ -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]
|
||||
|
356
packages/core/src/render3/styling_next/bindings.ts
Normal file
356
packages/core/src/render3/styling_next/bindings.ts
Normal file
@ -0,0 +1,356 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {ProceduralRenderer3, RElement, Renderer3, RendererStyleFlags3, isProceduralRenderer} from '../interfaces/renderer';
|
||||
import {ApplyStylingFn, StylingBindingData, TStylingContext, TStylingContextIndex} from './interfaces';
|
||||
import {allowStylingFlush, getGuardMask, getProp, getValue, getValuesCount, isContextLocked, lockContext} from './util';
|
||||
|
||||
|
||||
/**
|
||||
* This file contains the core logic for styling in Angular.
|
||||
*
|
||||
* All styling bindings (i.e. `[style]`, `[style.prop]`, `[class]` and `[class.name]`)
|
||||
* will have their values be applied through the logic in this file.
|
||||
*
|
||||
* When a binding is encountered (e.g. `<div [style.width]="w">`) then
|
||||
* the binding data will be populated into a `TStylingContext` data-structure.
|
||||
* There is only one `TStylingContext` per `TNode` and each element instance
|
||||
* will update its style/class binding values in concert with the styling
|
||||
* context.
|
||||
*
|
||||
* To learn more about the algorithm see `TStylingContext`.
|
||||
*/
|
||||
|
||||
// the values below are global to all styling code below. Each value
|
||||
// will either increment or mutate each time a styling instruction is
|
||||
// executed. Do not modify the values below.
|
||||
let currentStyleIndex = 0;
|
||||
let currentClassIndex = 0;
|
||||
let stylesBitMask = 0;
|
||||
let classesBitMask = 0;
|
||||
let deferredBindingQueue: (TStylingContext | number | string | null)[] = [];
|
||||
|
||||
const DEFAULT_BINDING_VALUE = null;
|
||||
const DEFAULT_SIZE_VALUE = 1;
|
||||
const DEFAULT_MASK_VALUE = 0;
|
||||
export const DEFAULT_BINDING_INDEX_VALUE = -1;
|
||||
export const BIT_MASK_APPLY_ALL = -1;
|
||||
|
||||
|
||||
/**
|
||||
* Visits a class-based binding and updates the new value (if changed).
|
||||
*
|
||||
* This function is called each time a class-based styling instruction
|
||||
* is executed. It's important that it's always called (even if the value
|
||||
* has not changed) so that the inner counter index value is incremented.
|
||||
* This way, each instruction is always guaranteed to get the same counter
|
||||
* state each time its called (which then allows the `TStylingContext`
|
||||
* and the bit mask values to be in sync).
|
||||
*/
|
||||
export function updateClassBinding(
|
||||
context: TStylingContext, data: StylingBindingData, prop: string, bindingIndex: number,
|
||||
value: boolean | null | undefined, deferRegistration: boolean): void {
|
||||
const index = currentClassIndex++;
|
||||
if (updateBindingData(context, data, index, prop, bindingIndex, value, deferRegistration)) {
|
||||
classesBitMask |= 1 << index;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Visits a style-based binding and updates the new value (if changed).
|
||||
*
|
||||
* This function is called each time a style-based styling instruction
|
||||
* is executed. It's important that it's always called (even if the value
|
||||
* has not changed) so that the inner counter index value is incremented.
|
||||
* This way, each instruction is always guaranteed to get the same counter
|
||||
* state each time its called (which then allows the `TStylingContext`
|
||||
* and the bit mask values to be in sync).
|
||||
*/
|
||||
export function updateStyleBinding(
|
||||
context: TStylingContext, data: StylingBindingData, prop: string, bindingIndex: number,
|
||||
value: String | string | number | null | undefined, deferRegistration: boolean): void {
|
||||
const index = currentStyleIndex++;
|
||||
if (updateBindingData(context, data, index, prop, bindingIndex, value, deferRegistration)) {
|
||||
stylesBitMask |= 1 << index;
|
||||
}
|
||||
}
|
||||
|
||||
function updateBindingData(
|
||||
context: TStylingContext, data: StylingBindingData, counterIndex: number, prop: string,
|
||||
bindingIndex: number, value: string | String | number | boolean | null | undefined,
|
||||
deferRegistration?: boolean): boolean {
|
||||
if (!isContextLocked(context)) {
|
||||
if (deferRegistration) {
|
||||
deferBindingRegistration(context, counterIndex, prop, bindingIndex);
|
||||
} else {
|
||||
deferredBindingQueue.length && flushDeferredBindings();
|
||||
|
||||
// this will only happen during the first update pass of the
|
||||
// context. The reason why we can't use `tNode.firstTemplatePass`
|
||||
// here is because its not guaranteed to be true when the first
|
||||
// update pass is executed (remember that all styling instructions
|
||||
// are run in the update phase, and, as a result, are no more
|
||||
// styling instructions that are run in the creation phase).
|
||||
registerBinding(context, counterIndex, prop, bindingIndex);
|
||||
}
|
||||
}
|
||||
|
||||
if (data[bindingIndex] !== value) {
|
||||
data[bindingIndex] = value;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules a binding registration to be run at a later point.
|
||||
*
|
||||
* The reasoning for this feature is to ensure that styling
|
||||
* bindings are registered in the correct order for when
|
||||
* directives/components have a super/sub class inheritance
|
||||
* chains. Each directive's styling bindings must be
|
||||
* registered into the context in reverse order. Therefore all
|
||||
* bindings will be buffered in reverse order and then applied
|
||||
* after the inheritance chain exits.
|
||||
*/
|
||||
function deferBindingRegistration(
|
||||
context: TStylingContext, counterIndex: number, prop: string, bindingIndex: number) {
|
||||
deferredBindingQueue.splice(0, 0, context, counterIndex, prop, bindingIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes the collection of deferred bindings and causes each entry
|
||||
* to be registered into the context.
|
||||
*/
|
||||
function flushDeferredBindings() {
|
||||
let i = 0;
|
||||
while (i < deferredBindingQueue.length) {
|
||||
const context = deferredBindingQueue[i++] as TStylingContext;
|
||||
const count = deferredBindingQueue[i++] as number;
|
||||
const prop = deferredBindingQueue[i++] as string;
|
||||
const bindingIndex = deferredBindingQueue[i++] as number | null;
|
||||
registerBinding(context, count, prop, bindingIndex);
|
||||
}
|
||||
deferredBindingQueue.length = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the provided binding (prop + bindingIndex) into the context.
|
||||
*
|
||||
* This function is shared between bindings that are assigned immediately
|
||||
* (via `updateBindingData`) and at a deferred stage. When called, it will
|
||||
* figure out exactly where to place the binding data in the context.
|
||||
*
|
||||
* It is needed because it will either update or insert a styling property
|
||||
* into the context at the correct spot.
|
||||
*
|
||||
* When called, one of two things will happen:
|
||||
*
|
||||
* 1) If the property already exists in the context then it will just add
|
||||
* the provided `bindingValue` to the end of the binding sources region
|
||||
* for that particular property.
|
||||
*
|
||||
* - If the binding value is a number then it will be added as a new
|
||||
* binding index source next to the other binding sources for the property.
|
||||
*
|
||||
* - Otherwise, if the binding value is a string/boolean/null type then it will
|
||||
* replace the default value for the property if the default value is `null`.
|
||||
*
|
||||
* 2) If the property does not exist then it will be inserted into the context.
|
||||
* The styling context relies on all properties being stored in alphabetical
|
||||
* order, so it knows exactly where to store it.
|
||||
*
|
||||
* When inserted, a default `null` value is created for the property which exists
|
||||
* as the default value for the binding. If the bindingValue property is inserted
|
||||
* and it is either a string, number or null value then that will replace the default
|
||||
* value.
|
||||
*/
|
||||
export function registerBinding(
|
||||
context: TStylingContext, countId: number, prop: string,
|
||||
bindingValue: number | null | string | boolean) {
|
||||
let i = TStylingContextIndex.ValuesStartPosition;
|
||||
let found = false;
|
||||
while (i < context.length) {
|
||||
const valuesCount = getValuesCount(context, i);
|
||||
const p = getProp(context, i);
|
||||
found = prop <= p;
|
||||
if (found) {
|
||||
// all style/class bindings are sorted by property name
|
||||
if (prop < p) {
|
||||
allocateNewContextEntry(context, i, prop);
|
||||
}
|
||||
addBindingIntoContext(context, i, bindingValue, countId);
|
||||
break;
|
||||
}
|
||||
i += TStylingContextIndex.BindingsStartOffset + valuesCount;
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
allocateNewContextEntry(context, context.length, prop);
|
||||
addBindingIntoContext(context, i, bindingValue, countId);
|
||||
}
|
||||
}
|
||||
|
||||
function allocateNewContextEntry(context: TStylingContext, index: number, prop: string) {
|
||||
context.splice(index, 0, DEFAULT_MASK_VALUE, DEFAULT_SIZE_VALUE, prop, DEFAULT_BINDING_VALUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a new binding value into a styling property tuple in the `TStylingContext`.
|
||||
*
|
||||
* A bindingValue is inserted into a context during the first update pass
|
||||
* of a template or host bindings function. When this occurs, two things
|
||||
* happen:
|
||||
*
|
||||
* - If the bindingValue value is a number then it is treated as a bindingIndex
|
||||
* value (a index in the `LView`) and it will be inserted next to the other
|
||||
* binding index entries.
|
||||
*
|
||||
* - Otherwise the binding value will update the default value for the property
|
||||
* and this will only happen if the default value is `null`.
|
||||
*/
|
||||
function addBindingIntoContext(
|
||||
context: TStylingContext, index: number, bindingValue: number | string | boolean | null,
|
||||
countId: number) {
|
||||
const valuesCount = getValuesCount(context, index);
|
||||
|
||||
// -1 is used because we want the last value that's in the list (not the next slot)
|
||||
const lastValueIndex = index + TStylingContextIndex.BindingsStartOffset + valuesCount - 1;
|
||||
|
||||
if (typeof bindingValue === 'number') {
|
||||
context.splice(lastValueIndex, 0, bindingValue);
|
||||
(context[index + TStylingContextIndex.ValuesCountOffset] as number)++;
|
||||
(context[index + TStylingContextIndex.MaskOffset] as number) |= 1 << countId;
|
||||
} else if (typeof bindingValue === 'string' && context[lastValueIndex] == null) {
|
||||
context[lastValueIndex] = bindingValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies all class entries in the provided context to the provided element.
|
||||
*/
|
||||
export function applyClasses(
|
||||
renderer: Renderer3 | ProceduralRenderer3 | null, data: StylingBindingData,
|
||||
context: TStylingContext, element: RElement, directiveIndex: number) {
|
||||
if (allowStylingFlush(context, directiveIndex)) {
|
||||
const isFirstPass = isContextLocked(context);
|
||||
isFirstPass && lockContext(context);
|
||||
applyStyling(context, renderer, element, data, classesBitMask, setClass, isFirstPass);
|
||||
currentClassIndex = 0;
|
||||
classesBitMask = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies all style entries in the provided context to the provided element.
|
||||
*/
|
||||
export function applyStyles(
|
||||
renderer: Renderer3 | ProceduralRenderer3 | null, data: StylingBindingData,
|
||||
context: TStylingContext, element: RElement, directiveIndex: number) {
|
||||
if (allowStylingFlush(context, directiveIndex)) {
|
||||
const isFirstPass = isContextLocked(context);
|
||||
isFirstPass && lockContext(context);
|
||||
applyStyling(context, renderer, element, data, stylesBitMask, setStyle, isFirstPass);
|
||||
currentStyleIndex = 0;
|
||||
stylesBitMask = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs through the provided styling context and applies each value to
|
||||
* the provided element (via the renderer) if one or more values are present.
|
||||
*
|
||||
* Note that this function is not designed to be called in isolation (use
|
||||
* `applyClasses` and `applyStyles` to actually apply styling values).
|
||||
*/
|
||||
export function applyStyling(
|
||||
context: TStylingContext, renderer: Renderer3 | ProceduralRenderer3 | null, element: RElement,
|
||||
bindingData: StylingBindingData, bitMask: number, applyStylingFn: ApplyStylingFn,
|
||||
forceApplyDefaultValues?: boolean) {
|
||||
deferredBindingQueue.length && flushDeferredBindings();
|
||||
|
||||
if (bitMask) {
|
||||
let processAllEntries = bitMask === BIT_MASK_APPLY_ALL;
|
||||
let i = TStylingContextIndex.ValuesStartPosition;
|
||||
while (i < context.length) {
|
||||
const valuesCount = getValuesCount(context, i);
|
||||
const guardMask = getGuardMask(context, i);
|
||||
|
||||
// the guard mask value is non-zero if and when
|
||||
// there are binding values present for the property.
|
||||
// If there are ONLY static values (i.e. `style="prop:val")
|
||||
// then the guard value will stay as zero.
|
||||
const processEntry =
|
||||
processAllEntries || (guardMask ? (bitMask & guardMask) : forceApplyDefaultValues);
|
||||
if (processEntry) {
|
||||
const prop = getProp(context, i);
|
||||
const limit = valuesCount - 1;
|
||||
for (let j = 0; j <= limit; j++) {
|
||||
const isFinalValue = j === limit;
|
||||
const bindingValue = getValue(context, i, j);
|
||||
const bindingIndex =
|
||||
isFinalValue ? DEFAULT_BINDING_INDEX_VALUE : (bindingValue as number);
|
||||
const valueToApply: string|null = isFinalValue ? bindingValue : bindingData[bindingIndex];
|
||||
if (isValueDefined(valueToApply) || isFinalValue) {
|
||||
applyStylingFn(renderer, element, prop, valueToApply, bindingIndex);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
i += TStylingContextIndex.BindingsStartOffset + valuesCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isValueDefined(value: any) {
|
||||
// the reason why null is compared against is because
|
||||
// a CSS class value that is set to `false` must be
|
||||
// respected (otherwise it would be treated as falsy).
|
||||
// Empty string values are because developers usually
|
||||
// set a value to an empty string to remove it.
|
||||
return value != null && value !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns a style value to a style property for the given element.
|
||||
*/
|
||||
const setStyle: ApplyStylingFn =
|
||||
(renderer: Renderer3 | null, native: any, prop: string, value: string | null) => {
|
||||
if (value) {
|
||||
// opacity, z-index and flexbox all have number values
|
||||
// and these need to be converted into strings so that
|
||||
// they can be assigned properly.
|
||||
value = value.toString();
|
||||
ngDevMode && ngDevMode.rendererSetStyle++;
|
||||
renderer && isProceduralRenderer(renderer) ?
|
||||
renderer.setStyle(native, prop, value, RendererStyleFlags3.DashCase) :
|
||||
native.style.setProperty(prop, value);
|
||||
} else {
|
||||
ngDevMode && ngDevMode.rendererRemoveStyle++;
|
||||
renderer && isProceduralRenderer(renderer) ?
|
||||
renderer.removeStyle(native, prop, RendererStyleFlags3.DashCase) :
|
||||
native.style.removeProperty(prop);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds/removes the provided className value to the provided element.
|
||||
*/
|
||||
const setClass: ApplyStylingFn =
|
||||
(renderer: Renderer3 | null, native: any, className: string, value: any) => {
|
||||
if (className !== '') {
|
||||
if (value) {
|
||||
ngDevMode && ngDevMode.rendererAddClass++;
|
||||
renderer && isProceduralRenderer(renderer) ? renderer.addClass(native, className) :
|
||||
native.classList.add(className);
|
||||
} else {
|
||||
ngDevMode && ngDevMode.rendererRemoveClass++;
|
||||
renderer && isProceduralRenderer(renderer) ? renderer.removeClass(native, className) :
|
||||
native.classList.remove(className);
|
||||
}
|
||||
}
|
||||
};
|
211
packages/core/src/render3/styling_next/instructions.ts
Normal file
211
packages/core/src/render3/styling_next/instructions.ts
Normal file
@ -0,0 +1,211 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {LContainer} from '../interfaces/container';
|
||||
import {AttributeMarker, TAttributes, TNode, TNodeType} from '../interfaces/node';
|
||||
import {RElement} from '../interfaces/renderer';
|
||||
import {StylingContext as OldStylingContext, StylingIndex as OldStylingIndex} from '../interfaces/styling';
|
||||
import {BINDING_INDEX, HEADER_OFFSET, HOST, LView, RENDERER} from '../interfaces/view';
|
||||
import {getActiveDirectiveId, getActiveDirectiveSuperClassDepth, getActiveDirectiveSuperClassHeight, getLView, getSelectedIndex} from '../state';
|
||||
import {getTNode, isStylingContext as isOldStylingContext} from '../util/view_utils';
|
||||
|
||||
import {applyClasses, applyStyles, registerBinding, updateClassBinding, updateStyleBinding} from './bindings';
|
||||
import {TStylingContext} from './interfaces';
|
||||
import {attachStylingDebugObject} from './styling_debug';
|
||||
import {allocStylingContext, updateContextDirectiveIndex} from './util';
|
||||
|
||||
|
||||
/**
|
||||
* This file contains the core logic for how styling instructions are processed in Angular.
|
||||
*
|
||||
* To learn more about the algorithm see `TStylingContext`.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Temporary function to bridge styling functionality between this new
|
||||
* refactor (which is here inside of `styling_next/`) and the old
|
||||
* implementation (which lives inside of `styling/`).
|
||||
*
|
||||
* This function is executed during the creation block of an element.
|
||||
* Because the existing styling implementation issues a call to the
|
||||
* `styling()` instruction, this instruction will also get run. The
|
||||
* central idea here is that the directive index values are bound
|
||||
* into the context. The directive index is temporary and is only
|
||||
* required until the `select(n)` instruction is fully functional.
|
||||
*/
|
||||
export function stylingInit() {
|
||||
const lView = getLView();
|
||||
const index = getSelectedIndex();
|
||||
const tNode = getTNode(index, lView);
|
||||
updateLastDirectiveIndex(tNode, getActiveDirectiveStylingIndex());
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirror implementation of the `styleProp()` instruction (found in `instructions/styling.ts`).
|
||||
*/
|
||||
export function styleProp(
|
||||
prop: string, value: string | number | String | null, suffix?: string | null): void {
|
||||
const index = getSelectedIndex();
|
||||
const lView = getLView();
|
||||
const bindingIndex = lView[BINDING_INDEX]++;
|
||||
const tNode = getTNode(index, lView);
|
||||
const tContext = getStylesContext(tNode);
|
||||
const defer = getActiveDirectiveSuperClassHeight() > 0;
|
||||
updateStyleBinding(tContext, lView, prop, bindingIndex, value, defer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirror implementation of the `classProp()` instruction (found in `instructions/styling.ts`).
|
||||
*/
|
||||
export function classProp(className: string, value: boolean | null): void {
|
||||
const index = getSelectedIndex();
|
||||
const lView = getLView();
|
||||
const bindingIndex = lView[BINDING_INDEX]++;
|
||||
const tNode = getTNode(index, lView);
|
||||
const tContext = getClassesContext(tNode);
|
||||
const defer = getActiveDirectiveSuperClassHeight() > 0;
|
||||
updateClassBinding(tContext, lView, className, bindingIndex, value, defer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporary function to bridge styling functionality between this new
|
||||
* refactor (which is here inside of `styling_next/`) and the old
|
||||
* implementation (which lives inside of `styling/`).
|
||||
*
|
||||
* The new styling refactor ensures that styling flushing is called
|
||||
* automatically when a template function exits or a follow-up element
|
||||
* is visited (i.e. when `select(n)` is called). Because the `select(n)`
|
||||
* instruction is not fully implemented yet (it doesn't actually execute
|
||||
* host binding instruction code at the right time), this means that a
|
||||
* styling apply function is still needed.
|
||||
*
|
||||
* This function is a mirror implementation of the `stylingApply()`
|
||||
* instruction (found in `instructions/styling.ts`).
|
||||
*/
|
||||
export function stylingApply() {
|
||||
const index = getSelectedIndex();
|
||||
const lView = getLView();
|
||||
const tNode = getTNode(index, lView);
|
||||
const renderer = getRenderer(tNode, lView);
|
||||
const native = getNativeFromLView(index, lView);
|
||||
const directiveIndex = getActiveDirectiveStylingIndex();
|
||||
applyClasses(renderer, lView, getClassesContext(tNode), native, directiveIndex);
|
||||
applyStyles(renderer, lView, getStylesContext(tNode), native, directiveIndex);
|
||||
}
|
||||
|
||||
function getStylesContext(tNode: TNode): TStylingContext {
|
||||
return getContext(tNode, false);
|
||||
}
|
||||
|
||||
function getClassesContext(tNode: TNode): TStylingContext {
|
||||
return getContext(tNode, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns/instantiates a styling context from/to a `tNode` instance.
|
||||
*/
|
||||
function getContext(tNode: TNode, isClassBased: boolean) {
|
||||
let context = isClassBased ? tNode.newClasses : tNode.newStyles;
|
||||
if (!context) {
|
||||
context = allocStylingContext();
|
||||
if (ngDevMode) {
|
||||
attachStylingDebugObject(context);
|
||||
}
|
||||
if (isClassBased) {
|
||||
tNode.newClasses = context;
|
||||
} else {
|
||||
tNode.newStyles = context;
|
||||
}
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporary function to bridge styling functionality between this new
|
||||
* refactor (which is here inside of `styling_next/`) and the old
|
||||
* implementation (which lives inside of `styling/`).
|
||||
*
|
||||
* The purpose of this function is to traverse through the LView data
|
||||
* for a specific element index and return the native node. Because the
|
||||
* current implementation relies on there being a styling context array,
|
||||
* the code below will need to loop through these array values until it
|
||||
* gets a native element node.
|
||||
*
|
||||
* Note that this code is temporary and will disappear once the new
|
||||
* styling refactor lands in its entirety.
|
||||
*/
|
||||
function getNativeFromLView(index: number, viewData: LView): RElement {
|
||||
let storageIndex = index + HEADER_OFFSET;
|
||||
let slotValue: LContainer|LView|OldStylingContext|RElement = viewData[storageIndex];
|
||||
let wrapper: LContainer|LView|OldStylingContext = viewData;
|
||||
while (Array.isArray(slotValue)) {
|
||||
wrapper = slotValue;
|
||||
slotValue = slotValue[HOST] as LView | OldStylingContext | RElement;
|
||||
}
|
||||
if (isOldStylingContext(wrapper)) {
|
||||
return wrapper[OldStylingIndex.ElementPosition] as RElement;
|
||||
} else {
|
||||
return slotValue;
|
||||
}
|
||||
}
|
||||
|
||||
function getRenderer(tNode: TNode, lView: LView) {
|
||||
return tNode.type === TNodeType.Element ? lView[RENDERER] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches and assigns provided all static style/class entries (found in the `attrs` value)
|
||||
* and registers them in their respective styling contexts.
|
||||
*/
|
||||
export function registerInitialStylingIntoContext(
|
||||
tNode: TNode, attrs: TAttributes, startIndex: number) {
|
||||
let classesContext !: TStylingContext;
|
||||
let stylesContext !: TStylingContext;
|
||||
let mode = -1;
|
||||
for (let i = startIndex; i < attrs.length; i++) {
|
||||
const attr = attrs[i];
|
||||
if (typeof attr == 'number') {
|
||||
mode = attr;
|
||||
} else if (mode == AttributeMarker.Classes) {
|
||||
classesContext = classesContext || getClassesContext(tNode);
|
||||
registerBinding(classesContext, -1, attr as string, true);
|
||||
} else if (mode == AttributeMarker.Styles) {
|
||||
stylesContext = stylesContext || getStylesContext(tNode);
|
||||
registerBinding(stylesContext, -1, attr as string, attrs[++i] as string);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirror implementation of the same function found in `instructions/styling.ts`.
|
||||
*/
|
||||
export function getActiveDirectiveStylingIndex(): number {
|
||||
// whenever a directive's hostBindings function is called a uniqueId value
|
||||
// is assigned. Normally this is enough to help distinguish one directive
|
||||
// from another for the styling context, but there are situations where a
|
||||
// sub-class directive could inherit and assign styling in concert with a
|
||||
// parent directive. To help the styling code distinguish between a parent
|
||||
// sub-classed directive the inheritance depth is taken into account as well.
|
||||
return getActiveDirectiveId() + getActiveDirectiveSuperClassDepth();
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporary function that will update the max directive index value in
|
||||
* both the classes and styles contexts present on the provided `tNode`.
|
||||
*
|
||||
* This code is only used because the `select(n)` code functionality is not
|
||||
* yet 100% functional. The `select(n)` instruction cannot yet evaluate host
|
||||
* bindings function code in sync with the associated template function code.
|
||||
* For this reason the styling algorithm needs to track the last directive index
|
||||
* value so that it knows exactly when to render styling to the element since
|
||||
* `stylingApply()` is called multiple times per CD (`stylingApply` will be
|
||||
* removed once `select(n)` is fixed).
|
||||
*/
|
||||
function updateLastDirectiveIndex(tNode: TNode, directiveIndex: number) {
|
||||
updateContextDirectiveIndex(getClassesContext(tNode), directiveIndex);
|
||||
updateContextDirectiveIndex(getStylesContext(tNode), directiveIndex);
|
||||
}
|
239
packages/core/src/render3/styling_next/interfaces.ts
Normal file
239
packages/core/src/render3/styling_next/interfaces.ts
Normal file
@ -0,0 +1,239 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {ProceduralRenderer3, RElement, Renderer3} from '../interfaces/renderer';
|
||||
import {LView} from '../interfaces/view';
|
||||
|
||||
/**
|
||||
* A static-level representation of all style or class bindings/values
|
||||
* associated with a `TNode`.
|
||||
*
|
||||
* The `TStylingContext` unites all template styling bindings (i.e.
|
||||
* `[class]` and `[style]` bindings) as well as all host-level
|
||||
* styling bindings (for components and directives) together into
|
||||
* a single manifest. It is used each time there are one or more
|
||||
* styling bindings present for an element.
|
||||
*
|
||||
* The styling context is stored on a `TNode` on and there are
|
||||
* two instances of it: one for classes and another for styles.
|
||||
*
|
||||
* ```typescript
|
||||
* tNode.styles = [ ... a context only for styles ... ];
|
||||
* tNode.classes = [ ... a context only for classes ... ];
|
||||
* ```
|
||||
*
|
||||
* Due to the fact the the `TStylingContext` is stored on a `TNode`
|
||||
* this means that all data within the context is static. Instead of
|
||||
* storing actual styling binding values, the lView binding index values
|
||||
* are stored within the context. (static nature means it is more compact.)
|
||||
|
||||
*
|
||||
* ```typescript
|
||||
* // <div [class.active]="c" // lView binding index = 20
|
||||
* // [style.width]="x" // lView binding index = 21
|
||||
* // [style.height]="y"> // lView binding index = 22
|
||||
* tNode.stylesContext = [
|
||||
* 0, // the context config value
|
||||
*
|
||||
* 0b001, // guard mask for width
|
||||
* 2, // total entries for width
|
||||
* 'width', // the property name
|
||||
* 21, // the binding location for the "x" binding in the lView
|
||||
* null,
|
||||
*
|
||||
* 0b010, // guard mask for height
|
||||
* 2, // total entries for height
|
||||
* 'height', // the property name
|
||||
* 22, // the binding location for the "y" binding in the lView
|
||||
* null,
|
||||
* ];
|
||||
*
|
||||
* tNode.classesContext = [
|
||||
* 0, // the context config value
|
||||
*
|
||||
* 0b001, // guard mask for active
|
||||
* 2, // total entries for active
|
||||
* 'active', // the property name
|
||||
* 20, // the binding location for the "c" binding in the lView
|
||||
* null,
|
||||
* ];
|
||||
* ```
|
||||
*
|
||||
* Entry value present in an entry (called a tuple) within the
|
||||
* styling context is as follows:
|
||||
*
|
||||
* ```typescript
|
||||
* context = [
|
||||
* CONFIG, // the styling context config value
|
||||
* //...
|
||||
* guardMask,
|
||||
* totalEntries,
|
||||
* propName,
|
||||
* bindingIndices...,
|
||||
* defaultValue
|
||||
* ];
|
||||
* ```
|
||||
*
|
||||
* Below is a breakdown of each value:
|
||||
*
|
||||
* - **guardMask**:
|
||||
* A numeric value where each bit represents a binding index
|
||||
* location. Each binding index location is assigned based on
|
||||
* a local counter value that increments each time an instruction
|
||||
* is called:
|
||||
*
|
||||
* ```
|
||||
* <div [style.width]="x" // binding index = 21 (counter index = 0)
|
||||
* [style.height]="y"> // binding index = 22 (counter index = 1)
|
||||
* ```
|
||||
*
|
||||
* In the example code above, if the `width` value where to change
|
||||
* then the first bit in the local bit mask value would be flipped
|
||||
* (and the second bit for when `height`).
|
||||
*
|
||||
* If and when there are more than 32 binding sources in the context
|
||||
* (more than 32 `[style/class]` bindings) then the bit masking will
|
||||
* overflow and we are left with a situation where a `-1` value will
|
||||
* represent the bit mask. Due to the way that JavaScript handles
|
||||
* negative values, when the bit mask is `-1` then all bits within
|
||||
* that value will be automatically flipped (this is a quick and
|
||||
* efficient way to flip all bits on the mask when a special kind
|
||||
* of caching scenario occurs or when there are more than 32 bindings).
|
||||
*
|
||||
* - **totalEntries**:
|
||||
* Each property present in the contains various binding sources of
|
||||
* where the styling data could come from. This includes template
|
||||
* level bindings, directive/component host bindings as well as the
|
||||
* default value (or static value) all writing to the same property.
|
||||
* This value depicts how many binding source entries exist for the
|
||||
* property.
|
||||
*
|
||||
* The reason why the totalEntries value is needed is because the
|
||||
* styling context is dynamic in size and it's not possible
|
||||
* for the flushing or update algorithms to know when and where
|
||||
* a property starts and ends without it.
|
||||
*
|
||||
* - **propName**:
|
||||
* The CSS property name or class name (e.g `width` or `active`).
|
||||
*
|
||||
* - **bindingIndices...**:
|
||||
* A series of numeric binding values that reflect where in the
|
||||
* lView to find the style/class values associated with the property.
|
||||
* Each value is in order in terms of priority (templates are first,
|
||||
* then directives and then components). When the context is flushed
|
||||
* and the style/class values are applied to the element (this happens
|
||||
* inside of the `stylingApply` instruction) then the flushing code
|
||||
* will keep checking each binding index against the associated lView
|
||||
* to find the first style/class value that is non-null.
|
||||
*
|
||||
* - **defaultValue**:
|
||||
* This is the default that will always be applied to the element if
|
||||
* and when all other binding sources return a result that is null.
|
||||
* Usually this value is null but it can also be a static value that
|
||||
* is intercepted when the tNode is first constructured (e.g.
|
||||
* `<div style="width:200px">` has a default value of `200px` for
|
||||
* the `width` property).
|
||||
*
|
||||
* Each time a new binding is encountered it is registered into the
|
||||
* context. The context then is continually updated until the first
|
||||
* styling apply call has been called (this is triggered by the
|
||||
* `stylingApply()` instruction for the active element).
|
||||
*
|
||||
* # How Styles/Classes are Applied
|
||||
* Each time a styling instruction (e.g. `[class.name]`, `[style.prop]`,
|
||||
* etc...) is executed, the associated `lView` for the view is updated
|
||||
* at the current binding location. Also, when this happens, a local
|
||||
* counter value is incremented. If the binding value has changed then
|
||||
* a local `bitMask` variable is updated with the specific bit based
|
||||
* on the counter value.
|
||||
*
|
||||
* Below is a lightweight example of what happens when a single style
|
||||
* property is updated (i.e. `<div [style.prop]="val">`):
|
||||
*
|
||||
* ```typescript
|
||||
* function updateStyleProp(prop: string, value: string) {
|
||||
* const lView = getLView();
|
||||
* const bindingIndex = BINDING_INDEX++;
|
||||
* const indexForStyle = localStylesCounter++;
|
||||
* if (lView[bindingIndex] !== value) {
|
||||
* lView[bindingIndex] = value;
|
||||
* localBitMaskForStyles |= 1 << indexForStyle;
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Once all the styling instructions have been evaluated, then the styling
|
||||
* context(s) are flushed to the element. When this happens, the context will
|
||||
* be iterated over (property by property) and each binding source will be
|
||||
* examined and the first non-null value will be applied to the element.
|
||||
*
|
||||
*/
|
||||
export interface TStylingContext extends Array<number|string|number|boolean|null> {
|
||||
[TStylingContextIndex.ConfigPosition]: TStylingConfigFlags;
|
||||
|
||||
/* Temporary value used to track directive index entries until
|
||||
the old styling code is fully removed. The reason why this
|
||||
is required is to figure out which directive is last and,
|
||||
when encountered, trigger a styling flush to happen */
|
||||
[TStylingContextIndex.MaxDirectiveIndexPosition]: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A series of flags used to configure the config value present within a
|
||||
* `TStylingContext` value.
|
||||
*/
|
||||
export const enum TStylingConfigFlags {
|
||||
/**
|
||||
* The initial state of the styling context config
|
||||
*/
|
||||
Initial = 0b0,
|
||||
|
||||
/**
|
||||
* A flag which marks the context as being locked.
|
||||
*
|
||||
* The styling context is constructed across an element template
|
||||
* function as well as any associated hostBindings functions. When
|
||||
* this occurs, the context itself is open to mutation and only once
|
||||
* it has been flushed once then it will be locked for good (no extra
|
||||
* bindings can be added to it).
|
||||
*/
|
||||
Locked = 0b1,
|
||||
}
|
||||
|
||||
/**
|
||||
* An index of position and offset values used to natigate the `TStylingContext`.
|
||||
*/
|
||||
export const enum TStylingContextIndex {
|
||||
ConfigPosition = 0,
|
||||
MaxDirectiveIndexPosition = 1,
|
||||
ValuesStartPosition = 2,
|
||||
|
||||
// each tuple entry in the context
|
||||
// (mask, count, prop, ...bindings||default-value)
|
||||
MaskOffset = 0,
|
||||
ValuesCountOffset = 1,
|
||||
PropOffset = 2,
|
||||
BindingsStartOffset = 3,
|
||||
}
|
||||
|
||||
/**
|
||||
* A function used to apply or remove styling from an element for a given property.
|
||||
*/
|
||||
export interface ApplyStylingFn {
|
||||
(renderer: Renderer3|ProceduralRenderer3|null, element: RElement, prop: string,
|
||||
value: string|null, bindingIndex: number): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runtime data type that is used to store binding data referenced from the `TStylingContext`.
|
||||
*
|
||||
* Because `LView` is just an array with data, there is no reason to
|
||||
* special case `LView` everywhere in the styling algorithm. By allowing
|
||||
* this data type to be an array that contains various scalar data types,
|
||||
* an instance of `LView` doesn't need to be constructed for tests.
|
||||
*/
|
||||
export type StylingBindingData = LView | (string | number | boolean)[];
|
37
packages/core/src/render3/styling_next/state.ts
Normal file
37
packages/core/src/render3/styling_next/state.ts
Normal file
@ -0,0 +1,37 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
/**
|
||||
* A temporary enum of states that inform the core whether or not
|
||||
* to defer all styling instruction calls to the old or new
|
||||
* styling implementation.
|
||||
*/
|
||||
export const enum RuntimeStylingMode {
|
||||
UseOld = 0,
|
||||
UseBothOldAndNew = 1,
|
||||
UseNew = 2,
|
||||
}
|
||||
|
||||
let _stylingMode = 0;
|
||||
|
||||
/**
|
||||
* Temporary function used to inform the existing styling algorithm
|
||||
* code to delegate all styling instruction calls to the new refactored
|
||||
* styling code.
|
||||
*/
|
||||
export function runtimeSetStylingMode(mode: RuntimeStylingMode) {
|
||||
_stylingMode = mode;
|
||||
}
|
||||
|
||||
export function runtimeIsNewStylingInUse() {
|
||||
return _stylingMode > RuntimeStylingMode.UseOld;
|
||||
}
|
||||
|
||||
export function runtimeAllowOldStyling() {
|
||||
return _stylingMode < RuntimeStylingMode.UseNew;
|
||||
}
|
210
packages/core/src/render3/styling_next/styling_debug.ts
Normal file
210
packages/core/src/render3/styling_next/styling_debug.ts
Normal file
@ -0,0 +1,210 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {RElement} from '../interfaces/renderer';
|
||||
import {attachDebugObject} from '../util/debug_utils';
|
||||
|
||||
import {BIT_MASK_APPLY_ALL, DEFAULT_BINDING_INDEX_VALUE, applyStyling} from './bindings';
|
||||
import {StylingBindingData, TStylingContext, TStylingContextIndex} from './interfaces';
|
||||
import {getDefaultValue, getGuardMask, getProp, getValuesCount, isContextLocked} from './util';
|
||||
|
||||
|
||||
/**
|
||||
* A debug/testing-oriented summary of a styling entry.
|
||||
*
|
||||
* A value such as this is generated as an artifact of the `DebugStyling`
|
||||
* summary.
|
||||
*/
|
||||
export interface StylingSummary {
|
||||
/** The style/class property that the summary is attached to */
|
||||
prop: string;
|
||||
|
||||
/** The last applied value for the style/class property */
|
||||
value: string|null;
|
||||
|
||||
/** The binding index of the last applied style/class property */
|
||||
bindingIndex: number|null;
|
||||
|
||||
/** Every binding source that is writing the style/class property represented in this tuple */
|
||||
sourceValues: {value: string | number | null, bindingIndex: number|null}[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A debug/testing-oriented summary of all styling entries for a `DebugNode` instance.
|
||||
*/
|
||||
export interface DebugStyling {
|
||||
/** The associated TStylingContext instance */
|
||||
context: TStylingContext;
|
||||
|
||||
/**
|
||||
* A summarization of each style/class property
|
||||
* present in the context.
|
||||
*/
|
||||
summary: {[key: string]: StylingSummary}|null;
|
||||
|
||||
/**
|
||||
* A key/value map of all styling properties and their
|
||||
* runtime values.
|
||||
*/
|
||||
values: {[key: string]: string | number | null | boolean};
|
||||
}
|
||||
|
||||
/**
|
||||
* A debug/testing-oriented summary of all styling entries within a `TStylingContext`.
|
||||
*/
|
||||
export interface TStylingTupleSummary {
|
||||
/** The property (style or class property) that this tuple represents */
|
||||
prop: string;
|
||||
|
||||
/** The total amount of styling entries apart of this tuple */
|
||||
valuesCount: number;
|
||||
|
||||
/**
|
||||
* The bit guard mask that is used to compare and protect against
|
||||
* styling changes when and styling bindings update
|
||||
*/
|
||||
guardMask: number;
|
||||
|
||||
/**
|
||||
* The default value that will be applied if any bindings are falsy.
|
||||
*/
|
||||
defaultValue: string|boolean|null;
|
||||
|
||||
/**
|
||||
* All bindingIndex sources that have been registered for this style.
|
||||
*/
|
||||
sources: (number|null|string)[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates and attaches an instance of `TStylingContextDebug` to the provided context.
|
||||
*/
|
||||
export function attachStylingDebugObject(context: TStylingContext) {
|
||||
const debug = new TStylingContextDebug(context);
|
||||
attachDebugObject(context, debug);
|
||||
return debug;
|
||||
}
|
||||
|
||||
/**
|
||||
* A human-readable debug summary of the styling data present within `TStylingContext`.
|
||||
*
|
||||
* This class is designed to be used within testing code or when an
|
||||
* application has `ngDevMode` activated.
|
||||
*/
|
||||
class TStylingContextDebug {
|
||||
constructor(public readonly context: TStylingContext) {}
|
||||
|
||||
get isLocked() { return isContextLocked(this.context); }
|
||||
|
||||
/**
|
||||
* Returns a detailed summary of each styling entry in the context.
|
||||
*
|
||||
* See `TStylingTupleSummary`.
|
||||
*/
|
||||
get entries(): {[prop: string]: TStylingTupleSummary} {
|
||||
const context = this.context;
|
||||
const entries: {[prop: string]: TStylingTupleSummary} = {};
|
||||
const start = TStylingContextIndex.ValuesStartPosition;
|
||||
let i = start;
|
||||
while (i < context.length) {
|
||||
const prop = getProp(context, i);
|
||||
const guardMask = getGuardMask(context, i);
|
||||
const valuesCount = getValuesCount(context, i);
|
||||
const defaultValue = getDefaultValue(context, i);
|
||||
|
||||
const bindingsStartPosition = i + TStylingContextIndex.BindingsStartOffset;
|
||||
const sources: (number | string | null)[] = [];
|
||||
|
||||
for (let j = 0; j < valuesCount; j++) {
|
||||
sources.push(context[bindingsStartPosition + j] as number | string | null);
|
||||
}
|
||||
|
||||
entries[prop] = {prop, guardMask, valuesCount, defaultValue, sources};
|
||||
|
||||
i += TStylingContextIndex.BindingsStartOffset + valuesCount;
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A human-readable debug summary of the styling data present for a `DebugNode` instance.
|
||||
*
|
||||
* This class is designed to be used within testing code or when an
|
||||
* application has `ngDevMode` activated.
|
||||
*/
|
||||
export class NodeStylingDebug implements DebugStyling {
|
||||
private _contextDebug: TStylingContextDebug;
|
||||
|
||||
constructor(public context: TStylingContext, private _data: StylingBindingData) {
|
||||
this._contextDebug = (this.context as any).debug as any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a detailed summary of each styling entry in the context and
|
||||
* what their runtime representation is.
|
||||
*
|
||||
* See `StylingSummary`.
|
||||
*/
|
||||
get summary(): {[key: string]: StylingSummary} {
|
||||
const contextEntries = this._contextDebug.entries;
|
||||
const finalValues: {[key: string]: {value: string, bindingIndex: number}} = {};
|
||||
this._mapValues((prop: string, value: any, bindingIndex: number) => {
|
||||
finalValues[prop] = {value, bindingIndex};
|
||||
});
|
||||
|
||||
const entries: {[key: string]: StylingSummary} = {};
|
||||
const values = this.values;
|
||||
const props = Object.keys(values);
|
||||
for (let i = 0; i < props.length; i++) {
|
||||
const prop = props[i];
|
||||
const contextEntry = contextEntries[prop];
|
||||
const sourceValues = contextEntry.sources.map(v => {
|
||||
let value: string|number|null;
|
||||
let bindingIndex: number|null;
|
||||
if (typeof v === 'number') {
|
||||
value = this._data[v];
|
||||
bindingIndex = v;
|
||||
} else {
|
||||
value = v;
|
||||
bindingIndex = null;
|
||||
}
|
||||
return {bindingIndex, value};
|
||||
});
|
||||
|
||||
const finalValue = finalValues[prop] !;
|
||||
let bindingIndex: number|null = finalValue.bindingIndex;
|
||||
bindingIndex = bindingIndex === DEFAULT_BINDING_INDEX_VALUE ? null : bindingIndex;
|
||||
|
||||
entries[prop] = {prop, value: finalValue.value, bindingIndex, sourceValues};
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a key/value map of all the styles/classes that were last applied to the element.
|
||||
*/
|
||||
get values(): {[key: string]: any} {
|
||||
const entries: {[key: string]: any} = {};
|
||||
this._mapValues((prop: string, value: any) => { entries[prop] = value; });
|
||||
return entries;
|
||||
}
|
||||
|
||||
private _mapValues(fn: (prop: string, value: any, bindingIndex: number) => any) {
|
||||
// there is no need to store/track an element instance. The
|
||||
// element is only used when the styling algorithm attempts to
|
||||
// style the value (and we mock out the stylingApplyFn anyway).
|
||||
const mockElement = {} as any;
|
||||
|
||||
const mapFn =
|
||||
(renderer: any, element: RElement, prop: string, value: any, bindingIndex: number) => {
|
||||
fn(prop, value, bindingIndex);
|
||||
};
|
||||
applyStyling(this.context, null, mockElement, this._data, BIT_MASK_APPLY_ALL, mapFn);
|
||||
}
|
||||
}
|
82
packages/core/src/render3/styling_next/util.ts
Normal file
82
packages/core/src/render3/styling_next/util.ts
Normal file
@ -0,0 +1,82 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {StylingContext} from '../interfaces/styling';
|
||||
import {getProp as getOldProp, getSinglePropIndexValue as getOldSinglePropIndexValue} from '../styling/class_and_style_bindings';
|
||||
import {TStylingConfigFlags, TStylingContext, TStylingContextIndex} from './interfaces';
|
||||
|
||||
/**
|
||||
* Creates a new instance of the `TStylingContext`.
|
||||
*/
|
||||
export function allocStylingContext(): TStylingContext {
|
||||
return [TStylingConfigFlags.Initial, 0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporary function that allows for a string-based property name to be
|
||||
* obtained from an index-based property identifier.
|
||||
*
|
||||
* This function will be removed once the new styling refactor code (which
|
||||
* lives inside of `render3/styling_next/`) replaces the existing styling
|
||||
* implementation.
|
||||
*/
|
||||
export function getBindingNameFromIndex(
|
||||
stylingContext: StylingContext, offset: number, directiveIndex: number, isClassBased: boolean) {
|
||||
const singleIndex =
|
||||
getOldSinglePropIndexValue(stylingContext, directiveIndex, offset, isClassBased);
|
||||
return getOldProp(stylingContext, singleIndex);
|
||||
}
|
||||
|
||||
export function updateContextDirectiveIndex(context: TStylingContext, index: number) {
|
||||
context[TStylingContextIndex.MaxDirectiveIndexPosition] = index;
|
||||
}
|
||||
|
||||
function getConfig(context: TStylingContext) {
|
||||
return context[TStylingContextIndex.ConfigPosition];
|
||||
}
|
||||
|
||||
export function setConfig(context: TStylingContext, value: number) {
|
||||
context[TStylingContextIndex.ConfigPosition] = value;
|
||||
}
|
||||
|
||||
export function getProp(context: TStylingContext, index: number) {
|
||||
return context[index + TStylingContextIndex.PropOffset] as string;
|
||||
}
|
||||
|
||||
export function getGuardMask(context: TStylingContext, index: number) {
|
||||
return context[index + TStylingContextIndex.MaskOffset] as number;
|
||||
}
|
||||
|
||||
export function getValuesCount(context: TStylingContext, index: number) {
|
||||
return context[index + TStylingContextIndex.ValuesCountOffset] as number;
|
||||
}
|
||||
|
||||
export function getValue(context: TStylingContext, index: number, offset: number) {
|
||||
return context[index + TStylingContextIndex.BindingsStartOffset + offset] as number | string;
|
||||
}
|
||||
|
||||
export function getDefaultValue(context: TStylingContext, index: number): string|boolean|null {
|
||||
const valuesCount = getValuesCount(context, index);
|
||||
return context[index + TStylingContextIndex.BindingsStartOffset + valuesCount - 1] as string |
|
||||
boolean | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporary function which determines whether or not a context is
|
||||
* allowed to be flushed based on the provided directive index.
|
||||
*/
|
||||
export function allowStylingFlush(context: TStylingContext, index: number) {
|
||||
return index === context[TStylingContextIndex.MaxDirectiveIndexPosition];
|
||||
}
|
||||
|
||||
export function lockContext(context: TStylingContext) {
|
||||
setConfig(context, getConfig(context) | TStylingConfigFlags.Locked);
|
||||
}
|
||||
|
||||
export function isContextLocked(context: TStylingContext): boolean {
|
||||
return (getConfig(context) & TStylingConfigFlags.Locked) > 0;
|
||||
}
|
10
packages/core/src/render3/util/debug_utils.ts
Normal file
10
packages/core/src/render3/util/debug_utils.ts
Normal file
@ -0,0 +1,10 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
export function attachDebugObject(obj: any, debug: any) {
|
||||
Object.defineProperty(obj, 'debug', {value: debug, enumerable: false});
|
||||
}
|
Reference in New Issue
Block a user