fix(ivy): merge static style rendering across elements, directives and components (#27661)
PR Close #27661
This commit is contained in:
@ -379,13 +379,9 @@ export const enum RenderFlags {
|
||||
Update = 0b10
|
||||
}
|
||||
|
||||
export const enum InitialStylingFlags {
|
||||
VALUES_MODE = 0b1,
|
||||
}
|
||||
|
||||
// Pasted from render3/interfaces/node.ts
|
||||
/**
|
||||
* A set of marker values to be used in the attributes arrays. Those markers indicate that some
|
||||
* A set of marker values to be used in the attributes arrays. These markers indicate that some
|
||||
* items are not regular attributes and the processing should be adapted accordingly.
|
||||
*/
|
||||
export const enum AttributeMarker {
|
||||
@ -396,11 +392,48 @@ export const enum AttributeMarker {
|
||||
*/
|
||||
NamespaceURI = 0,
|
||||
|
||||
/**
|
||||
* Signals class declaration.
|
||||
*
|
||||
* Each value following `Classes` designates a class name to include on the element.
|
||||
* ## Example:
|
||||
*
|
||||
* Given:
|
||||
* ```
|
||||
* <div class="foo bar baz">...<d/vi>
|
||||
* ```
|
||||
*
|
||||
* the generated code is:
|
||||
* ```
|
||||
* var _c1 = [AttributeMarker.Classes, 'foo', 'bar', 'baz'];
|
||||
* ```
|
||||
*/
|
||||
Classes = 1,
|
||||
|
||||
/**
|
||||
* Signals style declaration.
|
||||
*
|
||||
* Each pair of values following `Styles` designates a style name and value to include on the
|
||||
* element.
|
||||
* ## Example:
|
||||
*
|
||||
* Given:
|
||||
* ```
|
||||
* <div style="width:100px; height:200px; color:red">...</div>
|
||||
* ```
|
||||
*
|
||||
* the generated code is:
|
||||
* ```
|
||||
* var _c1 = [AttributeMarker.Styles, 'width', '100px', 'height'. '200px', 'color', 'red'];
|
||||
* ```
|
||||
*/
|
||||
Styles = 2,
|
||||
|
||||
/**
|
||||
* This marker indicates that the following attribute names were extracted from bindings (ex.:
|
||||
* [foo]="exp") and / or event handlers (ex. (bar)="doSth()").
|
||||
* Taking the above bindings and outputs as an example an attributes array could look as follows:
|
||||
* ['class', 'fade in', AttributeMarker.SelectOnly, 'foo', 'bar']
|
||||
*/
|
||||
SelectOnly = 1
|
||||
}
|
||||
SelectOnly = 3,
|
||||
}
|
||||
|
@ -43,6 +43,8 @@ export class Identifiers {
|
||||
|
||||
static elementStyling: o.ExternalReference = {name: 'ɵelementStyling', moduleName: CORE};
|
||||
|
||||
static elementHostAttrs: o.ExternalReference = {name: 'ɵelementHostAttrs', moduleName: CORE};
|
||||
|
||||
static elementStylingMap: o.ExternalReference = {name: 'ɵelementStylingMap', moduleName: CORE};
|
||||
|
||||
static elementStyleProp: o.ExternalReference = {name: 'ɵelementStyleProp', moduleName: CORE};
|
||||
|
@ -28,7 +28,7 @@ import {Render3ParseResult} from '../r3_template_transform';
|
||||
import {typeWithParameters} from '../util';
|
||||
|
||||
import {R3ComponentDef, R3ComponentMetadata, R3DirectiveDef, R3DirectiveMetadata, R3QueryMetadata} from './api';
|
||||
import {StylingBuilder, StylingInstruction} from './styling';
|
||||
import {StylingBuilder, StylingInstruction} from './styling_builder';
|
||||
import {BindingScope, TemplateDefinitionBuilder, ValueConverter, renderFlagCheckIfStmt} from './template';
|
||||
import {CONTEXT_NAME, DefinitionMap, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, conditionallyCreateMapObjectLiteral, getQueryPredicate, temporaryAllocator} from './util';
|
||||
|
||||
@ -709,16 +709,35 @@ function createHostBindingsFunction(
|
||||
}
|
||||
}
|
||||
|
||||
if (styleBuilder.hasBindingsOrInitialValues) {
|
||||
const createInstruction = styleBuilder.buildCreateLevelInstruction(null, constantPool);
|
||||
if (createInstruction) {
|
||||
const createStmt = createStylingStmt(createInstruction, bindingContext, bindingFn);
|
||||
createStatements.push(createStmt);
|
||||
if (styleBuilder.hasBindingsOrInitialValues()) {
|
||||
// since we're dealing with directives here and directives have a hostBinding
|
||||
// function, we need to generate special instructions that deal with styling
|
||||
// (both bindings and initial values). The instruction below will instruct
|
||||
// all initial styling (styling that is inside of a host binding within a
|
||||
// directive) to be attached to the host element of the directive.
|
||||
const hostAttrsInstruction =
|
||||
styleBuilder.buildDirectiveHostAttrsInstruction(null, constantPool);
|
||||
if (hostAttrsInstruction) {
|
||||
createStatements.push(createStylingStmt(hostAttrsInstruction, bindingContext, bindingFn));
|
||||
}
|
||||
|
||||
// singular style/class bindings (things like `[style.prop]` and `[class.name]`)
|
||||
// MUST be registered on a given element within the component/directive
|
||||
// templateFn/hostBindingsFn functions. The instruction below will figure out
|
||||
// what all the bindings are and then generate the statements required to register
|
||||
// those bindings to the element via `elementStyling`.
|
||||
const elementStylingInstruction =
|
||||
styleBuilder.buildElementStylingInstruction(null, constantPool);
|
||||
if (elementStylingInstruction) {
|
||||
createStatements.push(
|
||||
createStylingStmt(elementStylingInstruction, bindingContext, bindingFn));
|
||||
}
|
||||
|
||||
// finally each binding that was registered in the statement above will need to be added to
|
||||
// the update block of a component/directive templateFn/hostBindingsFn so that the bindings
|
||||
// are evaluated and updated for the element.
|
||||
styleBuilder.buildUpdateLevelInstructions(valueConverter).forEach(instruction => {
|
||||
const updateStmt = createStylingStmt(instruction, bindingContext, bindingFn);
|
||||
updateStatements.push(updateStmt);
|
||||
updateStatements.push(createStylingStmt(instruction, bindingContext, bindingFn));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -23,10 +23,15 @@ const enum Char {
|
||||
*
|
||||
* @param value string representation of style as used in the `style` attribute in HTML.
|
||||
* Example: `color: red; height: auto`.
|
||||
* @returns an object literal. `{ color: 'red', height: 'auto'}`.
|
||||
* @returns An array of style property name and value pairs, e.g. `['color', 'red', 'height',
|
||||
* 'auto']`
|
||||
*/
|
||||
export function parse(value: string): {[key: string]: any} {
|
||||
const styles: {[key: string]: any} = {};
|
||||
export function parse(value: string): string[] {
|
||||
// we use a string array here instead of a string map
|
||||
// because a string-map is not guaranteed to retain the
|
||||
// order of the entries whereas a string array can be
|
||||
// construted in a [key, value, key, value] format.
|
||||
const styles: string[] = [];
|
||||
|
||||
let i = 0;
|
||||
let parenDepth = 0;
|
||||
@ -72,7 +77,7 @@ export function parse(value: string): {[key: string]: any} {
|
||||
case Char.Semicolon:
|
||||
if (currentProp && valueStart > 0 && parenDepth === 0 && quote === Char.QuoteNone) {
|
||||
const styleVal = value.substring(valueStart, i - 1).trim();
|
||||
styles[currentProp] = valueHasQuotes ? stripUnnecessaryQuotes(styleVal) : styleVal;
|
||||
styles.push(currentProp, valueHasQuotes ? stripUnnecessaryQuotes(styleVal) : styleVal);
|
||||
propStart = i;
|
||||
valueStart = 0;
|
||||
currentProp = null;
|
||||
@ -84,7 +89,7 @@ export function parse(value: string): {[key: string]: any} {
|
||||
|
||||
if (currentProp && valueStart) {
|
||||
const styleVal = value.substr(valueStart).trim();
|
||||
styles[currentProp] = valueHasQuotes ? stripUnnecessaryQuotes(styleVal) : styleVal;
|
||||
styles.push(currentProp, valueHasQuotes ? stripUnnecessaryQuotes(styleVal) : styleVal);
|
||||
}
|
||||
|
||||
return styles;
|
||||
|
@ -6,8 +6,8 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {ConstantPool} from '../../constant_pool';
|
||||
import {InitialStylingFlags} from '../../core';
|
||||
import {AST, BindingType, ParseSpan} from '../../expression_parser/ast';
|
||||
import {AttributeMarker} from '../../core';
|
||||
import {AST, BindingType} from '../../expression_parser/ast';
|
||||
import * as o from '../../output/output_ast';
|
||||
import {ParseSourceSpan} from '../../parse_util';
|
||||
import * as t from '../r3_ast';
|
||||
@ -40,6 +40,10 @@ interface BoundStylingEntry {
|
||||
/**
|
||||
* 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="...")
|
||||
@ -63,25 +67,57 @@ interface BoundStylingEntry {
|
||||
* The creation/update methods within the builder class produce these instructions.
|
||||
*/
|
||||
export class StylingBuilder {
|
||||
public readonly hasBindingsOrInitialValues = false;
|
||||
/** 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]`)
|
||||
*/
|
||||
private _hasBindings = 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;
|
||||
|
||||
// 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. `<div [style.width]="w" [style.height]="h">` implies
|
||||
* that `width=0` and `height=1`)
|
||||
*/
|
||||
private _stylesIndex = new Map<string, number>();
|
||||
|
||||
/**
|
||||
* Represents the location of each class binding in the template
|
||||
* (e.g. `<div [class.big]="b" [class.hidden]="h">` implies
|
||||
* that `big=0` and `hidden=1`)
|
||||
*/
|
||||
private _classesIndex = new Map<string, number>();
|
||||
private _initialStyleValues: {[propName: string]: string} = {};
|
||||
private _initialClassValues: {[className: string]: boolean} = {};
|
||||
private _initialStyleValues: string[] = [];
|
||||
private _initialClassValues: string[] = [];
|
||||
|
||||
// certain style properties ALWAYS need sanitization
|
||||
// this is checked each time new styles are encountered
|
||||
private _useDefaultSanitizer = false;
|
||||
private _applyFnRequired = false;
|
||||
|
||||
constructor(private _elementIndexExpr: o.Expression, private _directiveExpr: o.Expression|null) {}
|
||||
|
||||
hasBindingsOrInitialValues() { return this._hasBindings || this._hasInitialValues; }
|
||||
|
||||
/**
|
||||
* 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
|
||||
@ -117,14 +153,12 @@ export class StylingBuilder {
|
||||
(this._singleStyleInputs = this._singleStyleInputs || []).push(entry);
|
||||
this._useDefaultSanitizer = this._useDefaultSanitizer || isStyleSanitizable(propertyName);
|
||||
registerIntoMap(this._stylesIndex, propertyName);
|
||||
(this as any).hasBindingsOrInitialValues = true;
|
||||
} else {
|
||||
this._useDefaultSanitizer = true;
|
||||
this._styleMapInput = entry;
|
||||
}
|
||||
this._lastStylingInput = entry;
|
||||
(this as any).hasBindingsOrInitialValues = true;
|
||||
this._applyFnRequired = true;
|
||||
this._hasBindings = true;
|
||||
return entry;
|
||||
}
|
||||
|
||||
@ -133,107 +167,152 @@ export class StylingBuilder {
|
||||
const entry = { name: className, value, sourceSpan } as BoundStylingEntry;
|
||||
if (className) {
|
||||
(this._singleClassInputs = this._singleClassInputs || []).push(entry);
|
||||
(this as any).hasBindingsOrInitialValues = true;
|
||||
registerIntoMap(this._classesIndex, className);
|
||||
} else {
|
||||
this._classMapInput = entry;
|
||||
}
|
||||
this._lastStylingInput = entry;
|
||||
(this as any).hasBindingsOrInitialValues = true;
|
||||
this._applyFnRequired = true;
|
||||
this._hasBindings = true;
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
Object.keys(this._initialStyleValues).forEach(prop => {
|
||||
registerIntoMap(this._stylesIndex, prop);
|
||||
(this as any).hasBindingsOrInitialValues = true;
|
||||
});
|
||||
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.split(/\s+/g).forEach(className => {
|
||||
this._initialClassValues[className] = true;
|
||||
registerIntoMap(this._classesIndex, className);
|
||||
(this as any).hasBindingsOrInitialValues = true;
|
||||
});
|
||||
this._initialClassValues = value.trim().split(/\s+/g);
|
||||
this._hasInitialValues = true;
|
||||
}
|
||||
|
||||
private _buildInitExpr(registry: Map<string, number>, initialValues: {[key: string]: any}):
|
||||
o.Expression|null {
|
||||
const exprs: o.Expression[] = [];
|
||||
const nameAndValueExprs: o.Expression[] = [];
|
||||
|
||||
// _c0 = [prop, prop2, prop3, ...]
|
||||
registry.forEach((value, key) => {
|
||||
const keyLiteral = o.literal(key);
|
||||
exprs.push(keyLiteral);
|
||||
const initialValue = initialValues[key];
|
||||
if (initialValue) {
|
||||
nameAndValueExprs.push(keyLiteral, o.literal(initialValue));
|
||||
/**
|
||||
* 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]));
|
||||
}
|
||||
});
|
||||
|
||||
if (nameAndValueExprs.length) {
|
||||
// _c0 = [... MARKER ...]
|
||||
exprs.push(o.literal(InitialStylingFlags.VALUES_MODE));
|
||||
// _c0 = [prop, VALUE, prop2, VALUE2, ...]
|
||||
exprs.push(...nameAndValueExprs);
|
||||
}
|
||||
|
||||
return exprs.length ? o.literalArr(exprs) : null;
|
||||
// [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]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildCreateLevelInstruction(sourceSpan: ParseSourceSpan|null, constantPool: ConstantPool):
|
||||
/**
|
||||
* 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)
|
||||
* to the directive host element.
|
||||
*/
|
||||
buildDirectiveHostAttrsInstruction(sourceSpan: ParseSourceSpan|null, constantPool: ConstantPool):
|
||||
StylingInstruction|null {
|
||||
if (this.hasBindingsOrInitialValues) {
|
||||
const initialClasses = this._buildInitExpr(this._classesIndex, this._initialClassValues);
|
||||
const initialStyles = this._buildInitExpr(this._stylesIndex, this._initialStyleValues);
|
||||
|
||||
// in the event that a [style] binding is used then sanitization will
|
||||
// always be imported because it is not possible to know ahead of time
|
||||
// whether style bindings will use or not use any sanitizable properties
|
||||
// that isStyleSanitizable() will detect
|
||||
const useSanitizer = this._useDefaultSanitizer;
|
||||
const params: (o.Expression)[] = [];
|
||||
|
||||
if (initialClasses) {
|
||||
// the template compiler handles initial class styling (e.g. class="foo") values
|
||||
// in a special command called `elementClass` so that the initial class
|
||||
// can be processed during runtime. These initial class values are bound to
|
||||
// a constant because the inital class values do not change (since they're static).
|
||||
params.push(constantPool.getConstLiteral(initialClasses, true));
|
||||
} else if (initialStyles || useSanitizer || this._directiveExpr) {
|
||||
// no point in having an extra `null` value unless there are follow-up params
|
||||
params.push(o.NULL_EXPR);
|
||||
}
|
||||
|
||||
if (initialStyles) {
|
||||
// the template compiler handles initial style (e.g. style="foo") values
|
||||
// in a special command called `elementStyle` so that the initial styles
|
||||
// can be processed during runtime. These initial styles values are bound to
|
||||
// a constant because the inital style values do not change (since they're static).
|
||||
params.push(constantPool.getConstLiteral(initialStyles, true));
|
||||
} else if (useSanitizer || this._directiveExpr) {
|
||||
// no point in having an extra `null` value unless there are follow-up params
|
||||
params.push(o.NULL_EXPR);
|
||||
}
|
||||
|
||||
if (useSanitizer || this._directiveExpr) {
|
||||
params.push(useSanitizer ? o.importExpr(R3.defaultStyleSanitizer) : o.NULL_EXPR);
|
||||
if (this._directiveExpr) {
|
||||
params.push(this._directiveExpr);
|
||||
if (this._hasInitialValues && this._directiveExpr) {
|
||||
return {
|
||||
sourceSpan,
|
||||
reference: R3.elementHostAttrs,
|
||||
buildParams: () => {
|
||||
const attrs: o.Expression[] = [];
|
||||
this.populateInitialStylingAttrs(attrs);
|
||||
return [this._directiveExpr !, getConstantLiteralFromArray(constantPool, attrs)];
|
||||
}
|
||||
}
|
||||
|
||||
return {sourceSpan, reference: R3.elementStyling, buildParams: () => params};
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private _buildStylingMap(valueConverter: ValueConverter): StylingInstruction|null {
|
||||
/**
|
||||
* Builds an instruction with all the expressions and parameters for `elementStyling`.
|
||||
*
|
||||
* The instruction generation code below is used for producing the AOT statement code which is
|
||||
* responsible for registering style/class bindings to an element.
|
||||
*/
|
||||
buildElementStylingInstruction(sourceSpan: ParseSourceSpan|null, constantPool: ConstantPool):
|
||||
StylingInstruction|null {
|
||||
if (this._hasBindings) {
|
||||
return {
|
||||
sourceSpan,
|
||||
reference: R3.elementStyling,
|
||||
buildParams: () => {
|
||||
// a string array of every style-based binding
|
||||
const styleBindingProps =
|
||||
this._singleStyleInputs ? this._singleStyleInputs.map(i => o.literal(i.name)) : [];
|
||||
// a string array of every class-based binding
|
||||
const classBindingNames =
|
||||
this._singleClassInputs ? this._singleClassInputs.map(i => o.literal(i.name)) : [];
|
||||
|
||||
// to salvage space in the AOT generated code, there is no point in passing
|
||||
// in `null` into a param if any follow-up params are not used. Therefore,
|
||||
// only when a trailing param is used then it will be filled with nulls in between
|
||||
// (otherwise a shorter amount of params will be filled). The code below helps
|
||||
// determine how many params are required in the expression code.
|
||||
//
|
||||
// min params => elementStyling()
|
||||
// max params => elementStyling(classBindings, styleBindings, sanitizer, directive)
|
||||
let expectedNumberOfArgs = 0;
|
||||
if (this._directiveExpr) {
|
||||
expectedNumberOfArgs = 4;
|
||||
} else if (this._useDefaultSanitizer) {
|
||||
expectedNumberOfArgs = 3;
|
||||
} else if (styleBindingProps.length) {
|
||||
expectedNumberOfArgs = 2;
|
||||
} else if (classBindingNames.length) {
|
||||
expectedNumberOfArgs = 1;
|
||||
}
|
||||
|
||||
const params: o.Expression[] = [];
|
||||
addParam(
|
||||
params, classBindingNames.length > 0,
|
||||
getConstantLiteralFromArray(constantPool, classBindingNames), 1,
|
||||
expectedNumberOfArgs);
|
||||
addParam(
|
||||
params, styleBindingProps.length > 0,
|
||||
getConstantLiteralFromArray(constantPool, styleBindingProps), 2,
|
||||
expectedNumberOfArgs);
|
||||
addParam(
|
||||
params, this._useDefaultSanitizer, o.importExpr(R3.defaultStyleSanitizer), 3,
|
||||
expectedNumberOfArgs);
|
||||
if (this._directiveExpr) {
|
||||
params.push(this._directiveExpr);
|
||||
}
|
||||
return params;
|
||||
}
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an instruction with all the expressions and parameters for `elementStylingMap`.
|
||||
*
|
||||
* The instruction data will contain all expressions for `elementStylingMap` to function
|
||||
* which include the `[style]` and `[class]` expression params (if they exist) as well as
|
||||
* the sanitizer and directive reference expression.
|
||||
*/
|
||||
buildElementStylingMapInstruction(valueConverter: ValueConverter): StylingInstruction|null {
|
||||
if (this._classMapInput || this._styleMapInput) {
|
||||
const stylingInput = this._classMapInput ! || this._styleMapInput !;
|
||||
|
||||
@ -332,18 +411,20 @@ export class StylingBuilder {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.hasBindingsOrInitialValues) {
|
||||
const mapInstruction = this._buildStylingMap(valueConverter);
|
||||
if (this._hasBindings) {
|
||||
const mapInstruction = this.buildElementStylingMapInstruction(valueConverter);
|
||||
if (mapInstruction) {
|
||||
instructions.push(mapInstruction);
|
||||
}
|
||||
instructions.push(...this._buildStyleInputs(valueConverter));
|
||||
instructions.push(...this._buildClassInputs(valueConverter));
|
||||
if (this._applyFnRequired) {
|
||||
instructions.push(this._buildApplyFn());
|
||||
}
|
||||
instructions.push(this._buildApplyFn());
|
||||
}
|
||||
return instructions;
|
||||
}
|
||||
@ -363,3 +444,26 @@ function isStyleSanitizable(prop: string): boolean {
|
||||
return prop === 'background-image' || prop === 'background' || prop === 'border-image' ||
|
||||
prop === 'filter' || prop === 'list-style' || prop === 'list-style-image';
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple helper function to either provide the constant literal that will house the value
|
||||
* here or a null value if the provided values are empty.
|
||||
*/
|
||||
function getConstantLiteralFromArray(
|
||||
constantPool: ConstantPool, values: o.Expression[]): o.Expression {
|
||||
return values.length ? constantPool.getConstLiteral(o.literalArr(values), true) : o.NULL_EXPR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple helper function that adds a parameter or does nothing at all depending on the provided
|
||||
* predicate and totalExpectedArgs values
|
||||
*/
|
||||
function addParam(
|
||||
params: o.Expression[], predicate: boolean, value: o.Expression, argNumber: number,
|
||||
totalExpectedArgs: number) {
|
||||
if (predicate) {
|
||||
params.push(value);
|
||||
} else if (argNumber < totalExpectedArgs) {
|
||||
params.push(o.NULL_EXPR);
|
||||
}
|
||||
}
|
@ -35,7 +35,7 @@ import {I18nContext} from './i18n/context';
|
||||
import {I18nMetaVisitor} from './i18n/meta';
|
||||
import {getSerializedI18nContent} from './i18n/serializer';
|
||||
import {I18N_ICU_MAPPING_PREFIX, assembleBoundTextPlaceholders, assembleI18nBoundString, formatI18nPlaceholderName, getTranslationConstPrefix, getTranslationDeclStmts, icuFromI18nMessage, isI18nRootNode, isSingleI18nIcu, metaFromI18nMessage, placeholdersToParams, wrapI18nPlaceholder} from './i18n/util';
|
||||
import {StylingBuilder, StylingInstruction} from './styling';
|
||||
import {StylingBuilder, StylingInstruction} from './styling_builder';
|
||||
import {CONTEXT_NAME, IMPLICIT_REFERENCE, NON_BINDABLE_ATTR, REFERENCE_PREFIX, RENDER_FLAGS, asLiteral, getAttrsForDirectiveMatching, invalid, trimTrailingNulls, unsupported} from './util';
|
||||
|
||||
function mapBindingToInstruction(type: BindingType): o.ExternalReference|undefined {
|
||||
@ -532,7 +532,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
|
||||
// this will build the instructions so that they fall into the following syntax
|
||||
// add attributes for directive matching purposes
|
||||
attributes.push(...this.prepareSyntheticAndSelectOnlyAttrs(allOtherInputs, element.outputs));
|
||||
attributes.push(...this.prepareSyntheticAndSelectOnlyAttrs(
|
||||
allOtherInputs, element.outputs, stylingBuilder));
|
||||
parameters.push(this.toAttrsParam(attributes));
|
||||
|
||||
// local refs (ex.: <div #foo #bar="baz">)
|
||||
@ -562,11 +563,11 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
return element.children.length > 0;
|
||||
};
|
||||
|
||||
const createSelfClosingInstruction = !stylingBuilder.hasBindingsOrInitialValues &&
|
||||
const createSelfClosingInstruction = !stylingBuilder.hasBindingsOrInitialValues() &&
|
||||
!isNgContainer && element.outputs.length === 0 && i18nAttrs.length === 0 && !hasChildren();
|
||||
|
||||
const createSelfClosingI18nInstruction = !createSelfClosingInstruction &&
|
||||
!stylingBuilder.hasBindingsOrInitialValues && hasTextChildrenOnly(element.children);
|
||||
!stylingBuilder.hasBindingsOrInitialValues() && hasTextChildrenOnly(element.children);
|
||||
|
||||
if (createSelfClosingInstruction) {
|
||||
this.creationInstruction(element.sourceSpan, R3.element, trimTrailingNulls(parameters));
|
||||
@ -616,10 +617,16 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
}
|
||||
}
|
||||
|
||||
// initial styling for static style="..." and class="..." attributes
|
||||
// The style bindings code is placed into two distinct blocks within the template function AOT
|
||||
// code: creation and update. The creation code contains the `elementStyling` instructions
|
||||
// which will apply the collected binding values to the element. `elementStyling` is
|
||||
// designed to run inside of `elementStart` and `elementEnd`. The update instructions
|
||||
// (things like `elementStyleProp`, `elementClassProp`, etc..) are applied later on in this
|
||||
// file
|
||||
this.processStylingInstruction(
|
||||
implicit,
|
||||
stylingBuilder.buildCreateLevelInstruction(element.sourceSpan, this.constantPool), true);
|
||||
stylingBuilder.buildElementStylingInstruction(element.sourceSpan, this.constantPool),
|
||||
true);
|
||||
|
||||
// Generate Listeners (outputs)
|
||||
element.outputs.forEach((outputAst: t.BoundEvent) => {
|
||||
@ -629,6 +636,10 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
});
|
||||
}
|
||||
|
||||
// the code here will collect all update-level styling instructions and add them to the
|
||||
// update block of the template function AOT code. Instructions like `elementStyleProp`,
|
||||
// `elementStylingMap`, `elementClassProp` and `elementStylingApply` are all generated
|
||||
// and assign in the code below.
|
||||
stylingBuilder.buildUpdateLevelInstructions(this._valueConverter).forEach(instruction => {
|
||||
this.processStylingInstruction(implicit, instruction, false);
|
||||
});
|
||||
@ -934,8 +945,26 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
}
|
||||
}
|
||||
|
||||
private prepareSyntheticAndSelectOnlyAttrs(inputs: t.BoundAttribute[], outputs: t.BoundEvent[]):
|
||||
o.Expression[] {
|
||||
/**
|
||||
* Prepares all attribute expression values for the `TAttributes` array.
|
||||
*
|
||||
* The purpose of this function is to properly construct an attributes array that
|
||||
* is passed into the `elementStart` (or just `element`) functions. Because there
|
||||
* are many different types of attributes, the array needs to be constructed in a
|
||||
* special way so that `elementStart` can properly evaluate them.
|
||||
*
|
||||
* The format looks like this:
|
||||
*
|
||||
* ```
|
||||
* attrs = [prop, value, prop2, value2,
|
||||
* CLASSES, class1, class2,
|
||||
* STYLES, style1, value1, style2, value2,
|
||||
* SELECT_ONLY, name1, name2, name2, ...]
|
||||
* ```
|
||||
*/
|
||||
private prepareSyntheticAndSelectOnlyAttrs(
|
||||
inputs: t.BoundAttribute[], outputs: t.BoundEvent[],
|
||||
styles?: StylingBuilder): o.Expression[] {
|
||||
const attrExprs: o.Expression[] = [];
|
||||
const nonSyntheticInputs: t.BoundAttribute[] = [];
|
||||
|
||||
@ -954,6 +983,13 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
});
|
||||
}
|
||||
|
||||
// it's important that this occurs before SelectOnly because once `elementStart`
|
||||
// comes across the SelectOnly marker then it will continue reading each value as
|
||||
// as single property value cell by cell.
|
||||
if (styles) {
|
||||
styles.populateInitialStylingAttrs(attrExprs);
|
||||
}
|
||||
|
||||
if (nonSyntheticInputs.length || outputs.length) {
|
||||
attrExprs.push(o.literal(core.AttributeMarker.SelectOnly));
|
||||
nonSyntheticInputs.forEach((i: t.BoundAttribute) => attrExprs.push(asLiteral(i.name)));
|
||||
|
@ -10,55 +10,53 @@ import {hyphenate, parse as parseStyle, stripUnnecessaryQuotes} from '../../src/
|
||||
describe('style parsing', () => {
|
||||
it('should parse empty or blank strings', () => {
|
||||
const result1 = parseStyle('');
|
||||
expect(result1).toEqual({});
|
||||
expect(result1).toEqual([]);
|
||||
|
||||
const result2 = parseStyle(' ');
|
||||
expect(result2).toEqual({});
|
||||
expect(result2).toEqual([]);
|
||||
});
|
||||
|
||||
it('should parse a string into a key/value map', () => {
|
||||
const result = parseStyle('width:100px;height:200px;opacity:0');
|
||||
expect(result).toEqual({width: '100px', height: '200px', opacity: '0'});
|
||||
expect(result).toEqual(['width', '100px', 'height', '200px', 'opacity', '0']);
|
||||
});
|
||||
|
||||
it('should trim values and properties', () => {
|
||||
const result = parseStyle('width :333px ; height:666px ; opacity: 0.5;');
|
||||
expect(result).toEqual({width: '333px', height: '666px', opacity: '0.5'});
|
||||
expect(result).toEqual(['width', '333px', 'height', '666px', 'opacity', '0.5']);
|
||||
});
|
||||
|
||||
it('should chomp out start/end quotes', () => {
|
||||
const result = parseStyle(
|
||||
'content: "foo"; opacity: \'0.5\'; font-family: "Verdana", Helvetica, "sans-serif"');
|
||||
expect(result).toEqual(
|
||||
{content: 'foo', opacity: '0.5', 'font-family': '"Verdana", Helvetica, "sans-serif"'});
|
||||
['content', 'foo', 'opacity', '0.5', 'font-family', '"Verdana", Helvetica, "sans-serif"']);
|
||||
});
|
||||
|
||||
it('should not mess up with quoted strings that contain [:;] values', () => {
|
||||
const result = parseStyle('content: "foo; man: guy"; width: 100px');
|
||||
expect(result).toEqual({content: 'foo; man: guy', width: '100px'});
|
||||
expect(result).toEqual(['content', 'foo; man: guy', 'width', '100px']);
|
||||
});
|
||||
|
||||
it('should not mess up with quoted strings that contain inner quote values', () => {
|
||||
const quoteStr = '"one \'two\' three \"four\" five"';
|
||||
const result = parseStyle(`content: ${quoteStr}; width: 123px`);
|
||||
expect(result).toEqual({content: quoteStr, width: '123px'});
|
||||
expect(result).toEqual(['content', quoteStr, 'width', '123px']);
|
||||
});
|
||||
|
||||
it('should respect parenthesis that are placed within a style', () => {
|
||||
const result = parseStyle('background-image: url("foo.jpg")');
|
||||
expect(result).toEqual({'background-image': 'url("foo.jpg")'});
|
||||
expect(result).toEqual(['background-image', 'url("foo.jpg")']);
|
||||
});
|
||||
|
||||
it('should respect multi-level parenthesis that contain special [:;] characters', () => {
|
||||
const result = parseStyle('color: rgba(calc(50 * 4), var(--cool), :5;); height: 100px;');
|
||||
expect(result).toEqual({color: 'rgba(calc(50 * 4), var(--cool), :5;)', height: '100px'});
|
||||
expect(result).toEqual(['color', 'rgba(calc(50 * 4), var(--cool), :5;)', 'height', '100px']);
|
||||
});
|
||||
|
||||
it('should hyphenate style properties from camel case', () => {
|
||||
const result = parseStyle('borderWidth: 200px');
|
||||
expect(result).toEqual({
|
||||
'border-width': '200px',
|
||||
});
|
||||
expect(result).toEqual(['border-width', '200px']);
|
||||
});
|
||||
|
||||
describe('quote chomping', () => {
|
||||
|
Reference in New Issue
Block a user