perf(ivy): apply [style]/[class] bindings directly to style/className (#33336)

This patch ensures that the `[style]` and `[class]` based bindings
are directly applied to an element's style and className attributes.

This patch optimizes the algorithm so that it...
- Doesn't construct an update an instance of `StylingMapArray` for
  `[style]` and `[class]` bindings
- Doesn't apply `[style]` and `[class]` based entries using
  `classList` and `style` (direct attributes are used instead)
- Doesn't split or iterate over all string-based tokens in a
  string value obtained from a `[class]` binding.

This patch speeds up the following cases:
- `<div [class]>` and `<div class="..." [class]>`
- `<div [style]>` and `<div style="..." [style]>`

The overall speec increase is by over 5x.

PR Close #33336
This commit is contained in:
Matias Niemelä
2019-10-22 15:18:40 -07:00
committed by Andrew Kushnir
parent ee4fc12e42
commit dcdb433b7d
13 changed files with 359 additions and 102 deletions

View File

@ -96,7 +96,7 @@ export function stylePropInternal(
// in this case we do not need to do anything, but the binding index
// still needs to be incremented because all styling binding values
// are stored inside of the lView.
const bindingIndex = lView[BINDING_INDEX]++;
const bindingIndex = getAndIncrementBindingIndex(lView, false);
const updated =
stylingProp(elementIndex, bindingIndex, prop, resolveStylePropValue(value, suffix), false);
@ -130,7 +130,7 @@ export function ɵɵclassProp(className: string, value: boolean | null): void {
// in this case we do not need to do anything, but the binding index
// still needs to be incremented because all styling binding values
// are stored inside of the lView.
const bindingIndex = lView[BINDING_INDEX]++;
const bindingIndex = getAndIncrementBindingIndex(lView, false);
const updated = stylingProp(getSelectedIndex(), bindingIndex, className, value, true);
if (ngDevMode) {
@ -189,8 +189,7 @@ function stylingProp(
const sanitizerToUse = isClassBased ? null : sanitizer;
const renderer = getRenderer(tNode, lView);
updated = applyStylingValueDirectly(
renderer, context, native, lView, bindingIndex, prop, value, isClassBased,
isClassBased ? setClass : setStyle, sanitizerToUse);
renderer, context, native, lView, bindingIndex, prop, value, isClassBased, sanitizerToUse);
if (sanitizerToUse) {
// it's important we remove the current style sanitizer once the
@ -243,28 +242,23 @@ export function ɵɵstyleMap(styles: {[styleName: string]: any} | NO_CHANGE | nu
const lView = getLView();
const tNode = getTNode(index, lView);
const context = getStylesContext(tNode);
const hasDirectiveInput = hasStyleInput(tNode);
// if a value is interpolated then it may render a `NO_CHANGE` value.
// in this case we do not need to do anything, but the binding index
// still needs to be incremented because all styling binding values
// are stored inside of the lView.
const bindingIndex = lView[BINDING_INDEX]++;
const bindingIndex = getAndIncrementBindingIndex(lView, true);
// inputs are only evaluated from a template binding into a directive, therefore,
// there should not be a situation where a directive host bindings function
// evaluates the inputs (this should only happen in the template function)
if (!isHostStyling() && hasStyleInput(tNode) && styles !== NO_CHANGE) {
if (!isHostStyling() && hasDirectiveInput && styles !== NO_CHANGE) {
updateDirectiveInputValue(context, lView, tNode, bindingIndex, styles, false);
styles = NO_CHANGE;
}
const updated = stylingMap(index, context, bindingIndex, styles, false);
if (ngDevMode) {
ngDevMode.styleMap++;
if (updated) {
ngDevMode.styleMapCacheMiss++;
}
}
stylingMap(context, tNode, lView, bindingIndex, styles, false, hasDirectiveInput);
}
/**
@ -300,28 +294,23 @@ export function classMapInternal(
const lView = getLView();
const tNode = getTNode(elementIndex, lView);
const context = getClassesContext(tNode);
const hasDirectiveInput = hasClassInput(tNode);
// if a value is interpolated then it may render a `NO_CHANGE` value.
// in this case we do not need to do anything, but the binding index
// still needs to be incremented because all styling binding values
// are stored inside of the lView.
const bindingIndex = lView[BINDING_INDEX]++;
const bindingIndex = getAndIncrementBindingIndex(lView, true);
// inputs are only evaluated from a template binding into a directive, therefore,
// there should not be a situation where a directive host bindings function
// evaluates the inputs (this should only happen in the template function)
if (!isHostStyling() && hasClassInput(tNode) && classes !== NO_CHANGE) {
if (!isHostStyling() && hasDirectiveInput && classes !== NO_CHANGE) {
updateDirectiveInputValue(context, lView, tNode, bindingIndex, classes, true);
classes = NO_CHANGE;
}
const updated = stylingMap(elementIndex, context, bindingIndex, classes, true);
if (ngDevMode) {
ngDevMode.classMap++;
if (updated) {
ngDevMode.classMapCacheMiss++;
}
}
stylingMap(context, tNode, lView, bindingIndex, classes, true, hasDirectiveInput);
}
/**
@ -331,13 +320,10 @@ export function classMapInternal(
* `[class]` bindings in Angular.
*/
function stylingMap(
elementIndex: number, context: TStylingContext, bindingIndex: number,
value: {[key: string]: any} | string | null, isClassBased: boolean): boolean {
let updated = false;
const lView = getLView();
context: TStylingContext, tNode: TNode, lView: LView, bindingIndex: number,
value: {[key: string]: any} | string | null, isClassBased: boolean,
hasDirectiveInput: boolean): void {
const directiveIndex = getActiveDirectiveId();
const tNode = getTNode(elementIndex, lView);
const native = getNativeByTNode(tNode, lView) as RElement;
const oldValue = getValue(lView, bindingIndex);
const hostBindingsMode = isHostStyling();
@ -359,17 +345,14 @@ function stylingMap(
patchConfig(context, TStylingConfig.HasMapBindings);
}
const stylingMapArr =
value === NO_CHANGE ? NO_CHANGE : normalizeIntoStylingMap(oldValue, value, !isClassBased);
// Direct Apply Case: bypass context resolution and apply the
// style/class map values directly to the element
if (allowDirectStyling(context, hostBindingsMode)) {
const sanitizerToUse = isClassBased ? null : sanitizer;
const renderer = getRenderer(tNode, lView);
updated = applyStylingMapDirectly(
renderer, context, native, lView, bindingIndex, stylingMapArr as StylingMapArray,
isClassBased, isClassBased ? setClass : setStyle, sanitizerToUse, valueHasChanged);
applyStylingMapDirectly(
renderer, context, native, lView, bindingIndex, value, isClassBased, sanitizerToUse,
valueHasChanged, hasDirectiveInput);
if (sanitizerToUse) {
// it's important we remove the current style sanitizer once the
// element exits, otherwise it will be used by the next styling
@ -377,7 +360,9 @@ function stylingMap(
setElementExitFn(stylingApply);
}
} else {
updated = valueHasChanged;
const stylingMapArr =
value === NO_CHANGE ? NO_CHANGE : normalizeIntoStylingMap(oldValue, value, !isClassBased);
activateStylingMapFeature();
// Context Resolution (or first update) Case: save the map value
@ -396,7 +381,12 @@ function stylingMap(
setElementExitFn(stylingApply);
}
return updated;
if (ngDevMode) {
isClassBased ? ngDevMode.classMap : ngDevMode.styleMap++;
if (valueHasChanged) {
isClassBased ? ngDevMode.classMapCacheMiss : ngDevMode.styleMapCacheMiss++;
}
}
}
/**
@ -452,8 +442,8 @@ function normalizeStylingDirectiveInputValue(
value = concatString(initialValue, forceClassesAsString(bindingValue));
} else {
value = concatString(
initialValue, forceStylesAsString(bindingValue as{[key: string]: any} | null | undefined),
';');
initialValue,
forceStylesAsString(bindingValue as{[key: string]: any} | null | undefined, true), ';');
}
}
return value;
@ -594,3 +584,11 @@ function resolveStylePropValue(
function isHostStyling(): boolean {
return isHostStylingActive(getActiveDirectiveId());
}
function getAndIncrementBindingIndex(lView: LView, isMapBased: boolean): number {
// map-based bindings use two slots because the previously constructed
// className / style value must be compared against.
const index = lView[BINDING_INDEX];
lView[BINDING_INDEX] += isMapBased ? 2 : 1;
return index;
}

View File

@ -7,14 +7,15 @@
*/
import {SafeValue, unwrapSafeValue} from '../../sanitization/bypass';
import {StyleSanitizeFn, StyleSanitizeMode} from '../../sanitization/style_sanitizer';
import {global} from '../../util/global';
import {ProceduralRenderer3, RElement, Renderer3, RendererStyleFlags3, isProceduralRenderer} from '../interfaces/renderer';
import {ApplyStylingFn, LStylingData, StylingMapArray, StylingMapArrayIndex, StylingMapsSyncMode, SyncStylingMapsFn, TStylingConfig, TStylingContext, TStylingContextIndex, TStylingContextPropConfigFlags} from '../interfaces/styling';
import {NO_CHANGE} from '../tokens';
import {DEFAULT_BINDING_INDEX, DEFAULT_BINDING_VALUE, DEFAULT_GUARD_MASK_VALUE, MAP_BASED_ENTRY_PROP_NAME, TEMPLATE_DIRECTIVE_INDEX, getBindingValue, getConfig, getDefaultValue, getGuardMask, getInitialStylingValue, getMapProp, getMapValue, getProp, getPropValuesStartPosition, getStylingMapArray, getTotalSources, getValue, getValuesCount, hasConfig, hasValueChanged, isContextLocked, isHostStylingActive, isSanitizationRequired, isStylingValueDefined, lockContext, patchConfig, setDefaultValue, setGuardMask, setMapAsDirty, setValue} from '../util/styling_utils';
import {DEFAULT_BINDING_INDEX, DEFAULT_BINDING_VALUE, DEFAULT_GUARD_MASK_VALUE, MAP_BASED_ENTRY_PROP_NAME, TEMPLATE_DIRECTIVE_INDEX, concatString, forceStylesAsString, getBindingValue, getConfig, getDefaultValue, getGuardMask, getInitialStylingValue, getMapProp, getMapValue, getProp, getPropValuesStartPosition, getStylingMapArray, getTotalSources, getValue, getValuesCount, hasConfig, hasValueChanged, isContextLocked, isHostStylingActive, isSanitizationRequired, isStylingMapArray, isStylingValueDefined, lockContext, normalizeIntoStylingMap, patchConfig, setDefaultValue, setGuardMask, setMapAsDirty, setValue} from '../util/styling_utils';
import {getStylingState, resetStylingState} from './state';
const VALUE_IS_EXTERNALLY_MODIFIED = {};
/**
* --------
@ -655,44 +656,99 @@ export function applyStylingViaContext(
*/
export function applyStylingMapDirectly(
renderer: any, context: TStylingContext, element: RElement, data: LStylingData,
bindingIndex: number, map: StylingMapArray, isClassBased: boolean, applyFn: ApplyStylingFn,
sanitizer?: StyleSanitizeFn | null, forceUpdate?: boolean): boolean {
if (forceUpdate || hasValueChanged(data[bindingIndex], map)) {
setValue(data, bindingIndex, map);
const initialStyles =
hasConfig(context, TStylingConfig.HasInitialStyling) ? getStylingMapArray(context) : null;
bindingIndex: number, value: {[key: string]: any} | string | null, isClassBased: boolean,
sanitizer?: StyleSanitizeFn | null, forceUpdate?: boolean,
bindingValueContainsInitial?: boolean): void {
const oldValue = getValue(data, bindingIndex);
if (forceUpdate || hasValueChanged(oldValue, value)) {
const config = getConfig(context);
const hasInitial = config & TStylingConfig.HasInitialStyling;
const initialValue =
hasInitial && !bindingValueContainsInitial ? getInitialStylingValue(context) : null;
setValue(data, bindingIndex, value);
for (let i = StylingMapArrayIndex.ValuesStartPosition; i < map.length;
i += StylingMapArrayIndex.TupleSize) {
const prop = getMapProp(map, i);
const value = getMapValue(map, i);
// the cached value is the last snapshot of the style or class
// attribute value and is used in the if statement below to
// keep track of internal/external changes.
const cachedValueIndex = bindingIndex + 1;
let cachedValue = getValue(data, cachedValueIndex);
if (cachedValue === NO_CHANGE) {
cachedValue = initialValue;
}
cachedValue = typeof cachedValue !== 'string' ? '' : cachedValue;
// case 1: apply the map value (if it exists)
let applied =
applyStylingValue(renderer, element, prop, value, applyFn, bindingIndex, sanitizer);
// If a class/style value was modified externally then the styling
// fast pass cannot guarantee that the external values are retained.
// When this happens, the algorithm will bail out and not write to
// the style or className attribute directly.
let writeToAttrDirectly = !(config & TStylingConfig.HasPropBindings);
if (writeToAttrDirectly &&
checkIfExternallyModified(element as HTMLElement, cachedValue, isClassBased)) {
writeToAttrDirectly = false;
if (oldValue !== VALUE_IS_EXTERNALLY_MODIFIED) {
// direct styling will reset the attribute entirely each time,
// and, for this reason, if the algorithm decides it cannot
// write to the class/style attributes directly then it must
// reset all the previous style/class values before it starts
// to apply values in the non-direct way.
removeStylingValues(renderer, element, oldValue, isClassBased);
// case 2: apply the initial value (if it exists)
if (!applied && initialStyles) {
applied = findAndApplyMapValue(
renderer, element, applyFn, initialStyles, prop, bindingIndex, sanitizer);
}
// default case: apply `null` to remove the value
if (!applied) {
applyFn(renderer, element, prop, null, bindingIndex);
// this will instruct the algorithm not to apply class or style
// values directly anymore.
setValue(data, cachedValueIndex, VALUE_IS_EXTERNALLY_MODIFIED);
}
}
const state = getStylingState(element, TEMPLATE_DIRECTIVE_INDEX);
if (isClassBased) {
state.lastDirectClassMap = map;
if (writeToAttrDirectly) {
let valueToApply: string;
if (isClassBased) {
valueToApply = typeof value === 'string' ? value : objectToClassName(value);
if (initialValue !== null) {
valueToApply = concatString(initialValue, valueToApply, ' ');
}
setClassName(renderer, element, valueToApply);
} else {
valueToApply = forceStylesAsString(value as{[key: string]: any}, true);
if (initialValue !== null) {
valueToApply = initialValue + ';' + valueToApply;
}
setStyleAttr(renderer, element, valueToApply);
}
setValue(data, cachedValueIndex, valueToApply || null);
} else {
state.lastDirectStyleMap = map;
}
const applyFn = isClassBased ? setClass : setStyle;
const map = normalizeIntoStylingMap(oldValue, value, !isClassBased);
const initialStyles = hasInitial ? getStylingMapArray(context) : null;
return true;
for (let i = StylingMapArrayIndex.ValuesStartPosition; i < map.length;
i += StylingMapArrayIndex.TupleSize) {
const prop = getMapProp(map, i);
const value = getMapValue(map, i);
// case 1: apply the map value (if it exists)
let applied =
applyStylingValue(renderer, element, prop, value, applyFn, bindingIndex, sanitizer);
// case 2: apply the initial value (if it exists)
if (!applied && initialStyles) {
applied = findAndApplyMapValue(
renderer, element, applyFn, initialStyles, prop, bindingIndex, sanitizer);
}
// default case: apply `null` to remove the value
if (!applied) {
applyFn(renderer, element, prop, null, bindingIndex);
}
}
const state = getStylingState(element, TEMPLATE_DIRECTIVE_INDEX);
if (isClassBased) {
state.lastDirectClassMap = map;
} else {
state.lastDirectStyleMap = map;
}
}
}
return false;
}
/**
@ -727,11 +783,12 @@ export function applyStylingMapDirectly(
*/
export function applyStylingValueDirectly(
renderer: any, context: TStylingContext, element: RElement, data: LStylingData,
bindingIndex: number, prop: string, value: any, isClassBased: boolean, applyFn: ApplyStylingFn,
bindingIndex: number, prop: string, value: any, isClassBased: boolean,
sanitizer?: StyleSanitizeFn | null): boolean {
let applied = false;
if (hasValueChanged(data[bindingIndex], value)) {
setValue(data, bindingIndex, value);
const applyFn = isClassBased ? setClass : setStyle;
// case 1: apply the provided value (if it exists)
applied = applyStylingValue(renderer, element, prop, value, applyFn, bindingIndex, sanitizer);
@ -888,6 +945,26 @@ export const setClass: ApplyStylingFn =
}
};
export const setClassName = (renderer: Renderer3 | null, native: RElement, className: string) => {
if (renderer !== null) {
if (isProceduralRenderer(renderer)) {
renderer.setAttribute(native, 'class', className);
} else {
native.className = className;
}
}
};
export const setStyleAttr = (renderer: Renderer3 | null, native: RElement, value: string) => {
if (renderer !== null) {
if (isProceduralRenderer(renderer)) {
renderer.setAttribute(native, 'style', value);
} else {
native.setAttribute('style', value);
}
}
};
/**
* Iterates over all provided styling entries and renders them on the element.
*
@ -914,3 +991,63 @@ export function renderStylingMap(
}
}
}
function objectToClassName(obj: {[key: string]: any} | null): string {
let str = '';
if (obj) {
for (let key in obj) {
const value = obj[key];
if (value) {
str += (str.length ? ' ' : '') + key;
}
}
}
return str;
}
/**
* Determines whether or not an element style/className value has changed since the last update.
*
* This function helps Angular determine if a style or class attribute value was
* modified by an external plugin or API outside of the style binding code. This
* means any JS code that adds/removes class/style values on an element outside
* of Angular's styling binding algorithm.
*
* @returns true when the value was modified externally.
*/
function checkIfExternallyModified(element: HTMLElement, cachedValue: any, isClassBased: boolean) {
// this means it was checked before and there is no reason
// to compare the style/class values again. Either that or
// web workers are being used.
if (global.Node === 'undefined' || cachedValue === VALUE_IS_EXTERNALLY_MODIFIED) return true;
// comparing the DOM value against the cached value is the best way to
// see if something has changed.
const currentValue =
(isClassBased ? element.className : (element.style && element.style.cssText)) || '';
return currentValue !== (cachedValue || '');
}
/**
* Removes provided styling values from the element
*/
function removeStylingValues(
renderer: any, element: RElement, values: string | {[key: string]: any} | StylingMapArray,
isClassBased: boolean) {
let arr: StylingMapArray;
if (isStylingMapArray(values)) {
arr = values as StylingMapArray;
} else {
arr = normalizeIntoStylingMap(null, values, !isClassBased);
}
const applyFn = isClassBased ? setClass : setStyle;
for (let i = StylingMapArrayIndex.ValuesStartPosition; i < arr.length;
i += StylingMapArrayIndex.TupleSize) {
const value = getMapValue(arr, i);
if (value) {
const prop = getMapProp(arr, i);
applyFn(renderer, element, prop, false);
}
}
}

View File

@ -5,12 +5,13 @@
* 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 {createProxy} from '../../debug/proxy';
import {StyleSanitizeFn} from '../../sanitization/style_sanitizer';
import {RElement} from '../interfaces/renderer';
import {ApplyStylingFn, LStylingData, TStylingConfig, TStylingContext, TStylingContextIndex} from '../interfaces/styling';
import {getCurrentStyleSanitizer} from '../state';
import {attachDebugObject} from '../util/debug_utils';
import {MAP_BASED_ENTRY_PROP_NAME, TEMPLATE_DIRECTIVE_INDEX, allowDirectStyling as _allowDirectStyling, getBindingValue, getDefaultValue, getGuardMask, getProp, getPropValuesStartPosition, getValuesCount, hasConfig, isContextLocked, isSanitizationRequired, isStylingContext} from '../util/styling_utils';
import {MAP_BASED_ENTRY_PROP_NAME, TEMPLATE_DIRECTIVE_INDEX, allowDirectStyling as _allowDirectStyling, getBindingValue, getDefaultValue, getGuardMask, getProp, getPropValuesStartPosition, getValue, getValuesCount, hasConfig, isContextLocked, isSanitizationRequired, isStylingContext, normalizeIntoStylingMap, setValue} from '../util/styling_utils';
import {applyStylingViaContext} from './bindings';
import {activateStylingMapFeature} from './map_based_bindings';
@ -374,10 +375,52 @@ export class NodeStylingDebug implements DebugNodeStyling {
*/
get summary(): {[key: string]: DebugNodeStylingEntry} {
const entries: {[key: string]: DebugNodeStylingEntry} = {};
this._mapValues((prop: string, value: any, bindingIndex: number | null) => {
const config = this.config;
const isClassBased = this._isClassBased;
let data = this._data;
// the direct pass code doesn't convert [style] or [class] values
// into StylingMapArray instances. For this reason, the values
// need to be converted ahead of time since the styling debug
// relies on context resolution to figure out what styling
// values have been added/removed on the element.
if (config.allowDirectStyling && config.hasMapBindings) {
data = data.concat([]); // make a copy
this._convertMapBindingsToStylingMapArrays(data);
}
this._mapValues(data, (prop: string, value: any, bindingIndex: number | null) => {
entries[prop] = {prop, value, bindingIndex};
});
return entries;
// because the styling algorithm runs into two different
// modes: direct and context-resolution, the output of the entries
// object is different because the removed values are not
// saved between updates. For this reason a proxy is created
// so that the behavior is the same when examining values
// that are no longer active on the element.
return createProxy({
get(target: {}, prop: string): DebugNodeStylingEntry{
let value: DebugNodeStylingEntry = entries[prop]; if (!value) {
value = {
prop,
value: isClassBased ? false : null,
bindingIndex: null,
};
} return value;
},
set(target: {}, prop: string, value: any) { return false; },
ownKeys() { return Object.keys(entries); },
getOwnPropertyDescriptor(k: any) {
// we use a special property descriptor here so that enumeration operations
// such as `Object.keys` will work on this proxy.
return {
enumerable: true,
configurable: true,
};
},
});
}
get config() { return buildConfig(this.context.context); }
@ -387,11 +430,41 @@ export class NodeStylingDebug implements DebugNodeStyling {
*/
get values(): {[key: string]: any} {
const entries: {[key: string]: any} = {};
this._mapValues((prop: string, value: any) => { entries[prop] = value; });
const config = this.config;
let data = this._data;
// the direct pass code doesn't convert [style] or [class] values
// into StylingMapArray instances. For this reason, the values
// need to be converted ahead of time since the styling debug
// relies on context resolution to figure out what styling
// values have been added/removed on the element.
if (config.allowDirectStyling && config.hasMapBindings) {
data = data.concat([]); // make a copy
this._convertMapBindingsToStylingMapArrays(data);
}
this._mapValues(data, (prop: string, value: any) => { entries[prop] = value; });
return entries;
}
private _mapValues(fn: (prop: string, value: string|null, bindingIndex: number|null) => any) {
private _convertMapBindingsToStylingMapArrays(data: LStylingData) {
const context = this.context.context;
const limit = getPropValuesStartPosition(context);
for (let i =
TStylingContextIndex.ValuesStartPosition + TStylingContextIndex.BindingsStartOffset;
i < limit; i++) {
const bindingIndex = context[i] as number;
const bindingValue = bindingIndex !== 0 ? getValue(data, bindingIndex) : null;
if (bindingValue && !Array.isArray(bindingValue)) {
const stylingMapArray = normalizeIntoStylingMap(null, bindingValue, !this._isClassBased);
setValue(data, bindingIndex, stylingMapArray);
}
}
}
private _mapValues(
data: LStylingData,
fn: (prop: string, value: string|null, bindingIndex: number|null) => 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).
@ -409,11 +482,11 @@ export class NodeStylingDebug implements DebugNodeStyling {
// run the template bindings
applyStylingViaContext(
this.context.context, null, mockElement, this._data, true, mapFn, sanitizer, false);
this.context.context, null, mockElement, data, true, mapFn, sanitizer, false);
// and also the host bindings
applyStylingViaContext(
this.context.context, null, mockElement, this._data, true, mapFn, sanitizer, true);
this.context.context, null, mockElement, data, true, mapFn, sanitizer, true);
}
}

View File

@ -247,7 +247,7 @@ export function isStylingContext(value: any): boolean {
typeof value[1] !== 'string';
}
export function isStylingMapArray(value: TStylingContext | StylingMapArray | null): boolean {
export function isStylingMapArray(value: any): boolean {
// the StylingMapArray is in the format of [initial, prop, string, prop, string]
// and this is the defining value to distinguish between arrays
return Array.isArray(value) &&
@ -295,13 +295,18 @@ export function forceClassesAsString(classes: string | {[key: string]: any} | nu
return (classes as string) || '';
}
export function forceStylesAsString(styles: {[key: string]: any} | null | undefined): string {
export function forceStylesAsString(
styles: {[key: string]: any} | null | undefined, hyphenateProps: boolean): string {
let str = '';
if (styles) {
const props = Object.keys(styles);
for (let i = 0; i < props.length; i++) {
const prop = props[i];
str = concatString(str, `${prop}:${styles[prop]}`, ';');
const propLabel = hyphenateProps ? hyphenate(prop) : prop;
const value = styles[prop];
if (value !== null) {
str = concatString(str, `${propLabel}:${value}`, ';');
}
}
}
return str;
@ -463,4 +468,4 @@ export function splitOnWhitespace(text: string): string[]|null {
// `input('class') + classMap()` instructions.
export function selectClassBasedInputName(inputs: PropertyAliases): string {
return inputs.hasOwnProperty('class') ? 'class' : 'className';
}
}