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:
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 */
|
||||
|
|
|
@ -30,7 +30,7 @@ import {PlayerContext} from './player';
|
|||
*
|
||||
* Say for example we have this:
|
||||
* ```
|
||||
* <!-- when myWidthExp=null then a width of "100px"
|
||||
* <!-- when `myWidthExp=null` then a width of `100px`
|
||||
* will be used a default value for width -->
|
||||
* <div style="width:100px" [style.width]="myWidthExp"></div>
|
||||
* ```
|
||||
|
@ -44,9 +44,9 @@ import {PlayerContext} from './player';
|
|||
* 1. elementStart or element (within the template function of a component)
|
||||
* 2. elementHostAttrs (for directive host bindings)
|
||||
*
|
||||
* In either case, a styling context will be created and stored within an element's LViewData. Once
|
||||
* the styling context is created then single and multi properties can stored within it. For this to
|
||||
* happen, the following function needs to be called:
|
||||
* In either case, a styling context will be created and stored within an element's `LViewData`.
|
||||
* Once the styling context is created then single and multi properties can be stored within it.
|
||||
* For this to happen, the following function needs to be called:
|
||||
*
|
||||
* `elementStyling` (called with style properties, class properties and a sanitizer + a directive
|
||||
* instance).
|
||||
|
@ -73,8 +73,8 @@ import {PlayerContext} from './player';
|
|||
*
|
||||
* The context generated from these values will look like this (note that
|
||||
* for each binding name (the class and style bindings) the values will
|
||||
* be inserted twice into the array (once for single property entries) and
|
||||
* another for multi property entries).
|
||||
* be inserted twice into the array (once for single property entries and
|
||||
* again for multi property entries).
|
||||
*
|
||||
* context = [
|
||||
* // 0-8: header values (about 8 entries of configuration data)
|
||||
|
@ -147,9 +147,10 @@ import {PlayerContext} from './player';
|
|||
* have changed.
|
||||
*
|
||||
* ## Directives
|
||||
* Directives style values (which are provided through host bindings) are also supported and
|
||||
* housed within the same styling context as are template-level style/class properties/bindings.
|
||||
* Both directive-level and template-level styling bindings share the same context.
|
||||
* Directive style/class values (which are provided through host bindings) are also supported and
|
||||
* housed within the same styling context as are template-level style/class properties/bindings
|
||||
* So long as they are all assigned to the same element, both directive-level and template-level
|
||||
* styling bindings share the same context.
|
||||
*
|
||||
* Each of the following instructions supports accepting a directive instance as an input parameter:
|
||||
*
|
||||
|
@ -160,22 +161,40 @@ import {PlayerContext} from './player';
|
|||
* - `elementStylingMap`
|
||||
* - `elementStylingApply`
|
||||
*
|
||||
* Each time a directiveRef is passed in, it will be converted into an index by examining the
|
||||
* Each time a directive value is passed in, it will be converted into an index by examining the
|
||||
* directive registry (which lives in the context configuration area). The index is then used
|
||||
* to help single style properties figure out where a value is located in the context.
|
||||
*
|
||||
*
|
||||
* ## Single-level styling bindings (`[style.prop]` and `[class.name]`)
|
||||
*
|
||||
* Both `[style.prop]` and `[class.name]` bindings are run through the `updateStyleProp`
|
||||
* and `updateClassProp` functions respectively. They work by examining the provided
|
||||
* `offset` value and are able to locate the exact spot in the context where the
|
||||
* matching style is located.
|
||||
*
|
||||
* Both `[style.prop]` and `[class.name]` bindings are able to process these values
|
||||
* from directive host bindings. When evaluated (from the host binding function) the
|
||||
* `directiveRef` value is then passed in.
|
||||
*
|
||||
* If two directives or a directive + a template binding both write to the same style/class
|
||||
* binding then the styling context code will decide which one wins based on the following
|
||||
* rule:
|
||||
*
|
||||
* 1. If the template binding has a value then it always wins
|
||||
* 2. If not then whichever first-registered directive that has that value first will win
|
||||
* 2. Otherwise whichever first-registered directive that has that value first will win
|
||||
*
|
||||
* The code example helps make this clear:
|
||||
*
|
||||
* ```
|
||||
* <div [style.width]="myWidth" [my-width-directive]="'600px">
|
||||
* @Directive({ selector: '[my-width-directive' ]})
|
||||
* <!--
|
||||
* <div [style.width]="myWidth"
|
||||
* [my-width-directive]="'600px'">
|
||||
* -->
|
||||
*
|
||||
* @Directive({
|
||||
* selector: '[my-width-directive']
|
||||
* })
|
||||
* class MyWidthDirective {
|
||||
* @Input('my-width-directive')
|
||||
* @HostBinding('style.width')
|
||||
|
@ -187,7 +206,8 @@ import {PlayerContext} from './player';
|
|||
* it will always win over the width binding that is present as a host binding within
|
||||
* the `MyWidthDirective`. However, if `[style.width]` renders as `null` (so `myWidth=null`)
|
||||
* then the `MyWidthDirective` will be able to write to the `width` style within the context.
|
||||
* Simply put, whichever directive writes to a value ends up having ownership of it.
|
||||
* Simply put, whichever directive writes to a value first ends up having ownership of it as
|
||||
* long as the template didn't set anything.
|
||||
*
|
||||
* The way in which the ownership is facilitated is through index value. The earliest directives
|
||||
* get the smallest index values (with 0 being reserved for the template element bindings). Each
|
||||
|
@ -195,10 +215,48 @@ import {PlayerContext} from './player';
|
|||
* assigned the directive index value in its data. If another directive writes a value again then
|
||||
* its directive index gets compared against the directive index that exists on the element. Only
|
||||
* when the new value's directive index is less than the existing directive index then the new
|
||||
* value will be written to the context.
|
||||
* value will be written to the context. But, if the existing value is null then the new value is
|
||||
* written by the less important directive.
|
||||
*
|
||||
* Each directive also has its own sanitizer and dirty flags. These values are consumed within the
|
||||
* rendering function.
|
||||
*
|
||||
*
|
||||
* ## Multi-level styling bindings (`[style]` and `[class]`)
|
||||
*
|
||||
* Multi-level styling bindings are treated as less important (less specific) as single-level
|
||||
* bindings (things like `[style.prop]` and `[class.name]`).
|
||||
*
|
||||
* Multi-level bindings are still applied to the context in a similar way as are single level
|
||||
* bindings, but this process works by diffing the new multi-level values (which are key/value
|
||||
* maps) against the existing set of styles that live in the context. Each time a new map value
|
||||
* is detected (via identity check) then it will loop through the values and figure out what
|
||||
* has changed and reorder the context array to match the ordering of the keys. This reordering
|
||||
* of the context makes sure that follow-up traversals of the context when updated against the
|
||||
* key/value map are as close as possible to o(n) (where "n" is the size of the key/value map).
|
||||
*
|
||||
* If a `directiveRef` value is passed in then the styling algorithm code will take the directive's
|
||||
* prioritization index into account and update the values with respect to more important
|
||||
* directives. This means that if a value such as `width` is updated in two different `[style]`
|
||||
* bindings (say one on the template and another within a directive that sits on the same element)
|
||||
* then the algorithm will decide how to update the value based on the following heuristic:
|
||||
*
|
||||
* 1. If the template binding has a value then it always wins
|
||||
* 2. If not then whichever first-registered directive that has that value first will win
|
||||
*
|
||||
* It will also update the value if it was set to `null` by a previous directive (or the template).
|
||||
*
|
||||
* Each time a value is updated (or removed) then the context will change shape to better match
|
||||
* the ordering of the styling data as well as the ordering of each directive that contains styling
|
||||
* data. (See `patchStylingMapIntoContext` inside of class_and_style_bindings.ts to better
|
||||
* understand how this works.)
|
||||
*
|
||||
* ## Rendering
|
||||
* The rendering mechanism (when the styling data is applied on screen) occurs via the
|
||||
* `elementStylingApply` function and is designed to run after **all** styling functions have been
|
||||
* evaluated. The rendering algorithm will loop over the context and only apply the styles that are
|
||||
* flagged as dirty (either because they are new, updated or have been removed via multi or
|
||||
* single bindings).
|
||||
*/
|
||||
export interface StylingContext extends
|
||||
Array<{[key: string]: any}|number|string|boolean|RElement|StyleSanitizeFn|PlayerContext|null> {
|
||||
|
@ -240,13 +298,13 @@ export interface StylingContext extends
|
|||
* The last class value that was interpreted by elementStylingMap. This is cached
|
||||
* So that the algorithm can exit early incase the value has not changed.
|
||||
*/
|
||||
[StylingIndex.CachedClassValueOrInitialClassString]: {[key: string]: any}|string|(string)[]|null;
|
||||
[StylingIndex.CachedMultiClasses]: any|MapBasedOffsetValues;
|
||||
|
||||
/**
|
||||
* The last style value that was interpreted by elementStylingMap. This is cached
|
||||
* So that the algorithm can exit early incase the value has not changed.
|
||||
*/
|
||||
[StylingIndex.CachedStyleValue]: {[key: string]: any}|(string)[]|null;
|
||||
[StylingIndex.CachedMultiStyles]: any|MapBasedOffsetValues;
|
||||
|
||||
/**
|
||||
* Location of animation context (which contains the active players) for this element styling
|
||||
|
@ -262,7 +320,10 @@ export interface StylingContext extends
|
|||
*
|
||||
* See [InitialStylingValuesIndex] for a breakdown of how all this works.
|
||||
*/
|
||||
export interface InitialStylingValues extends Array<string|boolean|null> { [0]: null; }
|
||||
export interface InitialStylingValues extends Array<string|boolean|null> {
|
||||
[InitialStylingValuesIndex.DefaultNullValuePosition]: null;
|
||||
[InitialStylingValuesIndex.InitialClassesStringPosition]: string|null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used as an offset/position index to figure out where initial styling
|
||||
|
@ -270,13 +331,16 @@ export interface InitialStylingValues extends Array<string|boolean|null> { [0]:
|
|||
*
|
||||
* Used as a reference point to provide markers to all static styling
|
||||
* values (the initial style and class values on an element) within an
|
||||
* array within the StylingContext. This array contains key/value pairs
|
||||
* array within the `StylingContext`. This array contains key/value pairs
|
||||
* where the key is the style property name or className and the value is
|
||||
* the style value or whether or not a class is present on the elment.
|
||||
*
|
||||
* The first value is also always null so that a initial index value of
|
||||
* The first value is always null so that a initial index value of
|
||||
* `0` will always point to a null value.
|
||||
*
|
||||
* The second value is also always null unless a string-based representation
|
||||
* of the styling data was constructed (it gets cached in this slot).
|
||||
*
|
||||
* If a <div> elements contains a list of static styling values like so:
|
||||
*
|
||||
* <div class="foo bar baz" style="width:100px; height:200px;">
|
||||
|
@ -284,14 +348,18 @@ export interface InitialStylingValues extends Array<string|boolean|null> { [0]:
|
|||
* Then the initial styles for that will look like so:
|
||||
*
|
||||
* Styles:
|
||||
* ```
|
||||
* StylingContext[InitialStylesIndex] = [
|
||||
* null, 'width', '100px', height, '200px'
|
||||
* null, null, 'width', '100px', height, '200px'
|
||||
* ]
|
||||
* ```
|
||||
*
|
||||
* Classes:
|
||||
* StylingContext[InitialStylesIndex] = [
|
||||
* null, 'foo', true, 'bar', true, 'baz', true
|
||||
* ```
|
||||
* StylingContext[InitialClassesIndex] = [
|
||||
* null, null, 'foo', true, 'bar', true, 'baz', true
|
||||
* ]
|
||||
* ```
|
||||
*
|
||||
* Initial style and class entries have their own arrays. This is because
|
||||
* it's easier to add to the end of one array and not then have to update
|
||||
|
@ -300,26 +368,32 @@ export interface InitialStylingValues extends Array<string|boolean|null> { [0]:
|
|||
* When property bindinds are added to a context then initial style/class
|
||||
* values will also be inserted into the array. This is to create a space
|
||||
* in the situation when a follow-up directive inserts static styling into
|
||||
* the array. By default style values are `null` and class values are
|
||||
* the array. By default, style values are `null` and class values are
|
||||
* `false` when inserted by property bindings.
|
||||
*
|
||||
* For example:
|
||||
* ```
|
||||
* <div class="foo bar baz"
|
||||
* [class.car]="myCarExp"
|
||||
* style="width:100px; height:200px;"
|
||||
* [style.opacity]="myOpacityExp">
|
||||
* ```
|
||||
*
|
||||
* Will construct initial styling values that look like:
|
||||
*
|
||||
* Styles:
|
||||
* ```
|
||||
* StylingContext[InitialStylesIndex] = [
|
||||
* null, 'width', '100px', height, '200px', 'opacity', null
|
||||
* null, null, 'width', '100px', height, '200px', 'opacity', null
|
||||
* ]
|
||||
* ```
|
||||
*
|
||||
* Classes:
|
||||
* StylingContext[InitialStylesIndex] = [
|
||||
* null, 'foo', true, 'bar', true, 'baz', true, 'car', false
|
||||
* ```
|
||||
* StylingContext[InitialClassesIndex] = [
|
||||
* null, null, 'foo', true, 'bar', true, 'baz', true, 'car', false
|
||||
* ]
|
||||
* ```
|
||||
*
|
||||
* Now if a directive comes along and introduces `car` as a static
|
||||
* class value or `opacity` then those values will be filled into
|
||||
|
@ -327,6 +401,7 @@ export interface InitialStylingValues extends Array<string|boolean|null> { [0]:
|
|||
*
|
||||
* For example:
|
||||
*
|
||||
* ```
|
||||
* @Directive({
|
||||
* selector: 'opacity-car-directive',
|
||||
* host: {
|
||||
|
@ -335,21 +410,28 @@ export interface InitialStylingValues extends Array<string|boolean|null> { [0]:
|
|||
* }
|
||||
* })
|
||||
* class OpacityCarDirective {}
|
||||
* ```
|
||||
*
|
||||
* This will render itself as:
|
||||
*
|
||||
* Styles:
|
||||
* ```
|
||||
* StylingContext[InitialStylesIndex] = [
|
||||
* null, 'width', '100px', height, '200px', 'opacity', null
|
||||
* null, null, 'width', '100px', height, '200px', 'opacity', '0.5'
|
||||
* ]
|
||||
* ```
|
||||
*
|
||||
* Classes:
|
||||
* StylingContext[InitialStylesIndex] = [
|
||||
* null, 'foo', true, 'bar', true, 'baz', true, 'car', false
|
||||
* ```
|
||||
* StylingContext[InitialClassesIndex] = [
|
||||
* null, null, 'foo', true, 'bar', true, 'baz', true, 'car', true
|
||||
* ]
|
||||
* ```
|
||||
*/
|
||||
export const enum InitialStylingValuesIndex {
|
||||
KeyValueStartPosition = 1,
|
||||
DefaultNullValuePosition = 0,
|
||||
InitialClassesStringPosition = 1,
|
||||
KeyValueStartPosition = 2,
|
||||
PropOffset = 0,
|
||||
ValueOffset = 1,
|
||||
Size = 2
|
||||
|
@ -361,28 +443,27 @@ export const enum InitialStylingValuesIndex {
|
|||
*
|
||||
* Each entry in this array represents a source of where style/class binding values could
|
||||
* come from. By default, there is always at least one directive here with a null value and
|
||||
* that represents bindings that live directly on an element (not host bindings).
|
||||
* that represents bindings that live directly on an element in the template (not host bindings).
|
||||
*
|
||||
* Each successive entry in the array is an actual instance of an array as well as some
|
||||
* additional info.
|
||||
* Each successive entry in the array is an actual instance of a directive as well as some
|
||||
* additional info about that entry.
|
||||
*
|
||||
* An entry within this array has the following values:
|
||||
* [0] = The instance of the directive (or null when it is not a directive, but a template binding
|
||||
* source)
|
||||
* [0] = The instance of the directive (the first entry is null because its reserved for the
|
||||
* template)
|
||||
* [1] = The pointer that tells where the single styling (stuff like [class.foo] and [style.prop])
|
||||
* offset values are located. This value will allow for a binding instruction to find exactly
|
||||
* where a style is located.
|
||||
* [2] = Whether or not the directive has any styling values that are dirty. This is used as
|
||||
* reference within the renderClassAndStyleBindings function to decide whether to skip
|
||||
* iterating through the context when rendering is executed.
|
||||
* reference within the `renderStyling` function to decide whether to skip iterating
|
||||
* through the context when rendering is executed.
|
||||
* [3] = The styleSanitizer instance that is assigned to the directive. Although it's unlikely,
|
||||
* a directive could introduce its own special style sanitizer and for this reach each
|
||||
* directive will get its own space for it (if null then the very first sanitizer is used).
|
||||
*
|
||||
* Each time a new directive is added it will insert these four values at the end of the array.
|
||||
* When this array is examined (using indexOf) then the resulting directiveIndex will be resolved
|
||||
* by dividing the index value by the size of the array entries (so if DirA is at spot 8 then its
|
||||
* index will be 2).
|
||||
* When this array is examined then the resulting directiveIndex will be resolved by dividing the
|
||||
* index value by the size of the array entries (so if DirA is at spot 8 then its index will be 2).
|
||||
*/
|
||||
export interface DirectiveRegistryValues extends Array<null|{}|boolean|number|StyleSanitizeFn> {
|
||||
[DirectiveRegistryValuesIndex.DirectiveValueOffset]: null;
|
||||
|
@ -441,29 +522,94 @@ export const enum SinglePropOffsetValuesIndex {
|
|||
ValueStartPosition = 2
|
||||
}
|
||||
|
||||
/**
|
||||
* Used a reference for all multi styling values (values that are assigned via the
|
||||
* `[style]` and `[class]` bindings).
|
||||
*
|
||||
* Single-styling properties (things set via `[style.prop]` and `[class.name]` bindings)
|
||||
* are not handled using the same approach as multi-styling bindings (such as `[style]`
|
||||
* `[class]` bindings).
|
||||
*
|
||||
* Multi-styling bindings rely on a diffing algorithm to figure out what properties have been added,
|
||||
* removed and modified. Multi-styling properties are also evaluated across directives--which means
|
||||
* that Angular supports having multiple directives all write to the same `[style]` and `[class]`
|
||||
* bindings (using host bindings) even if the `[style]` and/or `[class]` bindings are being written
|
||||
* to on the template element.
|
||||
*
|
||||
* All multi-styling values that are written to an element (whether it be from the template or any
|
||||
* directives attached to the element) are all written into the `MapBasedOffsetValues` array. (Note
|
||||
* that there are two arrays: one for styles and another for classes.)
|
||||
*
|
||||
* This array is shaped in the following way:
|
||||
*
|
||||
* [0] = The total amount of unique multi-style or multi-class entries that exist currently in the
|
||||
* context.
|
||||
* [1+] = Contains an entry of four values ... Each entry is a value assigned by a
|
||||
* `[style]`/`[class]`
|
||||
* binding (we call this a **source**).
|
||||
*
|
||||
* An example entry looks like so (at a given `i` index):
|
||||
* [i + 0] = Whether or not the value is dirty
|
||||
*
|
||||
* [i + 1] = The index of where the map-based values
|
||||
* (for this **source**) start within the context
|
||||
*
|
||||
* [i + 2] = The untouched, last set value of the binding
|
||||
*
|
||||
* [i + 3] = The total amount of unqiue binding values that were
|
||||
* extracted and set into the context. (Note that this value does
|
||||
* not reflect the total amount of values within the binding
|
||||
* value (since it's a map), but instead reflects the total values
|
||||
* that were not used by another directive).
|
||||
*
|
||||
* Each time a directive (or template) writes a value to a `[class]`/`[style]` binding then the
|
||||
* styling diffing algorithm code will decide whether or not to update the value based on the
|
||||
* following rules:
|
||||
*
|
||||
* 1. If a more important directive (either the template or a directive that was registered
|
||||
* beforehand) has written a specific styling value into the context then any follow-up styling
|
||||
* values (set by another directive via its `[style]` and/or `[class]` host binding) will not be
|
||||
* able to set it. This is because the former directive has priorty.
|
||||
* 2. Only if a former directive has set a specific styling value to null (whether by actually
|
||||
* setting it to null or not including it in is map value) then a less imporatant directive can
|
||||
* set its own value.
|
||||
*
|
||||
* ## How the map-based styling algorithm updates itself
|
||||
*/
|
||||
export interface MapBasedOffsetValues extends Array<any> {
|
||||
[MapBasedOffsetValuesIndex.EntriesCountPosition]: number;
|
||||
}
|
||||
|
||||
export const enum MapBasedOffsetValuesIndex {
|
||||
EntriesCountPosition = 0,
|
||||
ValuesStartPosition = 1,
|
||||
DirtyFlagOffset = 0,
|
||||
PositionStartOffset = 1,
|
||||
ValueOffset = 2,
|
||||
ValueCountOffset = 3,
|
||||
Size = 4
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to set the context to be dirty or not both on the master flag (position 1)
|
||||
* or for each single/multi property that exists in the context.
|
||||
*/
|
||||
export const enum StylingFlags {
|
||||
// Implies no configurations
|
||||
None = 0b000000,
|
||||
None = 0b00000,
|
||||
// Whether or not the entry or context itself is dirty
|
||||
Dirty = 0b000001,
|
||||
Dirty = 0b00001,
|
||||
// Whether or not this is a class-based assignment
|
||||
Class = 0b000010,
|
||||
Class = 0b00010,
|
||||
// Whether or not a sanitizer was applied to this property
|
||||
Sanitize = 0b000100,
|
||||
Sanitize = 0b00100,
|
||||
// Whether or not any player builders within need to produce new players
|
||||
PlayerBuildersDirty = 0b001000,
|
||||
// If NgClass is present (or some other class handler) then it will handle the map expressions and
|
||||
// initial classes
|
||||
OnlyProcessSingleClasses = 0b010000,
|
||||
PlayerBuildersDirty = 0b01000,
|
||||
// The max amount of bits used to represent these configuration values
|
||||
BindingAllocationLocked = 0b100000,
|
||||
BitCountSize = 6,
|
||||
// There are only six bits here
|
||||
BitMask = 0b111111
|
||||
BindingAllocationLocked = 0b10000,
|
||||
BitCountSize = 5,
|
||||
// There are only five bits here
|
||||
BitMask = 0b11111
|
||||
}
|
||||
|
||||
/** Used as numeric pointer values to determine what cells to update in the `StylingContext` */
|
||||
|
@ -482,9 +628,9 @@ export const enum StylingIndex {
|
|||
ElementPosition = 5,
|
||||
// Position of where the last string-based CSS class value was stored (or a cached version of the
|
||||
// initial styles when a [class] directive is present)
|
||||
CachedClassValueOrInitialClassString = 6,
|
||||
CachedMultiClasses = 6,
|
||||
// Position of where the last string-based CSS class value was stored
|
||||
CachedStyleValue = 7,
|
||||
CachedMultiStyles = 7,
|
||||
// Multi and single entries are stored in `StylingContext` as: Flag; PropertyName; PropertyValue
|
||||
// Position of where the initial styles are stored in the styling context
|
||||
PlayerContext = 8,
|
||||
|
|
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
Loading…
Reference in New Issue