/** * @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 {AttributeMarker} from '../../core'; import {AST, ASTWithSource, BindingPipe, BindingType, Interpolation} from '../../expression_parser/ast'; import * as o from '../../output/output_ast'; import {ParseSourceSpan} from '../../parse_util'; import {isEmptyExpression} from '../../template_parser/template_parser'; import * as t from '../r3_ast'; import {Identifiers as R3} from '../r3_identifiers'; import {hyphenate, parse as parseStyle} from './style_parser'; import {ValueConverter} from './template'; import {DefinitionMap, getInterpolationArgsLength} from './util'; const IMPORTANT_FLAG = '!important'; /** * Minimum amount of binding slots required in the runtime for style/class bindings. * * Styling in Angular uses up two slots in the runtime LView/TData data structures to * record binding data, property information and metadata. * * When a binding is registered it will place the following information in the `LView`: * * slot 1) binding value * slot 2) cached value (all other values collected before it in string form) * * When a binding is registered it will place the following information in the `TData`: * * slot 1) prop name * slot 2) binding index that points to the previous style/class binding (and some extra config * values) * * Let's imagine we have a binding that looks like so: * * ``` *
* ``` * * Our `LView` and `TData` data-structures look like so: * * ```typescript * LView = [ * // ... * x, // value of x * "width: x", * * y, // value of y * "width: x; height: y", * // ... * ]; * * TData = [ * // ... * "width", // binding slot 20 * 0, * * "height", * 20, * // ... * ]; * ``` * * */ export const MIN_STYLING_BINDING_SLOTS_REQUIRED = 2; /** * A styling expression summary that is to be processed by the compiler */ export interface StylingInstruction { reference: o.ExternalReference; /** Calls to individual styling instructions. Used when chaining calls to the same instruction. */ calls: StylingInstructionCall[]; } export interface StylingInstructionCall { sourceSpan: ParseSourceSpan|null; supportsInterpolation: boolean; allocateBindingSlots: number; params: ((convertFn: (value: any) => o.Expression | o.Expression[]) => o.Expression[]); } /** * An internal record of the input data for a styling binding */ interface BoundStylingEntry { hasOverrideFlag: boolean; name: string|null; suffix: string|null; sourceSpan: ParseSourceSpan; value: AST; } /** * Produces creation/update instructions for all styling bindings (class and style) * * It also produces the creation instruction to register all initial styling values * (which are all the static class="..." and style="..." attribute values that exist * on an element within a template). * * The builder class below handles producing instructions for the following cases: * * - Static style/class attributes (style="..." and class="...") * - Dynamic style/class map bindings ([style]="map" and [class]="map|string") * - Dynamic style/class property bindings ([style.prop]="exp" and [class.name]="exp") * * Due to the complex relationship of all of these cases, the instructions generated * for these attributes/properties/bindings must be done so in the correct order. The * order which these must be generated is as follows: * * if (createMode) { * styling(...) * } * if (updateMode) { * styleMap(...) * classMap(...) * styleProp(...) * classProp(...) * } * * The creation/update methods within the builder class produce these instructions. */ export class StylingBuilder { /** Whether or not there are any static styling values present */ private _hasInitialValues = false; /** * Whether or not there are any styling bindings present * (i.e. `[style]`, `[class]`, `[style.prop]` or `[class.name]`) */ public hasBindings = false; public hasBindingsWithPipes = false; /** the input for [class] (if it exists) */ private _classMapInput: BoundStylingEntry|null = null; /** the input for [style] (if it exists) */ private _styleMapInput: BoundStylingEntry|null = null; /** an array of each [style.prop] input */ private _singleStyleInputs: BoundStylingEntry[]|null = null; /** an array of each [class.name] input */ private _singleClassInputs: BoundStylingEntry[]|null = null; private _lastStylingInput: BoundStylingEntry|null = null; private _firstStylingInput: BoundStylingEntry|null = null; // maps are used instead of hash maps because a Map will // retain the ordering of the keys /** * Represents the location of each style binding in the template * (e.g. `
` implies * that `width=0` and `height=1`) */ private _stylesIndex = new Map(); /** * Represents the location of each class binding in the template * (e.g. `
` implies * that `big=0` and `hidden=1`) */ private _classesIndex = new Map(); private _initialStyleValues: string[] = []; private _initialClassValues: string[] = []; constructor(private _directiveExpr: o.Expression|null) {} /** * Registers a given input to the styling builder to be later used when producing AOT code. * * The code below will only accept the input if it is somehow tied to styling (whether it be * style/class bindings or static style/class attributes). */ registerBoundInput(input: t.BoundAttribute): boolean { // [attr.style] or [attr.class] are skipped in the code below, // they should not be treated as styling-based bindings since // they are intended to be written directly to the attr and // will therefore skip all style/class resolution that is present // with style="", [style]="" and [style.prop]="", class="", // [class.prop]="". [class]="" assignments let binding: BoundStylingEntry|null = null; let name = input.name; switch (input.type) { case BindingType.Property: binding = this.registerInputBasedOnName(name, input.value, input.sourceSpan); break; case BindingType.Style: binding = this.registerStyleInput(name, false, input.value, input.sourceSpan, input.unit); break; case BindingType.Class: binding = this.registerClassInput(name, false, input.value, input.sourceSpan); break; } return binding ? true : false; } registerInputBasedOnName(name: string, expression: AST, sourceSpan: ParseSourceSpan) { let binding: BoundStylingEntry|null = null; const prefix = name.substring(0, 6); const isStyle = name === 'style' || prefix === 'style.' || prefix === 'style!'; const isClass = !isStyle && (name === 'class' || prefix === 'class.' || prefix === 'class!'); if (isStyle || isClass) { const isMapBased = name.charAt(5) !== '.'; // style.prop or class.prop makes this a no const property = name.substr(isMapBased ? 5 : 6); // the dot explains why there's a +1 if (isStyle) { binding = this.registerStyleInput(property, isMapBased, expression, sourceSpan); } else { binding = this.registerClassInput(property, isMapBased, expression, sourceSpan); } } return binding; } registerStyleInput( name: string, isMapBased: boolean, value: AST, sourceSpan: ParseSourceSpan, suffix?: string|null): BoundStylingEntry|null { if (isEmptyExpression(value)) { return null; } name = normalizePropName(name); const {property, hasOverrideFlag, suffix: bindingSuffix} = parseProperty(name); suffix = typeof suffix === 'string' && suffix.length !== 0 ? suffix : bindingSuffix; const entry: BoundStylingEntry = {name: property, suffix: suffix, value, sourceSpan, hasOverrideFlag}; if (isMapBased) { this._styleMapInput = entry; } else { (this._singleStyleInputs = this._singleStyleInputs || []).push(entry); registerIntoMap(this._stylesIndex, property); } this._lastStylingInput = entry; this._firstStylingInput = this._firstStylingInput || entry; this._checkForPipes(value); this.hasBindings = true; return entry; } registerClassInput(name: string, isMapBased: boolean, value: AST, sourceSpan: ParseSourceSpan): BoundStylingEntry|null { if (isEmptyExpression(value)) { return null; } const {property, hasOverrideFlag} = parseProperty(name); const entry: BoundStylingEntry = {name: property, value, sourceSpan, hasOverrideFlag, suffix: null}; if (isMapBased) { if (this._classMapInput) { throw new Error( '[class] and [className] bindings cannot be used on the same element simultaneously'); } this._classMapInput = entry; } else { (this._singleClassInputs = this._singleClassInputs || []).push(entry); registerIntoMap(this._classesIndex, property); } this._lastStylingInput = entry; this._firstStylingInput = this._firstStylingInput || entry; this._checkForPipes(value); this.hasBindings = true; return entry; } private _checkForPipes(value: AST) { if ((value instanceof ASTWithSource) && (value.ast instanceof BindingPipe)) { this.hasBindingsWithPipes = true; } } /** * Registers the element's static style string value to the builder. * * @param value the style string (e.g. `width:100px; height:200px;`) */ registerStyleAttr(value: string) { this._initialStyleValues = parseStyle(value); this._hasInitialValues = true; } /** * Registers the element's static class string value to the builder. * * @param value the className string (e.g. `disabled gold zoom`) */ registerClassAttr(value: string) { this._initialClassValues = value.trim().split(/\s+/g); this._hasInitialValues = true; } /** * Appends all styling-related expressions to the provided attrs array. * * @param attrs an existing array where each of the styling expressions * will be inserted into. */ populateInitialStylingAttrs(attrs: o.Expression[]): void { // [CLASS_MARKER, 'foo', 'bar', 'baz' ...] if (this._initialClassValues.length) { attrs.push(o.literal(AttributeMarker.Classes)); for (let i = 0; i < this._initialClassValues.length; i++) { attrs.push(o.literal(this._initialClassValues[i])); } } // [STYLE_MARKER, 'width', '200px', 'height', '100px', ...] if (this._initialStyleValues.length) { attrs.push(o.literal(AttributeMarker.Styles)); for (let i = 0; i < this._initialStyleValues.length; i += 2) { attrs.push( o.literal(this._initialStyleValues[i]), o.literal(this._initialStyleValues[i + 1])); } } } /** * Builds an instruction with all the expressions and parameters for `elementHostAttrs`. * * The instruction generation code below is used for producing the AOT statement code which is * responsible for registering initial styles (within a directive hostBindings' creation block), * as well as any of the provided attribute values, to the directive host element. */ assignHostAttrs(attrs: o.Expression[], definitionMap: DefinitionMap): void { if (this._directiveExpr && (attrs.length || this._hasInitialValues)) { this.populateInitialStylingAttrs(attrs); definitionMap.set('hostAttrs', o.literalArr(attrs)); } } /** * Builds an instruction with all the expressions and parameters for `classMap`. * * The instruction data will contain all expressions for `classMap` to function * which includes the `[class]` expression params. */ buildClassMapInstruction(valueConverter: ValueConverter): StylingInstruction|null { if (this._classMapInput) { return this._buildMapBasedInstruction(valueConverter, true, this._classMapInput); } return null; } /** * Builds an instruction with all the expressions and parameters for `styleMap`. * * The instruction data will contain all expressions for `styleMap` to function * which includes the `[style]` expression params. */ buildStyleMapInstruction(valueConverter: ValueConverter): StylingInstruction|null { if (this._styleMapInput) { return this._buildMapBasedInstruction(valueConverter, false, this._styleMapInput); } return null; } private _buildMapBasedInstruction( valueConverter: ValueConverter, isClassBased: boolean, stylingInput: BoundStylingEntry): StylingInstruction { // each styling binding value is stored in the LView // map-based bindings allocate two slots: one for the // previous binding value and another for the previous // className or style attribute value. let totalBindingSlotsRequired = MIN_STYLING_BINDING_SLOTS_REQUIRED; // these values must be outside of the update block so that they can // be evaluated (the AST visit call) during creation time so that any // pipes can be picked up in time before the template is built const mapValue = stylingInput.value.visit(valueConverter); let reference: o.ExternalReference; if (mapValue instanceof Interpolation) { totalBindingSlotsRequired += mapValue.expressions.length; reference = isClassBased ? getClassMapInterpolationExpression(mapValue) : getStyleMapInterpolationExpression(mapValue); } else { reference = isClassBased ? R3.classMap : R3.styleMap; } return { reference, calls: [{ supportsInterpolation: true, sourceSpan: stylingInput.sourceSpan, allocateBindingSlots: totalBindingSlotsRequired, params: (convertFn: (value: any) => o.Expression|o.Expression[]) => { const convertResult = convertFn(mapValue); const params = Array.isArray(convertResult) ? convertResult : [convertResult]; return params; } }] }; } private _buildSingleInputs( reference: o.ExternalReference, inputs: BoundStylingEntry[], valueConverter: ValueConverter, getInterpolationExpressionFn: ((value: Interpolation) => o.ExternalReference)|null, isClassBased: boolean): StylingInstruction[] { const instructions: StylingInstruction[] = []; inputs.forEach(input => { const previousInstruction: StylingInstruction|undefined = instructions[instructions.length - 1]; const value = input.value.visit(valueConverter); let referenceForCall = reference; // each styling binding value is stored in the LView // but there are two values stored for each binding: // 1) the value itself // 2) an intermediate value (concatenation of style up to this point). // We need to store the intermediate value so that we don't allocate // the strings on each CD. let totalBindingSlotsRequired = MIN_STYLING_BINDING_SLOTS_REQUIRED; if (value instanceof Interpolation) { totalBindingSlotsRequired += value.expressions.length; if (getInterpolationExpressionFn) { referenceForCall = getInterpolationExpressionFn(value); } } const call = { sourceSpan: input.sourceSpan, allocateBindingSlots: totalBindingSlotsRequired, supportsInterpolation: !!getInterpolationExpressionFn, params: (convertFn: (value: any) => o.Expression | o.Expression[]) => { // params => stylingProp(propName, value, suffix) const params: o.Expression[] = []; params.push(o.literal(input.name)); const convertResult = convertFn(value); if (Array.isArray(convertResult)) { params.push(...convertResult); } else { params.push(convertResult); } // [style.prop] bindings may use suffix values (e.g. px, em, etc...), therefore, // if that is detected then we need to pass that in as an optional param. if (!isClassBased && input.suffix !== null) { params.push(o.literal(input.suffix)); } return params; } }; // If we ended up generating a call to the same instruction as the previous styling property // we can chain the calls together safely to save some bytes, otherwise we have to generate // a separate instruction call. This is primarily a concern with interpolation instructions // where we may start off with one `reference`, but end up using another based on the // number of interpolations. if (previousInstruction && previousInstruction.reference === referenceForCall) { previousInstruction.calls.push(call); } else { instructions.push({reference: referenceForCall, calls: [call]}); } }); return instructions; } private _buildClassInputs(valueConverter: ValueConverter): StylingInstruction[] { if (this._singleClassInputs) { return this._buildSingleInputs( R3.classProp, this._singleClassInputs, valueConverter, null, true); } return []; } private _buildStyleInputs(valueConverter: ValueConverter): StylingInstruction[] { if (this._singleStyleInputs) { return this._buildSingleInputs( R3.styleProp, this._singleStyleInputs, valueConverter, getStylePropInterpolationExpression, false); } return []; } /** * Constructs all instructions which contain the expressions that will be placed * into the update block of a template function or a directive hostBindings function. */ buildUpdateLevelInstructions(valueConverter: ValueConverter) { const instructions: StylingInstruction[] = []; if (this.hasBindings) { const styleMapInstruction = this.buildStyleMapInstruction(valueConverter); if (styleMapInstruction) { instructions.push(styleMapInstruction); } const classMapInstruction = this.buildClassMapInstruction(valueConverter); if (classMapInstruction) { instructions.push(classMapInstruction); } instructions.push(...this._buildStyleInputs(valueConverter)); instructions.push(...this._buildClassInputs(valueConverter)); } return instructions; } } function registerIntoMap(map: Map, key: string) { if (!map.has(key)) { map.set(key, map.size); } } export function parseProperty(name: string): {property: string, suffix: string|null, hasOverrideFlag: boolean} { let hasOverrideFlag = false; const overrideIndex = name.indexOf(IMPORTANT_FLAG); if (overrideIndex !== -1) { name = overrideIndex > 0 ? name.substring(0, overrideIndex) : ''; hasOverrideFlag = true; } let suffix: string|null = null; let property = name; const unitIndex = name.lastIndexOf('.'); if (unitIndex > 0) { suffix = name.substr(unitIndex + 1); property = name.substring(0, unitIndex); } return {property, suffix, hasOverrideFlag}; } /** * Gets the instruction to generate for an interpolated class map. * @param interpolation An Interpolation AST */ function getClassMapInterpolationExpression(interpolation: Interpolation): o.ExternalReference { switch (getInterpolationArgsLength(interpolation)) { case 1: return R3.classMap; case 3: return R3.classMapInterpolate1; case 5: return R3.classMapInterpolate2; case 7: return R3.classMapInterpolate3; case 9: return R3.classMapInterpolate4; case 11: return R3.classMapInterpolate5; case 13: return R3.classMapInterpolate6; case 15: return R3.classMapInterpolate7; case 17: return R3.classMapInterpolate8; default: return R3.classMapInterpolateV; } } /** * Gets the instruction to generate for an interpolated style map. * @param interpolation An Interpolation AST */ function getStyleMapInterpolationExpression(interpolation: Interpolation): o.ExternalReference { switch (getInterpolationArgsLength(interpolation)) { case 1: return R3.styleMap; case 3: return R3.styleMapInterpolate1; case 5: return R3.styleMapInterpolate2; case 7: return R3.styleMapInterpolate3; case 9: return R3.styleMapInterpolate4; case 11: return R3.styleMapInterpolate5; case 13: return R3.styleMapInterpolate6; case 15: return R3.styleMapInterpolate7; case 17: return R3.styleMapInterpolate8; default: return R3.styleMapInterpolateV; } } /** * Gets the instruction to generate for an interpolated style prop. * @param interpolation An Interpolation AST */ function getStylePropInterpolationExpression(interpolation: Interpolation) { switch (getInterpolationArgsLength(interpolation)) { case 1: return R3.styleProp; case 3: return R3.stylePropInterpolate1; case 5: return R3.stylePropInterpolate2; case 7: return R3.stylePropInterpolate3; case 9: return R3.stylePropInterpolate4; case 11: return R3.stylePropInterpolate5; case 13: return R3.stylePropInterpolate6; case 15: return R3.stylePropInterpolate7; case 17: return R3.stylePropInterpolate8; default: return R3.stylePropInterpolateV; } } function normalizePropName(prop: string): string { return hyphenate(prop); }