refactor(ivy): Implement `computeStaticStyling` (#34418)

The `computeStaticStyling` will be used for computing static styling value during `firstCreatePass`.

The function takes into account static styling from the template as well as from the host bindings. The host bindings need to be merged in front of the template so that they have the correct priority.

PR Closes #34418
This commit is contained in:
Misko Hevery 2019-12-15 21:09:02 -08:00 committed by Miško Hevery
parent 54af220107
commit b7ff38b1ef
13 changed files with 179 additions and 38 deletions

View File

@ -12,7 +12,7 @@
"master": { "master": {
"uncompressed": { "uncompressed": {
"runtime-es2015": 1485, "runtime-es2015": 1485,
"main-es2015": 18271, "main-es2015": 18214,
"polyfills-es2015": 36808 "polyfills-es2015": 36808
} }
} }
@ -30,7 +30,7 @@
"master": { "master": {
"uncompressed": { "uncompressed": {
"runtime-es2015": 1485, "runtime-es2015": 1485,
"main-es2015": 137141, "main-es2015": 139487,
"polyfills-es2015": 37494 "polyfills-es2015": 37494
} }
} }
@ -39,7 +39,7 @@
"master": { "master": {
"uncompressed": { "uncompressed": {
"runtime-es2015": 2289, "runtime-es2015": 2289,
"main-es2015": 267182, "main-es2015": 268796,
"polyfills-es2015": 36808, "polyfills-es2015": 36808,
"5-es2015": 751 "5-es2015": 751
} }
@ -49,7 +49,7 @@
"master": { "master": {
"uncompressed": { "uncompressed": {
"runtime-es2015": 2289, "runtime-es2015": 2289,
"main-es2015": 226288, "main-es2015": 228770,
"polyfills-es2015": 36808, "polyfills-es2015": 36808,
"5-es2015": 779 "5-es2015": 779
} }

View File

@ -171,7 +171,7 @@ export function ɵɵelement(
} }
function setDirectiveStylingInput( function setDirectiveStylingInput(
context: TStylingContext | StylingMapArray | null, lView: LView, context: TStylingContext | StylingMapArray | string | null, lView: LView,
stylingInputs: (string | number)[], propName: string) { stylingInputs: (string | number)[], propName: string) {
// older versions of Angular treat the input as `null` in the // older versions of Angular treat the input as `null` in the
// event that the value does not exist at all. For this reason // event that the value does not exist at all. For this reason

View File

@ -553,7 +553,8 @@ export function registerInitialStylingOnTNode(
return hasAdditionalInitialStyling; return hasAdditionalInitialStyling;
} }
function updateRawValueOnContext(context: TStylingContext | StylingMapArray, value: string) { function updateRawValueOnContext(
context: TStylingContext | StylingMapArray | string, value: string) {
const stylingMapArr = getStylingMapArray(context) !; const stylingMapArr = getStylingMapArray(context) !;
stylingMapArr[StylingMapArrayIndex.RawValuePosition] = value; stylingMapArr[StylingMapArrayIndex.RawValuePosition] = value;
} }

View File

@ -600,7 +600,8 @@ export interface TNode {
* are encountered. If and when this happens then the existing `StylingMapArray` value * are encountered. If and when this happens then the existing `StylingMapArray` value
* will be placed into the initial styling slot in the newly created `TStylingContext`. * will be placed into the initial styling slot in the newly created `TStylingContext`.
*/ */
styles: StylingMapArray|TStylingContext|null; // TODO(misko): `Remove StylingMapArray|TStylingContext|null` in follow up PR.
styles: StylingMapArray|TStylingContext|string|null;
/** /**
* A collection of all class bindings and/or static class values for an element. * A collection of all class bindings and/or static class values for an element.
@ -620,7 +621,8 @@ export interface TNode {
* are encountered. If and when this happens then the existing `StylingMapArray` value * are encountered. If and when this happens then the existing `StylingMapArray` value
* will be placed into the initial styling slot in the newly created `TStylingContext`. * will be placed into the initial styling slot in the newly created `TStylingContext`.
*/ */
classes: StylingMapArray|TStylingContext|null; // TODO(misko): `Remove StylingMapArray|TStylingContext|null` in follow up PR.
classes: StylingMapArray|TStylingContext|string|null;
/** /**
* Stores the head/tail index of the class bindings. * Stores the head/tail index of the class bindings.

View File

@ -1030,8 +1030,8 @@ export const setStyleAttr = (renderer: Renderer3 | null, native: RElement, value
* initial styling values on an element. * initial styling values on an element.
*/ */
export function renderStylingMap( export function renderStylingMap(
renderer: Renderer3, element: RElement, stylingValues: TStylingContext | StylingMapArray | null, renderer: Renderer3, element: RElement,
isClassBased: boolean): void { stylingValues: TStylingContext | StylingMapArray | string | null, isClassBased: boolean): void {
const stylingMapArr = getStylingMapArray(stylingValues); const stylingMapArr = getStylingMapArray(stylingValues);
if (stylingMapArr) { if (stylingMapArr) {
for (let i = StylingMapArrayIndex.ValuesStartPosition; i < stylingMapArr.length; for (let i = StylingMapArrayIndex.ValuesStartPosition; i < stylingMapArr.length;

View File

@ -0,0 +1,43 @@
/**
* @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 {concatStringsWithSpace} from '../../util/stringify';
import {assertFirstCreatePass} from '../assert';
import {AttributeMarker, TAttributes, TNode} from '../interfaces/node';
import {TVIEW} from '../interfaces/view';
import {getLView} from '../state';
/**
* Compute the static styling (class/style) from `TAttributes`.
*
* This function should be called during `firstCreatePass` only.
*
* @param tNode The `TNode` into which the styling information should be loaded.
* @param attrs `TAttributes` containing the styling information.
*/
export function computeStaticStyling(tNode: TNode, attrs: TAttributes): void {
ngDevMode && assertFirstCreatePass(
getLView()[TVIEW], 'Expecting to be called in first template pass only');
let styles: string|null = tNode.styles as string | null;
let classes: string|null = tNode.classes as string | null;
let mode: AttributeMarker|0 = 0;
for (let i = 0; i < attrs.length; i++) {
const value = attrs[i];
if (typeof value === 'number') {
mode = value;
} else if (mode == AttributeMarker.Classes) {
classes = concatStringsWithSpace(classes, value as string);
} else if (mode == AttributeMarker.Styles) {
const style = value as string;
const styleValue = attrs[++i] as string;
styles = concatStringsWithSpace(styles, style + ': ' + styleValue + ';');
}
}
styles !== null && (tNode.styles = styles);
classes !== null && (tNode.classes = classes);
}

View File

@ -10,12 +10,12 @@ import {stylePropNeedsSanitization, ɵɵsanitizeStyle} from '../../sanitization/
import {assertEqual, throwError} from '../../util/assert'; import {assertEqual, throwError} from '../../util/assert';
import {TNode} from '../interfaces/node'; import {TNode} from '../interfaces/node';
import {SanitizerFn} from '../interfaces/sanitization'; import {SanitizerFn} from '../interfaces/sanitization';
import {StylingMapArray, TStylingKey, TStylingMapKey, TStylingRange, getTStylingRangeNext, getTStylingRangePrev, getTStylingRangePrevDuplicate, setTStylingRangeNext, setTStylingRangeNextDuplicate, setTStylingRangePrev, setTStylingRangePrevDuplicate, toTStylingRange} from '../interfaces/styling'; import {TStylingKey, TStylingMapKey, TStylingRange, getTStylingRangeNext, getTStylingRangePrev, getTStylingRangePrevDuplicate, setTStylingRangeNext, setTStylingRangeNextDuplicate, setTStylingRangePrev, setTStylingRangePrevDuplicate, toTStylingRange} from '../interfaces/styling';
import {LView, TData, TVIEW} from '../interfaces/view'; import {LView, TData, TVIEW} from '../interfaces/view';
import {getLView} from '../state'; import {getLView} from '../state';
import {splitClassList, toggleClass} from './class_differ'; import {splitClassList, toggleClass} from './class_differ';
import {StyleChangesMap, parseKeyValue, removeStyle} from './style_differ'; import {StyleChangesMap, parseKeyValue, removeStyle} from './style_differ';
import {getLastParsedKey, parseClassName, parseClassNameNext, parseStyle, parseStyleNext} from './styling_parser';
@ -254,8 +254,10 @@ export function insertTStylingBinding(
// Now we need to update / compute the duplicates. // Now we need to update / compute the duplicates.
// Starting with our location search towards head (least priority) // Starting with our location search towards head (least priority)
markDuplicates(tData, tStylingKey, index, isClassBinding ? tNode.classes : tNode.styles, true); markDuplicates(
markDuplicates(tData, tStylingKey, index, null, false); tData, tStylingKey, index, (isClassBinding ? tNode.classes : tNode.styles) as string, true,
isClassBinding);
markDuplicates(tData, tStylingKey, index, '', false, isClassBinding);
tBindings = toTStylingRange(tmplHead, tmplTail); tBindings = toTStylingRange(tmplHead, tmplTail);
if (isClassBinding) { if (isClassBinding) {
@ -320,8 +322,8 @@ export function insertTStylingBinding(
* @param isPrevDir * @param isPrevDir
*/ */
function markDuplicates( function markDuplicates(
tData: TData, tStylingKey: TStylingKey, index: number, staticValues: StylingMapArray | null, tData: TData, tStylingKey: TStylingKey, index: number, staticValues: string, isPrevDir: boolean,
isPrevDir: boolean) { isClassBinding: boolean) {
const tStylingAtIndex = tData[index + 1] as TStylingRange; const tStylingAtIndex = tData[index + 1] as TStylingRange;
const key: string|null = typeof tStylingKey === 'object' ? tStylingKey.key : tStylingKey; const key: string|null = typeof tStylingKey === 'object' ? tStylingKey.key : tStylingKey;
const isMap = key === null; const isMap = key === null;
@ -345,16 +347,19 @@ function markDuplicates(
cursor = isPrevDir ? getTStylingRangePrev(tStyleRangeAtCursor) : cursor = isPrevDir ? getTStylingRangePrev(tStyleRangeAtCursor) :
getTStylingRangeNext(tStyleRangeAtCursor); getTStylingRangeNext(tStyleRangeAtCursor);
} }
if (staticValues !== null && // If we have static values to search if (staticValues !== '' && // If we have static values to search
!foundDuplicate // If we have duplicate don't bother since we are already marked as !foundDuplicate // If we have duplicate don't bother since we are already marked as
// duplicate // duplicate
) { ) {
if (isMap) { if (isMap) {
// if we are a Map (and we have statics) we must assume duplicate // if we are a Map (and we have statics) we must assume duplicate
foundDuplicate = true; foundDuplicate = true;
} else { } else if (staticValues != null) {
for (let i = 1; foundDuplicate === false && i < staticValues.length; i = i + 2) { for (let i = isClassBinding ? parseClassName(staticValues) : parseStyle(staticValues); //
if (staticValues[i] === key) { i >= 0; //
i = isClassBinding ? parseClassNameNext(staticValues, i) :
parseStyleNext(staticValues, i)) {
if (getLastParsedKey(staticValues) === key) {
foundDuplicate = true; foundDuplicate = true;
break; break;
} }
@ -386,8 +391,9 @@ export function flushStyleBinding(
// When styling changes we don't have to start at the begging. Instead we start at the change // When styling changes we don't have to start at the begging. Instead we start at the change
// value and look up the previous concatenation as a starting point going forward. // value and look up the previous concatenation as a starting point going forward.
const lastUnchangedValueIndex = getTStylingRangePrev(tStylingRangeAtIndex); const lastUnchangedValueIndex = getTStylingRangePrev(tStylingRangeAtIndex);
let text = lastUnchangedValueIndex === 0 ? getStaticStylingValue(tNode, isClassBinding) : let text = lastUnchangedValueIndex === 0 ?
lView[lastUnchangedValueIndex + 1] as string; ((isClassBinding ? tNode.classes : tNode.styles) as string) :
lView[lastUnchangedValueIndex + 1] as string;
let cursor = index; let cursor = index;
while (cursor !== 0) { while (cursor !== 0) {
const value = lView[cursor]; const value = lView[cursor];
@ -400,16 +406,6 @@ export function flushStyleBinding(
return text; return text;
} }
/**
* Retrieves the static value for styling.
*
* @param tNode
* @param isClassBinding
*/
function getStaticStylingValue(tNode: TNode, isClassBinding: Boolean) {
// TODO(misko): implement once we have more code integrated.
return '';
}
/** /**
* Append new styling to the currently concatenated styling text. * Append new styling to the currently concatenated styling text.

View File

@ -219,8 +219,10 @@ export function hyphenate(value: string): string {
* will copy over an initial styling values from the tNode (which are stored as a * will copy over an initial styling values from the tNode (which are stored as a
* `StylingMapArray` on the `tNode.classes` or `tNode.styles` values). * `StylingMapArray` on the `tNode.classes` or `tNode.styles` values).
*/ */
export function getStylingMapArray(value: TStylingContext | StylingMapArray | null): export function getStylingMapArray(value: TStylingContext | StylingMapArray | string | null):
StylingMapArray|null { StylingMapArray|null {
// TODO(misko): remove after TNode.classes/styles becomes `string` only
if (typeof value === 'string') return null;
return isStylingContext(value) ? return isStylingContext(value) ?
(value as TStylingContext)[TStylingContextIndex.InitialStylingValuePosition] : (value as TStylingContext)[TStylingContextIndex.InitialStylingValuePosition] :
value as StylingMapArray; value as StylingMapArray;
@ -240,7 +242,10 @@ export function isStylingMapArray(value: any): boolean {
(typeof(value as StylingMapArray)[StylingMapArrayIndex.ValuesStartPosition] === 'string'); (typeof(value as StylingMapArray)[StylingMapArrayIndex.ValuesStartPosition] === 'string');
} }
export function getInitialStylingValue(context: TStylingContext | StylingMapArray | null): string { export function getInitialStylingValue(context: TStylingContext | StylingMapArray | string | null):
string {
// TODO(misko): remove after TNode.classes/styles becomes `string` only
if (typeof context === 'string') return context;
const map = getStylingMapArray(context); const map = getStylingMapArray(context);
return map && (map[StylingMapArrayIndex.RawValuePosition] as string | null) || ''; return map && (map[StylingMapArrayIndex.RawValuePosition] as string | null) || '';
} }

View File

@ -36,3 +36,18 @@ export function stringify(token: any): string {
const newLineIndex = res.indexOf('\n'); const newLineIndex = res.indexOf('\n');
return newLineIndex === -1 ? res : res.substring(0, newLineIndex); return newLineIndex === -1 ? res : res.substring(0, newLineIndex);
} }
/**
* Concatenates two strings with separator, allocating new strings only when necessary.
*
* @param before before string.
* @param separator separator string.
* @param after after string.
* @returns concatenated string.
*/
export function concatStringsWithSpace(before: string | null, after: string | null): string {
return (before == null || before === '') ?
(after === null ? '' : after) :
((after == null || after === '') ? before : before + ' ' + after);
}

View File

@ -0,0 +1,49 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {createTNode} from '@angular/core/src/render3/instructions/shared';
import {AttributeMarker, TAttributes, TNode, TNodeType} from '@angular/core/src/render3/interfaces/node';
import {LView} from '@angular/core/src/render3/interfaces/view';
import {enterView} from '@angular/core/src/render3/state';
import {computeStaticStyling} from '@angular/core/src/render3/styling/static_styling';
describe('static styling', () => {
const mockFirstCreatePassLView: LView = [null, {firstCreatePass: true}] as any;
let tNode !: TNode;
beforeEach(() => {
enterView(mockFirstCreatePassLView, null);
tNode = createTNode(null !, null !, TNodeType.Element, 0, '', null);
});
it('should initialize when no attrs', () => {
computeStaticStyling(tNode, []);
expect(tNode.classes).toEqual(null);
expect(tNode.styles).toEqual(null);
});
it('should initialize from attrs', () => {
const tAttrs: TAttributes = [
'ignore', //
AttributeMarker.Classes, 'my-class', //
AttributeMarker.Styles, 'color', 'red' //
];
computeStaticStyling(tNode, tAttrs);
expect(tNode.classes).toEqual('my-class');
expect(tNode.styles).toEqual('color: red;');
});
it('should initialize from attrs when multiple', () => {
const tAttrs: TAttributes = [
'ignore', //
AttributeMarker.Classes, 'my-class', 'other', //
AttributeMarker.Styles, 'color', 'red', 'width', '100px' //
];
computeStaticStyling(tNode, tAttrs);
expect(tNode.classes).toEqual('my-class other');
expect(tNode.styles).toEqual('color: red; width: 100px;');
});
});

View File

@ -115,6 +115,7 @@ describe('TNode styling linked list', () => {
// ɵɵstyleProp('color', '#008'); // Binding index: 30 // ɵɵstyleProp('color', '#008'); // Binding index: 30
const tNode = createTNode(null !, null !, TNodeType.Element, 0, '', null); const tNode = createTNode(null !, null !, TNodeType.Element, 0, '', null);
tNode.styles = '';
const tData: TData = newArray(32, null); const tData: TData = newArray(32, null);
const STYLE = STYLE_MAP_STYLING_KEY; const STYLE = STYLE_MAP_STYLING_KEY;
@ -408,7 +409,7 @@ describe('TNode styling linked list', () => {
it('should mark duplicate on static fields', () => { it('should mark duplicate on static fields', () => {
const tNode = createTNode(null !, null !, TNodeType.Element, 0, '', null); const tNode = createTNode(null !, null !, TNodeType.Element, 0, '', null);
tNode.styles = [null, 'color', 'blue']; tNode.styles = 'color: blue;';
const tData: TData = [null, null]; const tData: TData = [null, null];
insertTStylingBinding(tData, tNode, 'width', 2, false, false); insertTStylingBinding(tData, tNode, 'width', 2, false, false);
expectPriorityOrder(tData, tNode, false).toEqual([ expectPriorityOrder(tData, tNode, false).toEqual([
@ -636,6 +637,8 @@ class StylingFixture {
lView: LView = [null, null !] as any; lView: LView = [null, null !] as any;
tNode: TNode = createTNode(null !, null !, TNodeType.Element, 0, '', null); tNode: TNode = createTNode(null !, null !, TNodeType.Element, 0, '', null);
constructor(bindingSources: TStylingKey[][], public isClassBinding: boolean) { constructor(bindingSources: TStylingKey[][], public isClassBinding: boolean) {
this.tNode.classes = '';
this.tNode.styles = '';
let bindingIndex = this.tData.length; let bindingIndex = this.tData.length;
for (let i = 0; i < bindingSources.length; i++) { for (let i = 0; i < bindingSources.length; i++) {
const bindings = bindingSources[i]; const bindings = bindingSources[i];

View File

@ -0,0 +1,27 @@
/**
* @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 {concatStringsWithSpace} from '@angular/core/src/util/stringify';
describe('stringify', () => {
describe('concatStringsWithSpace', () => {
it('should concat with null', () => {
expect(concatStringsWithSpace(null, null)).toEqual('');
expect(concatStringsWithSpace('a', null)).toEqual('a');
expect(concatStringsWithSpace(null, 'b')).toEqual('b');
});
it('should concat when empty', () => {
expect(concatStringsWithSpace('', '')).toEqual('');
expect(concatStringsWithSpace('a', '')).toEqual('a');
expect(concatStringsWithSpace('', 'b')).toEqual('b');
});
it('should concat when not empty',
() => { expect(concatStringsWithSpace('before', 'after')).toEqual('before after'); });
});
});

View File

@ -786,7 +786,7 @@ export declare const ɵɵdefineDirective: <T>(directiveDefinition: {
features?: DirectiveDefFeature[] | undefined; features?: DirectiveDefFeature[] | undefined;
hostBindings?: HostBindingsFunction<T> | undefined; hostBindings?: HostBindingsFunction<T> | undefined;
hostVars?: number | undefined; hostVars?: number | undefined;
hostAttrs?: (string | (string | SelectorFlags)[] | AttributeMarker)[] | undefined; hostAttrs?: TAttributes | undefined;
contentQueries?: ContentQueriesFunction<T> | undefined; contentQueries?: ContentQueriesFunction<T> | undefined;
viewQuery?: ViewQueriesFunction<T> | null | undefined; viewQuery?: ViewQueriesFunction<T> | null | undefined;
exportAs?: string[] | undefined; exportAs?: string[] | undefined;