From f03475cac8bbc79aaf13caf94b099b0c234028bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Wed, 8 May 2019 16:30:28 -0700 Subject: [PATCH] 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 --- integration/_payload-limits.json | 2 +- .../compiler/src/render3/view/compiler.ts | 2 +- .../src/render3/view/styling_builder.ts | 6 + .../src/render3/view/styling_state.ts | 37 ++ packages/core/src/render3/debug.ts | 26 +- .../core/src/render3/instructions/element.ts | 9 +- .../core/src/render3/instructions/shared.ts | 4 + .../core/src/render3/instructions/styling.ts | 51 ++- packages/core/src/render3/interfaces/node.ts | 6 +- packages/core/src/render3/jit/environment.ts | 5 +- packages/core/src/render3/state.ts | 21 ++ .../styling/class_and_style_bindings.ts | 2 +- .../core/src/render3/styling_next/bindings.ts | 356 ++++++++++++++++++ .../src/render3/styling_next/instructions.ts | 211 +++++++++++ .../src/render3/styling_next/interfaces.ts | 239 ++++++++++++ .../core/src/render3/styling_next/state.ts | 37 ++ .../src/render3/styling_next/styling_debug.ts | 210 +++++++++++ .../core/src/render3/styling_next/util.ts | 82 ++++ packages/core/src/render3/util/debug_utils.ts | 10 + .../core/test/acceptance/styling_next_spec.ts | 330 ++++++++++++++++ .../cyclic_import/bundle.golden_symbols.json | 45 +++ .../bundling/todo/bundle.golden_symbols.json | 153 +++++++- .../styling_next/styling_context_spec.ts | 93 +++++ .../styling_next/styling_debug_spec.ts | 82 ++++ 24 files changed, 1993 insertions(+), 26 deletions(-) create mode 100644 packages/compiler/src/render3/view/styling_state.ts create mode 100644 packages/core/src/render3/styling_next/bindings.ts create mode 100644 packages/core/src/render3/styling_next/instructions.ts create mode 100644 packages/core/src/render3/styling_next/interfaces.ts create mode 100644 packages/core/src/render3/styling_next/state.ts create mode 100644 packages/core/src/render3/styling_next/styling_debug.ts create mode 100644 packages/core/src/render3/styling_next/util.ts create mode 100644 packages/core/src/render3/util/debug_utils.ts create mode 100644 packages/core/test/acceptance/styling_next_spec.ts create mode 100644 packages/core/test/render3/styling_next/styling_context_spec.ts create mode 100644 packages/core/test/render3/styling_next/styling_debug_spec.ts diff --git a/integration/_payload-limits.json b/integration/_payload-limits.json index 1b13ca56bd..9d62c9f0fa 100644 --- a/integration/_payload-limits.json +++ b/integration/_payload-limits.json @@ -21,7 +21,7 @@ "master": { "uncompressed": { "runtime": 1440, - "main": 146225, + "main": 147764, "polyfills": 43567 } } diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index 92dcd4cf53..7ea88f0221 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -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)); }); } diff --git a/packages/compiler/src/render3/view/styling_builder.ts b/packages/compiler/src/render3/view/styling_builder.ts index 0260b29f1f..b355d20cb7 100644 --- a/packages/compiler/src/render3/view/styling_builder.ts +++ b/packages/compiler/src/render3/view/styling_builder.ts @@ -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, diff --git a/packages/compiler/src/render3/view/styling_state.ts b/packages/compiler/src/render3/view/styling_state.ts new file mode 100644 index 0000000000..c63ab6d8e6 --- /dev/null +++ b/packages/compiler/src/render3/view/styling_state.ts @@ -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; +} diff --git a/packages/core/src/render3/debug.ts b/packages/core/src/render3/debug.ts index f0cf7258ed..e8d9997b76 100644 --- a/packages/core/src/render3/debug.ts +++ b/packages/core/src/render3/debug.ts @@ -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; } diff --git a/packages/core/src/render3/instructions/element.ts b/packages/core/src/render3/instructions/element.ts index 5f8e81a8c7..2fdb61954e 100644 --- a/packages/core/src/render3/instructions/element.ts +++ b/packages/core/src/render3/instructions/element.ts @@ -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); diff --git a/packages/core/src/render3/instructions/shared.ts b/packages/core/src/render3/instructions/shared.ts index df17271738..d3a849e7a0 100644 --- a/packages/core/src/render3/instructions/shared.ts +++ b/packages/core/src/render3/instructions/shared.ts @@ -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, }; } diff --git a/packages/core/src/render3/instructions/styling.ts b/packages/core/src/render3/instructions/styling.ts index 2d2911ba95..6fd0186849 100644 --- a/packages/core/src/render3/instructions/styling.ts +++ b/packages/core/src/render3/instructions/styling.ts @@ -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() { diff --git a/packages/core/src/render3/interfaces/node.ts b/packages/core/src/render3/interfaces/node.ts index c4f476ce7c..cb6f99b466 100644 --- a/packages/core/src/render3/interfaces/node.ts +++ b/packages/core/src/render3/interfaces/node.ts @@ -5,7 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - +import {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 */ diff --git a/packages/core/src/render3/jit/environment.ts b/packages/core/src/render3/jit/environment.ts index 1518a0e7f6..c246c12672 100644 --- a/packages/core/src/render3/jit/environment.ts +++ b/packages/core/src/render3/jit/environment.ts @@ -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'; + /** diff --git a/packages/core/src/render3/state.ts b/packages/core/src/render3/state.ts index 7234afc2c2..647446ae32 100644 --- a/packages/core/src/render3/state.ts +++ b/packages/core/src/render3/state.ts @@ -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 `
` is used then the super class height is `1` + * // if `
` 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. * diff --git a/packages/core/src/render3/styling/class_and_style_bindings.ts b/packages/core/src/render3/styling/class_and_style_bindings.ts index 7f08b2748a..4a30e99837 100644 --- a/packages/core/src/render3/styling/class_and_style_bindings.ts +++ b/packages/core/src/render3/styling/class_and_style_bindings.ts @@ -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] diff --git a/packages/core/src/render3/styling_next/bindings.ts b/packages/core/src/render3/styling_next/bindings.ts new file mode 100644 index 0000000000..d3ef4c9c7e --- /dev/null +++ b/packages/core/src/render3/styling_next/bindings.ts @@ -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. `
`) 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); + } + } + }; diff --git a/packages/core/src/render3/styling_next/instructions.ts b/packages/core/src/render3/styling_next/instructions.ts new file mode 100644 index 0000000000..81b8b33d4b --- /dev/null +++ b/packages/core/src/render3/styling_next/instructions.ts @@ -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); +} diff --git a/packages/core/src/render3/styling_next/interfaces.ts b/packages/core/src/render3/styling_next/interfaces.ts new file mode 100644 index 0000000000..3ccdcf1142 --- /dev/null +++ b/packages/core/src/render3/styling_next/interfaces.ts @@ -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 + * //
// 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: + * + * ``` + *
// 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. + * `
` 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. `
`): + * + * ```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 { + [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)[]; diff --git a/packages/core/src/render3/styling_next/state.ts b/packages/core/src/render3/styling_next/state.ts new file mode 100644 index 0000000000..d4c5d281e2 --- /dev/null +++ b/packages/core/src/render3/styling_next/state.ts @@ -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; +} diff --git a/packages/core/src/render3/styling_next/styling_debug.ts b/packages/core/src/render3/styling_next/styling_debug.ts new file mode 100644 index 0000000000..5dda787365 --- /dev/null +++ b/packages/core/src/render3/styling_next/styling_debug.ts @@ -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); + } +} diff --git a/packages/core/src/render3/styling_next/util.ts b/packages/core/src/render3/styling_next/util.ts new file mode 100644 index 0000000000..13a76aa3f3 --- /dev/null +++ b/packages/core/src/render3/styling_next/util.ts @@ -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; +} diff --git a/packages/core/src/render3/util/debug_utils.ts b/packages/core/src/render3/util/debug_utils.ts new file mode 100644 index 0000000000..3c30200ed1 --- /dev/null +++ b/packages/core/src/render3/util/debug_utils.ts @@ -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}); +} diff --git a/packages/core/test/acceptance/styling_next_spec.ts b/packages/core/test/acceptance/styling_next_spec.ts new file mode 100644 index 0000000000..8ca9277a45 --- /dev/null +++ b/packages/core/test/acceptance/styling_next_spec.ts @@ -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: ` +
+ ` + }) + 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: ` +
+ ` + }) + 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: ` + ... + ` + }) + 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: ` +
+ ` + }) + 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: `
`}) + 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; +} diff --git a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json index a57178edd4..94c5b7ccc8 100644 --- a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json +++ b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json @@ -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" }, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 1fbcba97b2..be0d39505d 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -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" }, diff --git a/packages/core/test/render3/styling_next/styling_context_spec.ts b/packages/core/test/render3/styling_next/styling_context_spec.ts new file mode 100644 index 0000000000..ff21327acd --- /dev/null +++ b/packages/core/test/render3/styling_next/styling_context_spec.ts @@ -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; +} diff --git a/packages/core/test/render3/styling_next/styling_debug_spec.ts b/packages/core/test/render3/styling_next/styling_debug_spec.ts new file mode 100644 index 0000000000..a43e1fd107 --- /dev/null +++ b/packages/core/test/render3/styling_next/styling_debug_spec.ts @@ -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); +}