feat(ivy): provide support for map-based host bindings for [style] and [class] (#28246)
Up until now, `[style]` and `[class]` bindings (the map-based ones) have only worked as template bindings and have not been supported at all inside of host bindings. This patch ensures that multiple host binding sources (components and directives) all properly assign style values and merge them correctly in terms of priority. Jira: FW-882 PR Close #28246
This commit is contained in:
committed by
Miško Hevery
parent
e5861e1c79
commit
fe8301c462
@@ -1062,6 +1062,84 @@ describe('compiler compliance: styling', () => {
|
||||
expectEmit(result.source, template, 'Incorrect template');
|
||||
});
|
||||
|
||||
it('should generate override instructions for only single-level styling bindings when !important is present',
|
||||
() => {
|
||||
const files = {
|
||||
app: {
|
||||
'spec.ts': `
|
||||
import {Component, NgModule, HostBinding} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'my-component',
|
||||
template: \`
|
||||
<div [style!important]="myStyleExp"
|
||||
[class!important]="myClassExp"
|
||||
[style.height!important]="myHeightExp"
|
||||
[class.bar!important]="myBarClassExp"></div>
|
||||
\`,
|
||||
host: {
|
||||
'[style!important]': 'myStyleExp',
|
||||
'[class!important]': 'myClassExp'
|
||||
}
|
||||
})
|
||||
export class MyComponent {
|
||||
@HostBinding('class.foo!important')
|
||||
myFooClassExp = true;
|
||||
|
||||
@HostBinding('style.width!important')
|
||||
myWidthExp = '100px';
|
||||
|
||||
myBarClassExp = true;
|
||||
myHeightExp = '200px';
|
||||
}
|
||||
|
||||
@NgModule({declarations: [MyComponent]})
|
||||
export class MyModule {}
|
||||
`
|
||||
}
|
||||
};
|
||||
|
||||
const template = `
|
||||
const _c2 = ["bar"];
|
||||
const _c3 = ["height"];
|
||||
…
|
||||
function MyComponent_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
$r3$.ɵelementStart(0, "div");
|
||||
$r3$.ɵelementStyling(_c2, _c3, $r3$.ɵdefaultStyleSanitizer);
|
||||
$r3$.ɵelementEnd();
|
||||
}
|
||||
if (rf & 2) {
|
||||
$r3$.ɵelementStylingMap(0, ctx.myClassExp, ctx.myStyleExp);
|
||||
$r3$.ɵelementStyleProp(0, 0, ctx.myHeightExp, null, true);
|
||||
$r3$.ɵelementClassProp(0, 0, ctx.myBarClassExp, null, true);
|
||||
$r3$.ɵelementStylingApply(0);
|
||||
}
|
||||
},
|
||||
`;
|
||||
|
||||
const hostBindings = `
|
||||
const _c0 = ["foo"];
|
||||
const _c1 = ["width"];
|
||||
…
|
||||
hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) {
|
||||
if (rf & 1) {
|
||||
$r3$.ɵelementStyling(_c0, _c1, $r3$.ɵdefaultStyleSanitizer, ctx);
|
||||
}
|
||||
if (rf & 2) {
|
||||
$r3$.ɵelementStylingMap(elIndex, ctx.myClassExp, ctx.myStyleExp, ctx);
|
||||
$r3$.ɵelementStyleProp(elIndex, 0, ctx.myWidthExp, null, ctx, true);
|
||||
$r3$.ɵelementClassProp(elIndex, 0, ctx.myFooClassExp, ctx, true);
|
||||
$r3$.ɵelementStylingApply(elIndex, ctx);
|
||||
}
|
||||
},
|
||||
`;
|
||||
|
||||
const result = compile(files, angularFiles);
|
||||
expectEmit(result.source, hostBindings, 'Incorrect template');
|
||||
expectEmit(result.source, template, 'Incorrect template');
|
||||
});
|
||||
|
||||
it('should generate styling instructions for multiple directives that contain host binding definitions',
|
||||
() => {
|
||||
const files = {
|
||||
|
||||
@@ -39,8 +39,8 @@ const EMPTY_ARRAY: any[] = [];
|
||||
// If there is a match, the first matching group will contain the attribute name to bind.
|
||||
const ATTR_REGEX = /attr\.([^\]]+)/;
|
||||
|
||||
function getStylingPrefix(propName: string): string {
|
||||
return propName.substring(0, 5).toLowerCase();
|
||||
function getStylingPrefix(name: string): string {
|
||||
return name.substring(0, 5); // style or class
|
||||
}
|
||||
|
||||
function baseDirectiveFields(
|
||||
@@ -672,14 +672,9 @@ function createHostBindingsFunction(
|
||||
const bindings = bindingParser.createBoundHostProperties(directiveSummary, hostBindingSourceSpan);
|
||||
(bindings || []).forEach((binding: ParsedProperty) => {
|
||||
const name = binding.name;
|
||||
const stylePrefix = getStylingPrefix(name);
|
||||
if (stylePrefix === 'style') {
|
||||
const {propertyName, unit} = parseNamedProperty(name);
|
||||
styleBuilder.registerStyleInput(propertyName, binding.expression, unit, binding.sourceSpan);
|
||||
} else if (stylePrefix === 'class') {
|
||||
styleBuilder.registerClassInput(
|
||||
parseNamedProperty(name).propertyName, binding.expression, binding.sourceSpan);
|
||||
} else {
|
||||
const stylingInputWasSet =
|
||||
styleBuilder.registerInputBasedOnName(name, binding.expression, binding.sourceSpan);
|
||||
if (!stylingInputWasSet) {
|
||||
// resolve literal arrays and literal objects
|
||||
const value = binding.expression.visit(getValueConverter());
|
||||
const bindingExpr = bindingFn(bindingContext, value);
|
||||
@@ -923,19 +918,3 @@ function compileStyles(styles: string[], selector: string, hostSelector: string)
|
||||
const shadowCss = new ShadowCss();
|
||||
return styles.map(style => { return shadowCss !.shimCssText(style, selector, hostSelector); });
|
||||
}
|
||||
|
||||
function parseNamedProperty(name: string): {propertyName: string, unit: string} {
|
||||
let unit = '';
|
||||
let propertyName = '';
|
||||
const index = name.indexOf('.');
|
||||
if (index > 0) {
|
||||
const unitIndex = name.lastIndexOf('.');
|
||||
if (unitIndex !== index) {
|
||||
unit = name.substring(unitIndex + 1, name.length);
|
||||
propertyName = name.substring(index + 1, unitIndex);
|
||||
} else {
|
||||
propertyName = name.substring(index + 1, name.length);
|
||||
}
|
||||
}
|
||||
return {propertyName, unit};
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {Identifiers as R3} from '../r3_identifiers';
|
||||
import {parse as parseStyle} from './style_parser';
|
||||
import {ValueConverter} from './template';
|
||||
|
||||
const IMPORTANT_FLAG = '!important';
|
||||
|
||||
/**
|
||||
* A styling expression summary that is to be processed by the compiler
|
||||
@@ -31,7 +32,8 @@ export interface Instruction {
|
||||
* An internal record of the input data for a styling binding
|
||||
*/
|
||||
interface BoundStylingEntry {
|
||||
name: string;
|
||||
hasOverrideFlag: boolean;
|
||||
name: string|null;
|
||||
unit: string|null;
|
||||
sourceSpan: ParseSourceSpan;
|
||||
value: AST;
|
||||
@@ -123,51 +125,70 @@ export class StylingBuilder {
|
||||
// will therefore skip all style/class resolution that is present
|
||||
// with style="", [style]="" and [style.prop]="", class="",
|
||||
// [class.prop]="". [class]="" assignments
|
||||
const name = input.name;
|
||||
let binding: BoundStylingEntry|null = null;
|
||||
let name = input.name;
|
||||
switch (input.type) {
|
||||
case BindingType.Property:
|
||||
if (name == 'style') {
|
||||
binding = this.registerStyleInput(null, input.value, '', input.sourceSpan);
|
||||
} else if (isClassBinding(input.name)) {
|
||||
binding = this.registerClassInput(null, input.value, input.sourceSpan);
|
||||
}
|
||||
binding = this.registerInputBasedOnName(name, input.value, input.sourceSpan);
|
||||
break;
|
||||
case BindingType.Style:
|
||||
binding = this.registerStyleInput(input.name, input.value, input.unit, input.sourceSpan);
|
||||
binding = this.registerStyleInput(name, false, input.value, input.sourceSpan, input.unit);
|
||||
break;
|
||||
case BindingType.Class:
|
||||
binding = this.registerClassInput(input.name, input.value, input.sourceSpan);
|
||||
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 nameToMatch = name.substring(0, 5); // class | style
|
||||
const isStyle = nameToMatch === 'style';
|
||||
const isClass = isStyle ? false : (nameToMatch === '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(
|
||||
propertyName: string|null, value: AST, unit: string|null,
|
||||
sourceSpan: ParseSourceSpan): BoundStylingEntry {
|
||||
const entry = { name: propertyName, unit, value, sourceSpan } as BoundStylingEntry;
|
||||
if (propertyName) {
|
||||
(this._singleStyleInputs = this._singleStyleInputs || []).push(entry);
|
||||
this._useDefaultSanitizer = this._useDefaultSanitizer || isStyleSanitizable(propertyName);
|
||||
registerIntoMap(this._stylesIndex, propertyName);
|
||||
} else {
|
||||
name: string, isMapBased: boolean, value: AST, sourceSpan: ParseSourceSpan,
|
||||
unit?: string|null): BoundStylingEntry {
|
||||
const {property, hasOverrideFlag, unit: bindingUnit} = parseProperty(name);
|
||||
const entry: BoundStylingEntry = {
|
||||
name: property,
|
||||
unit: unit || bindingUnit, value, sourceSpan, hasOverrideFlag
|
||||
};
|
||||
if (isMapBased) {
|
||||
this._useDefaultSanitizer = true;
|
||||
this._styleMapInput = entry;
|
||||
} else {
|
||||
(this._singleStyleInputs = this._singleStyleInputs || []).push(entry);
|
||||
this._useDefaultSanitizer = this._useDefaultSanitizer || isStyleSanitizable(name);
|
||||
registerIntoMap(this._stylesIndex, property);
|
||||
}
|
||||
this._lastStylingInput = entry;
|
||||
this.hasBindings = true;
|
||||
return entry;
|
||||
}
|
||||
|
||||
registerClassInput(className: string|null, value: AST, sourceSpan: ParseSourceSpan):
|
||||
registerClassInput(name: string, isMapBased: boolean, value: AST, sourceSpan: ParseSourceSpan):
|
||||
BoundStylingEntry {
|
||||
const entry = { name: className, value, sourceSpan } as BoundStylingEntry;
|
||||
if (className) {
|
||||
(this._singleClassInputs = this._singleClassInputs || []).push(entry);
|
||||
registerIntoMap(this._classesIndex, className);
|
||||
} else {
|
||||
const {property, hasOverrideFlag} = parseProperty(name);
|
||||
const entry:
|
||||
BoundStylingEntry = {name: property, value, sourceSpan, hasOverrideFlag, unit: null};
|
||||
if (isMapBased) {
|
||||
this._classMapInput = entry;
|
||||
} else {
|
||||
(this._singleClassInputs = this._singleClassInputs || []).push(entry);
|
||||
registerIntoMap(this._classesIndex, property);
|
||||
}
|
||||
this._lastStylingInput = entry;
|
||||
this.hasBindings = true;
|
||||
@@ -235,6 +256,7 @@ export class StylingBuilder {
|
||||
reference: R3.elementHostAttrs,
|
||||
allocateBindingSlots: 0,
|
||||
buildParams: () => {
|
||||
// params => elementHostAttrs(directive, attrs)
|
||||
this.populateInitialStylingAttrs(attrs);
|
||||
return [this._directiveExpr !, getConstantLiteralFromArray(constantPool, attrs)];
|
||||
}
|
||||
@@ -337,24 +359,26 @@ export class StylingBuilder {
|
||||
reference: R3.elementStylingMap,
|
||||
allocateBindingSlots: totalBindingSlotsRequired,
|
||||
buildParams: (convertFn: (value: any) => o.Expression) => {
|
||||
const params: o.Expression[] = [this._elementIndexExpr];
|
||||
|
||||
if (mapBasedClassValue) {
|
||||
params.push(convertFn(mapBasedClassValue));
|
||||
} else if (this._styleMapInput) {
|
||||
params.push(o.NULL_EXPR);
|
||||
}
|
||||
|
||||
if (mapBasedStyleValue) {
|
||||
params.push(convertFn(mapBasedStyleValue));
|
||||
} else if (this._directiveExpr) {
|
||||
params.push(o.NULL_EXPR);
|
||||
}
|
||||
|
||||
// min params => elementStylingMap(index, classMap)
|
||||
// max params => elementStylingMap(index, classMap, styleMap, directive)
|
||||
let expectedNumberOfArgs = 0;
|
||||
if (this._directiveExpr) {
|
||||
params.push(this._directiveExpr);
|
||||
expectedNumberOfArgs = 4;
|
||||
} else if (mapBasedStyleValue) {
|
||||
expectedNumberOfArgs = 3;
|
||||
} else if (mapBasedClassValue) {
|
||||
// index and class = 2
|
||||
expectedNumberOfArgs = 2;
|
||||
}
|
||||
|
||||
const params: o.Expression[] = [this._elementIndexExpr];
|
||||
addParam(
|
||||
params, mapBasedClassValue, mapBasedClassValue ? convertFn(mapBasedClassValue) : null,
|
||||
2, expectedNumberOfArgs);
|
||||
addParam(
|
||||
params, mapBasedStyleValue, mapBasedStyleValue ? convertFn(mapBasedStyleValue) : null,
|
||||
3, expectedNumberOfArgs);
|
||||
addParam(params, this._directiveExpr, this._directiveExpr, 4, expectedNumberOfArgs);
|
||||
return params;
|
||||
}
|
||||
};
|
||||
@@ -367,14 +391,18 @@ export class StylingBuilder {
|
||||
allowUnits: boolean, valueConverter: ValueConverter): Instruction[] {
|
||||
let totalBindingSlotsRequired = 0;
|
||||
return inputs.map(input => {
|
||||
const bindingIndex: number = mapIndex.get(input.name) !;
|
||||
const bindingIndex: number = mapIndex.get(input.name !) !;
|
||||
const value = input.value.visit(valueConverter);
|
||||
totalBindingSlotsRequired += (value instanceof Interpolation) ? value.expressions.length : 0;
|
||||
return {
|
||||
sourceSpan: input.sourceSpan,
|
||||
allocateBindingSlots: totalBindingSlotsRequired, reference,
|
||||
buildParams: (convertFn: (value: any) => o.Expression) => {
|
||||
// min params => elementStlyingProp(elmIndex, bindingIndex, value)
|
||||
// max params => elementStlyingProp(elmIndex, bindingIndex, value, overrideFlag)
|
||||
|
||||
const params = [this._elementIndexExpr, o.literal(bindingIndex), convertFn(value)];
|
||||
|
||||
if (allowUnits) {
|
||||
if (input.unit) {
|
||||
params.push(o.literal(input.unit));
|
||||
@@ -385,7 +413,14 @@ export class StylingBuilder {
|
||||
|
||||
if (this._directiveExpr) {
|
||||
params.push(this._directiveExpr);
|
||||
} else if (input.hasOverrideFlag) {
|
||||
params.push(o.NULL_EXPR);
|
||||
}
|
||||
|
||||
if (input.hasOverrideFlag) {
|
||||
params.push(o.literal(true));
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
};
|
||||
@@ -414,6 +449,8 @@ export class StylingBuilder {
|
||||
reference: R3.elementStylingApply,
|
||||
allocateBindingSlots: 0,
|
||||
buildParams: () => {
|
||||
// min params => elementStylingApply(elmIndex)
|
||||
// max params => elementStylingApply(elmIndex, directive)
|
||||
const params: o.Expression[] = [this._elementIndexExpr];
|
||||
if (this._directiveExpr) {
|
||||
params.push(this._directiveExpr);
|
||||
@@ -442,10 +479,6 @@ export class StylingBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
function isClassBinding(name: string): boolean {
|
||||
return name == 'className' || name == 'class';
|
||||
}
|
||||
|
||||
function registerIntoMap(map: Map<string, number>, key: string) {
|
||||
if (!map.has(key)) {
|
||||
map.set(key, map.size);
|
||||
@@ -471,11 +504,31 @@ function getConstantLiteralFromArray(
|
||||
* predicate and totalExpectedArgs values
|
||||
*/
|
||||
function addParam(
|
||||
params: o.Expression[], predicate: boolean, value: o.Expression, argNumber: number,
|
||||
params: o.Expression[], predicate: any, value: o.Expression | null, argNumber: number,
|
||||
totalExpectedArgs: number) {
|
||||
if (predicate) {
|
||||
if (predicate && value) {
|
||||
params.push(value);
|
||||
} else if (argNumber < totalExpectedArgs) {
|
||||
params.push(o.NULL_EXPR);
|
||||
}
|
||||
}
|
||||
|
||||
export function parseProperty(name: string):
|
||||
{property: string, unit: string, hasOverrideFlag: boolean} {
|
||||
let hasOverrideFlag = false;
|
||||
const overrideIndex = name.indexOf(IMPORTANT_FLAG);
|
||||
if (overrideIndex !== -1) {
|
||||
name = overrideIndex > 0 ? name.substring(0, overrideIndex) : '';
|
||||
hasOverrideFlag = true;
|
||||
}
|
||||
|
||||
let unit = '';
|
||||
let property = name;
|
||||
const unitIndex = name.lastIndexOf('.');
|
||||
if (unitIndex > 0) {
|
||||
unit = name.substr(unitIndex + 1);
|
||||
property = name.substring(0, unitIndex);
|
||||
}
|
||||
|
||||
return {property, unit, hasOverrideFlag};
|
||||
}
|
||||
|
||||
@@ -549,7 +549,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
const allOtherInputs: t.BoundAttribute[] = [];
|
||||
|
||||
element.inputs.forEach((input: t.BoundAttribute) => {
|
||||
if (!stylingBuilder.registerBoundInput(input)) {
|
||||
const stylingInputWasSet = stylingBuilder.registerBoundInput(input);
|
||||
if (!stylingInputWasSet) {
|
||||
if (input.type === BindingType.Property) {
|
||||
if (input.i18n) {
|
||||
i18nAttrs.push(input);
|
||||
|
||||
@@ -897,4 +897,4 @@ function isEmptyExpression(ast: AST): boolean {
|
||||
ast = ast.ast;
|
||||
}
|
||||
return ast instanceof EmptyExpr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,9 +37,9 @@ import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert';
|
||||
import {appendChild, appendProjectedNode, createTextNode, getLViewChild, insertView, removeView} from './node_manipulation';
|
||||
import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher';
|
||||
import {decreaseElementDepthCount, enterView, getBindingsEnabled, getCheckNoChangesMode, getContextLView, getCurrentDirectiveDef, getElementDepthCount, getIsParent, getLView, getPreviousOrParentTNode, increaseElementDepthCount, isCreationMode, leaveView, nextContextImpl, resetComponentState, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setCurrentQueryIndex, setIsParent, setPreviousOrParentTNode} from './state';
|
||||
import {getInitialClassNameValue, initializeStaticContext as initializeStaticStylingContext, patchContextWithStaticAttrs, renderInitialStylesAndClasses, renderStyling, updateClassProp as updateElementClassProp, updateContextWithBindings, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling/class_and_style_bindings';
|
||||
import {getInitialClassNameValue, getInitialStyleStringValue, initializeStaticContext as initializeStaticStylingContext, patchContextWithStaticAttrs, renderInitialClasses, renderInitialStyles, renderStyling, updateClassProp as updateElementClassProp, updateContextWithBindings, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling/class_and_style_bindings';
|
||||
import {BoundPlayerFactory} from './styling/player_factory';
|
||||
import {ANIMATION_PROP_PREFIX, createEmptyStylingContext, getStylingContext, hasClassInput, hasStyling, isAnimationProp} from './styling/util';
|
||||
import {ANIMATION_PROP_PREFIX, allocateDirectiveIntoContext, createEmptyStylingContext, forceClassesAsString, forceStylesAsString, getStylingContext, hasClassInput, hasStyleInput, hasStyling, isAnimationProp} from './styling/util';
|
||||
import {NO_CHANGE} from './tokens';
|
||||
import {INTERPOLATION_DELIMITER, findComponentView, getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getRootContext, getRootView, getTNode, isComponent, isComponentDef, isContentQueryHost, loadInternal, readElementValue, readPatchedLView, renderStringify} from './util';
|
||||
|
||||
@@ -633,12 +633,16 @@ export function elementStart(
|
||||
if (inputData && inputData.hasOwnProperty('class')) {
|
||||
tNode.flags |= TNodeFlags.hasClassInput;
|
||||
}
|
||||
if (inputData && inputData.hasOwnProperty('style')) {
|
||||
tNode.flags |= TNodeFlags.hasStyleInput;
|
||||
}
|
||||
}
|
||||
|
||||
// There is no point in rendering styles when a class directive is present since
|
||||
// it will take that over for us (this will be removed once #FW-882 is in).
|
||||
if (tNode.stylingTemplate && (tNode.flags & TNodeFlags.hasClassInput) === 0) {
|
||||
renderInitialStylesAndClasses(native, tNode.stylingTemplate, lView[RENDERER]);
|
||||
if (tNode.stylingTemplate) {
|
||||
renderInitialClasses(native, tNode.stylingTemplate, lView[RENDERER]);
|
||||
renderInitialStyles(native, tNode.stylingTemplate, lView[RENDERER]);
|
||||
}
|
||||
|
||||
const currentQueries = lView[QUERIES];
|
||||
@@ -1072,6 +1076,20 @@ export function elementEnd(): void {
|
||||
previousOrParentTNode = previousOrParentTNode.parent !;
|
||||
setPreviousOrParentTNode(previousOrParentTNode);
|
||||
}
|
||||
|
||||
// there may be some instructions that need to run in a specific
|
||||
// order because the CREATE block in a directive runs before the
|
||||
// CREATE block in a template. To work around this instructions
|
||||
// can get access to the function array below and defer any code
|
||||
// to run after the element is created.
|
||||
let fns: Function[]|null;
|
||||
if (fns = previousOrParentTNode.onElementCreationFns) {
|
||||
for (let i = 0; i < fns.length; i++) {
|
||||
fns[i]();
|
||||
}
|
||||
previousOrParentTNode.onElementCreationFns = null;
|
||||
}
|
||||
|
||||
ngDevMode && assertNodeType(previousOrParentTNode, TNodeType.Element);
|
||||
const lView = getLView();
|
||||
const currentQueries = lView[QUERIES];
|
||||
@@ -1090,6 +1108,12 @@ export function elementEnd(): void {
|
||||
setInputsForProperty(
|
||||
lView, previousOrParentTNode.inputs !['class'] !, getInitialClassNameValue(stylingContext));
|
||||
}
|
||||
if (hasStyleInput(previousOrParentTNode)) {
|
||||
const stylingContext = getStylingContext(previousOrParentTNode.index, lView);
|
||||
setInputsForProperty(
|
||||
lView, previousOrParentTNode.inputs !['style'] !,
|
||||
getInitialStyleStringValue(stylingContext));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1297,7 +1321,8 @@ export function createTNode(
|
||||
child: null,
|
||||
parent: tParent,
|
||||
stylingTemplate: null,
|
||||
projection: null
|
||||
projection: null,
|
||||
onElementCreationFns: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1412,9 +1437,33 @@ export function elementStyling(
|
||||
if (!tNode.stylingTemplate) {
|
||||
tNode.stylingTemplate = createEmptyStylingContext();
|
||||
}
|
||||
|
||||
if (directive) {
|
||||
// this will ALWAYS happen first before the bindings are applied so that the ordering
|
||||
// of directives is correct (otherwise if a follow-up directive contains static styling,
|
||||
// which is applied through elementHostAttrs, then it may end up being listed in the
|
||||
// context directive array before a former one (because the former one didn't contain
|
||||
// any static styling values))
|
||||
allocateDirectiveIntoContext(tNode.stylingTemplate, directive);
|
||||
|
||||
const fns = tNode.onElementCreationFns = tNode.onElementCreationFns || [];
|
||||
fns.push(
|
||||
() => initElementStyling(
|
||||
tNode, classBindingNames, styleBindingNames, styleSanitizer, directive));
|
||||
} else {
|
||||
// this will make sure that the root directive (the template) will always be
|
||||
// run FIRST before all the other styling properties are populated into the
|
||||
// context...
|
||||
initElementStyling(tNode, classBindingNames, styleBindingNames, styleSanitizer, directive);
|
||||
}
|
||||
}
|
||||
|
||||
function initElementStyling(
|
||||
tNode: TNode, classBindingNames?: string[] | null, styleBindingNames?: string[] | null,
|
||||
styleSanitizer?: StyleSanitizeFn | null, directive?: {}): void {
|
||||
updateContextWithBindings(
|
||||
tNode.stylingTemplate !, directive || null, classBindingNames, styleBindingNames,
|
||||
styleSanitizer, hasClassInput(tNode));
|
||||
styleSanitizer);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1521,7 +1570,7 @@ components
|
||||
*/
|
||||
export function elementStyleProp(
|
||||
index: number, styleIndex: number, value: string | number | String | PlayerFactory | null,
|
||||
suffix?: string | null, directive?: {}): void {
|
||||
suffix?: string | null, directive?: {}, forceOverride?: boolean): void {
|
||||
let valueToAdd: string|null = null;
|
||||
if (value !== null) {
|
||||
if (suffix) {
|
||||
@@ -1537,7 +1586,8 @@ export function elementStyleProp(
|
||||
}
|
||||
}
|
||||
updateElementStyleProp(
|
||||
getStylingContext(index + HEADER_OFFSET, getLView()), styleIndex, valueToAdd, directive);
|
||||
getStylingContext(index + HEADER_OFFSET, getLView()), styleIndex, valueToAdd, directive,
|
||||
forceOverride);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1555,26 +1605,36 @@ export function elementStyleProp(
|
||||
* @param value A true/false value which will turn the class on or off.
|
||||
* @param directive Directive instance that is attempting to change styling. (Defaults to the
|
||||
* component of the current view).
|
||||
components
|
||||
* @param forceOverride Whether or not this value will be applied regardless of where it is being
|
||||
* set within the directive priority structure.
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export function elementClassProp(
|
||||
index: number, classIndex: number, value: boolean | PlayerFactory, directive?: {}): void {
|
||||
const onOrOffClassValue =
|
||||
(value instanceof BoundPlayerFactory) ? (value as BoundPlayerFactory<boolean>) : (!!value);
|
||||
index: number, classIndex: number, value: boolean | PlayerFactory, directive?: {},
|
||||
forceOverride?: boolean): void {
|
||||
const input = (value instanceof BoundPlayerFactory) ?
|
||||
(value as BoundPlayerFactory<boolean|null>) :
|
||||
booleanOrNull(value);
|
||||
updateElementClassProp(
|
||||
getStylingContext(index + HEADER_OFFSET, getLView()), classIndex, onOrOffClassValue,
|
||||
directive);
|
||||
getStylingContext(index + HEADER_OFFSET, getLView()), classIndex, input, directive,
|
||||
forceOverride);
|
||||
}
|
||||
|
||||
function booleanOrNull(value: any): boolean|null {
|
||||
if (typeof value === 'boolean') return value;
|
||||
return value ? true : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update style and/or class bindings using object literal.
|
||||
*
|
||||
* This instruction is meant apply styling via the `[style]="exp"` and `[class]="exp"` template
|
||||
* bindings. When styles are applied to the Element they will then be placed with respect to
|
||||
* bindings. When styles are applied to the element they will then be placed with respect to
|
||||
* any styles set with `elementStyleProp`. If any styles are set to `null` then they will be
|
||||
* removed from the element.
|
||||
* removed from the element. This instruction is also called for host bindings that write to
|
||||
* `[style]` and `[class]` (the directive param helps the instruction code determine where the
|
||||
* binding values come from).
|
||||
*
|
||||
* (Note that the styling instruction will not be applied until `elementStylingApply` is called.)
|
||||
*
|
||||
@@ -1593,29 +1653,33 @@ export function elementClassProp(
|
||||
export function elementStylingMap<T>(
|
||||
index: number, classes: {[key: string]: any} | string | NO_CHANGE | null,
|
||||
styles?: {[styleName: string]: any} | NO_CHANGE | null, directive?: {}): void {
|
||||
if (directive != undefined)
|
||||
return hackImplementationOfElementStylingMap(
|
||||
index, classes, styles, directive); // supported in next PR
|
||||
const lView = getLView();
|
||||
const tNode = getTNode(index, lView);
|
||||
const stylingContext = getStylingContext(index + HEADER_OFFSET, lView);
|
||||
if (hasClassInput(tNode) && classes !== NO_CHANGE) {
|
||||
const initialClasses = getInitialClassNameValue(stylingContext);
|
||||
const classInputVal =
|
||||
(initialClasses.length ? (initialClasses + ' ') : '') + (classes as string);
|
||||
setInputsForProperty(lView, tNode.inputs !['class'] !, classInputVal);
|
||||
} else {
|
||||
updateStylingMap(stylingContext, classes, styles);
|
||||
}
|
||||
}
|
||||
|
||||
/* START OF HACK BLOCK */
|
||||
function hackImplementationOfElementStylingMap<T>(
|
||||
index: number, classes: {[key: string]: any} | string | NO_CHANGE | null,
|
||||
styles?: {[styleName: string]: any} | NO_CHANGE | null, directive?: {}): void {
|
||||
throw new Error('unimplemented. Should not be needed by ViewEngine compatibility');
|
||||
// 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 (!directive) {
|
||||
if (hasClassInput(tNode) && classes !== NO_CHANGE) {
|
||||
const initialClasses = getInitialClassNameValue(stylingContext);
|
||||
const classInputVal =
|
||||
(initialClasses.length ? (initialClasses + ' ') : '') + forceClassesAsString(classes);
|
||||
setInputsForProperty(lView, tNode.inputs !['class'] !, classInputVal);
|
||||
classes = NO_CHANGE;
|
||||
}
|
||||
|
||||
if (hasStyleInput(tNode) && styles !== NO_CHANGE) {
|
||||
const initialStyles = getInitialClassNameValue(stylingContext);
|
||||
const styleInputVal =
|
||||
(initialStyles.length ? (initialStyles + ' ') : '') + forceStylesAsString(styles);
|
||||
setInputsForProperty(lView, tNode.inputs !['style'] !, styleInputVal);
|
||||
styles = NO_CHANGE;
|
||||
}
|
||||
}
|
||||
|
||||
updateStylingMap(stylingContext, classes, styles, directive);
|
||||
}
|
||||
/* END OF HACK BLOCK */
|
||||
|
||||
//////////////////////////
|
||||
//// Text
|
||||
|
||||
@@ -30,16 +30,19 @@ export const enum TNodeType {
|
||||
*/
|
||||
export const enum TNodeFlags {
|
||||
/** This bit is set if the node is a component */
|
||||
isComponent = 0b0001,
|
||||
isComponent = 0b00001,
|
||||
|
||||
/** This bit is set if the node has been projected */
|
||||
isProjected = 0b0010,
|
||||
isProjected = 0b00010,
|
||||
|
||||
/** This bit is set if any directive on this node has content queries */
|
||||
hasContentQuery = 0b0100,
|
||||
hasContentQuery = 0b00100,
|
||||
|
||||
/** This bit is set if the node has any directives that contain [class properties */
|
||||
hasClassInput = 0b1000,
|
||||
/** This bit is set if the node has any "class" inputs */
|
||||
hasClassInput = 0b01000,
|
||||
|
||||
/** This bit is set if the node has any "style" inputs */
|
||||
hasStyleInput = 0b10000,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -182,7 +185,7 @@ export interface TNode {
|
||||
propertyMetadataEndIndex: number;
|
||||
|
||||
/**
|
||||
* Stores if Node isComponent, isProjected, hasContentQuery and hasClassInput
|
||||
* Stores if Node isComponent, isProjected, hasContentQuery, hasClassInput and hasStyleInput
|
||||
*/
|
||||
flags: TNodeFlags;
|
||||
|
||||
@@ -345,6 +348,19 @@ export interface TNode {
|
||||
* projectable nodes during dynamic component creation.
|
||||
*/
|
||||
projection: (TNode|RNode[])[]|number|null;
|
||||
|
||||
/**
|
||||
* A buffer of functions that will be called once `elementEnd` (or `element`) completes.
|
||||
*
|
||||
* Due to the nature of how directives work in Angular, some directive code may
|
||||
* need to fire after any template-level code runs. If present, this array will
|
||||
* be flushed (each function will be invoked) once the associated element is
|
||||
* created.
|
||||
*
|
||||
* If an element is created multiple times then this function will be populated
|
||||
* with functions each time the creation block is called.
|
||||
*/
|
||||
onElementCreationFns: Function[]|null;
|
||||
}
|
||||
|
||||
/** Static data for an element */
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -26,17 +26,24 @@ export function createEmptyStylingContext(
|
||||
element?: RElement | null, sanitizer?: StyleSanitizeFn | null,
|
||||
initialStyles?: InitialStylingValues | null,
|
||||
initialClasses?: InitialStylingValues | null): StylingContext {
|
||||
return [
|
||||
0, // MasterFlags
|
||||
[null, -1, false, sanitizer || null], // DirectiveRefs
|
||||
initialStyles || [null], // InitialStyles
|
||||
initialClasses || [null], // InitialClasses
|
||||
[0, 0], // SinglePropOffsets
|
||||
element || null, // Element
|
||||
null, // PreviousMultiClassValue
|
||||
null, // PreviousMultiStyleValue
|
||||
null, // PlayerContext
|
||||
const context: StylingContext = [
|
||||
0, // MasterFlags
|
||||
[] as any, // DirectiveRefs (this gets filled below)
|
||||
initialStyles || [null, null], // InitialStyles
|
||||
initialClasses || [null, null], // InitialClasses
|
||||
[0, 0], // SinglePropOffsets
|
||||
element || null, // Element
|
||||
[0], // CachedMultiClassValue
|
||||
[0], // CachedMultiStyleValue
|
||||
null, // PlayerContext
|
||||
];
|
||||
allocateDirectiveIntoContext(context, null);
|
||||
return context;
|
||||
}
|
||||
|
||||
export function allocateDirectiveIntoContext(context: StylingContext, directiveRef: any | null) {
|
||||
// this is a new directive which we have not seen yet.
|
||||
context[StylingIndex.DirectiveRegistryPosition].push(directiveRef, -1, false, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,6 +110,34 @@ export function isAnimationProp(name: string): boolean {
|
||||
return name[0] === ANIMATION_PROP_PREFIX;
|
||||
}
|
||||
|
||||
export function hasClassInput(tNode: TNode) {
|
||||
return (tNode.flags & TNodeFlags.hasClassInput) !== 0;
|
||||
}
|
||||
|
||||
export function hasStyleInput(tNode: TNode) {
|
||||
return (tNode.flags & TNodeFlags.hasStyleInput) !== 0;
|
||||
}
|
||||
|
||||
export function forceClassesAsString(classes: string | {[key: string]: any} | null | undefined):
|
||||
string {
|
||||
if (classes && typeof classes !== 'string') {
|
||||
classes = Object.keys(classes).join(' ');
|
||||
}
|
||||
return (classes as string) || '';
|
||||
}
|
||||
|
||||
export function forceStylesAsString(styles: {[key: string]: any} | null | undefined): string {
|
||||
let str = '';
|
||||
if (styles) {
|
||||
const props = Object.keys(styles);
|
||||
for (let i = 0; i < props.length; i++) {
|
||||
const prop = props[i];
|
||||
str += (i ? ';' : '') + `${prop}:${styles[prop]}`;
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
export function addPlayerInternal(
|
||||
playerContext: PlayerContext, rootContext: RootContext, element: HTMLElement,
|
||||
player: Player | null, playerContextIndex: number, ref?: any): boolean {
|
||||
@@ -196,7 +231,3 @@ export function hasStyling(attrs: TAttributes): boolean {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function hasClassInput(tNode: TNode) {
|
||||
return tNode.flags & TNodeFlags.hasClassInput ? true : false;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import '@angular/core/test/bundling/util/src/reflect_metadata';
|
||||
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {Component, Directive, ElementRef, HostBinding, NgModule, ɵPlayState as PlayState, ɵPlayer as Player, ɵPlayerHandler as PlayerHandler, ɵaddPlayer as addPlayer, ɵbindPlayerFactory as bindPlayerFactory, ɵmarkDirty as markDirty, ɵrenderComponent as renderComponent} from '@angular/core';
|
||||
import {Component, Directive, ElementRef, HostBinding, HostListener, NgModule, ɵPlayState as PlayState, ɵPlayer as Player, ɵPlayerHandler as PlayerHandler, ɵaddPlayer as addPlayer, ɵbindPlayerFactory as bindPlayerFactory, ɵmarkDirty as markDirty, ɵrenderComponent as renderComponent} from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[make-color-grey]',
|
||||
@@ -33,6 +33,36 @@ class MakeColorGreyDirective {
|
||||
toggle() { this._backgroundColor ? this.off() : this.on(); }
|
||||
}
|
||||
|
||||
@Component({selector: 'box-with-overridden-styles', template: '...'})
|
||||
class BoxWithOverriddenStylesComponent {
|
||||
public active = false;
|
||||
|
||||
@HostBinding('style')
|
||||
styles = {};
|
||||
|
||||
constructor() { this.onInActive(); }
|
||||
|
||||
@HostListener('click', ['$event'])
|
||||
toggle() {
|
||||
if (this.active) {
|
||||
this.onInActive();
|
||||
} else {
|
||||
this.onActive();
|
||||
}
|
||||
markDirty(this);
|
||||
}
|
||||
|
||||
onActive() {
|
||||
this.active = true;
|
||||
this.styles = {height: '500px', 'font-size': '200px', background: 'red'};
|
||||
}
|
||||
|
||||
onInActive() {
|
||||
this.active = false;
|
||||
this.styles = {width: '200px', height: '500px', border: '10px solid black', background: 'grey'};
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'animation-world',
|
||||
template: `
|
||||
@@ -48,7 +78,7 @@ class MakeColorGreyDirective {
|
||||
class="record"
|
||||
[style.transform]="item.active ? 'scale(1.5)' : 'none'"
|
||||
[class]="makeClass(item)"
|
||||
style="border-radius: 10px"
|
||||
style="border-radius: 10px"
|
||||
[style]="styles"
|
||||
[style.color]="item.value == 4 ? 'red' : null"
|
||||
[style.background-color]="item.value == 4 ? 'white' : null"
|
||||
@@ -56,6 +86,13 @@ class MakeColorGreyDirective {
|
||||
{{ item.value }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<box-with-overridden-styles
|
||||
style="display:block"
|
||||
[style]="{'border-radius':'50px', 'border': '50px solid teal'}" [ngStyle]="{transform:'rotate(50deg)'}">
|
||||
</box-with-overridden-styles>
|
||||
`,
|
||||
})
|
||||
class AnimationWorldComponent {
|
||||
@@ -93,8 +130,10 @@ class AnimationWorldComponent {
|
||||
}
|
||||
}
|
||||
|
||||
@NgModule(
|
||||
{declarations: [AnimationWorldComponent, MakeColorGreyDirective], imports: [CommonModule]})
|
||||
@NgModule({
|
||||
declarations: [AnimationWorldComponent, MakeColorGreyDirective, BoxWithOverriddenStylesComponent],
|
||||
imports: [CommonModule]
|
||||
})
|
||||
class AnimationWorldModule {
|
||||
}
|
||||
|
||||
|
||||
@@ -170,6 +170,9 @@
|
||||
{
|
||||
"name": "allocStylingContext"
|
||||
},
|
||||
{
|
||||
"name": "allocateDirectiveIntoContext"
|
||||
},
|
||||
{
|
||||
"name": "appendChild"
|
||||
},
|
||||
@@ -335,6 +338,9 @@
|
||||
{
|
||||
"name": "getInitialClassNameValue"
|
||||
},
|
||||
{
|
||||
"name": "getInitialStyleStringValue"
|
||||
},
|
||||
{
|
||||
"name": "getInjectorIndex"
|
||||
},
|
||||
@@ -407,6 +413,9 @@
|
||||
{
|
||||
"name": "hasParentInjector"
|
||||
},
|
||||
{
|
||||
"name": "hasStyleInput"
|
||||
},
|
||||
{
|
||||
"name": "hasStyling"
|
||||
},
|
||||
@@ -555,7 +564,10 @@
|
||||
"name": "renderEmbeddedTemplate"
|
||||
},
|
||||
{
|
||||
"name": "renderInitialStylesAndClasses"
|
||||
"name": "renderInitialClasses"
|
||||
},
|
||||
{
|
||||
"name": "renderInitialStyles"
|
||||
},
|
||||
{
|
||||
"name": "renderInitialStylingValues"
|
||||
|
||||
@@ -44,9 +44,6 @@
|
||||
{
|
||||
"name": "HOST"
|
||||
},
|
||||
{
|
||||
"name": "T_HOST"
|
||||
},
|
||||
{
|
||||
"name": "INJECTOR"
|
||||
},
|
||||
@@ -107,6 +104,9 @@
|
||||
{
|
||||
"name": "TVIEW"
|
||||
},
|
||||
{
|
||||
"name": "T_HOST"
|
||||
},
|
||||
{
|
||||
"name": "UnsubscriptionErrorImpl"
|
||||
},
|
||||
|
||||
@@ -362,9 +362,6 @@
|
||||
{
|
||||
"name": "_symbolIterator"
|
||||
},
|
||||
{
|
||||
"name": "_updateSingleStylingValue"
|
||||
},
|
||||
{
|
||||
"name": "addComponentLogic"
|
||||
},
|
||||
@@ -386,6 +383,9 @@
|
||||
{
|
||||
"name": "allocStylingContext"
|
||||
},
|
||||
{
|
||||
"name": "allocateDirectiveIntoContext"
|
||||
},
|
||||
{
|
||||
"name": "allowValueChange"
|
||||
},
|
||||
@@ -419,6 +419,9 @@
|
||||
{
|
||||
"name": "bloomHashBitOrFactory"
|
||||
},
|
||||
{
|
||||
"name": "booleanOrNull"
|
||||
},
|
||||
{
|
||||
"name": "cacheMatchingLocalNames"
|
||||
},
|
||||
@@ -689,6 +692,9 @@
|
||||
{
|
||||
"name": "getInitialIndex"
|
||||
},
|
||||
{
|
||||
"name": "getInitialStyleStringValue"
|
||||
},
|
||||
{
|
||||
"name": "getInitialStylingValuesIndexOf"
|
||||
},
|
||||
@@ -720,7 +726,7 @@
|
||||
"name": "getMultiOrSingleIndex"
|
||||
},
|
||||
{
|
||||
"name": "getMultiStartIndex"
|
||||
"name": "getMultiStylesStartIndex"
|
||||
},
|
||||
{
|
||||
"name": "getNativeAnchorNode"
|
||||
@@ -839,6 +845,9 @@
|
||||
{
|
||||
"name": "hasPlayerBuilderChanged"
|
||||
},
|
||||
{
|
||||
"name": "hasStyleInput"
|
||||
},
|
||||
{
|
||||
"name": "hasStyling"
|
||||
},
|
||||
@@ -848,12 +857,21 @@
|
||||
{
|
||||
"name": "hasValueChanged"
|
||||
},
|
||||
{
|
||||
"name": "hyphenate"
|
||||
},
|
||||
{
|
||||
"name": "hyphenateEntries"
|
||||
},
|
||||
{
|
||||
"name": "includeViewProviders"
|
||||
},
|
||||
{
|
||||
"name": "increaseElementDepthCount"
|
||||
},
|
||||
{
|
||||
"name": "initElementStyling"
|
||||
},
|
||||
{
|
||||
"name": "initNodeFlags"
|
||||
},
|
||||
@@ -965,9 +983,6 @@
|
||||
{
|
||||
"name": "leaveView"
|
||||
},
|
||||
{
|
||||
"name": "limitToSingleClasses"
|
||||
},
|
||||
{
|
||||
"name": "listener"
|
||||
},
|
||||
@@ -1104,7 +1119,10 @@
|
||||
"name": "renderEmbeddedTemplate"
|
||||
},
|
||||
{
|
||||
"name": "renderInitialStylesAndClasses"
|
||||
"name": "renderInitialClasses"
|
||||
},
|
||||
{
|
||||
"name": "renderInitialStyles"
|
||||
},
|
||||
{
|
||||
"name": "renderInitialStylingValues"
|
||||
@@ -1256,6 +1274,9 @@
|
||||
{
|
||||
"name": "updateContextWithBindings"
|
||||
},
|
||||
{
|
||||
"name": "updateSingleStylingValue"
|
||||
},
|
||||
{
|
||||
"name": "valueExists"
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user