feat(ivy): properly apply class="", [class], [class.foo] and [attr.class] bindings (#24822)

PR Close #24822
This commit is contained in:
Matias Niemelä 2018-07-11 09:56:47 -07:00
parent c8ad9657c9
commit ba3eb8b654
24 changed files with 1806 additions and 928 deletions

View File

@ -48,13 +48,13 @@ export class LargeTableComponent {
{ {
if (rf2 & RenderFlags.Create) { if (rf2 & RenderFlags.Create) {
E(0, 'td'); E(0, 'td');
s(1, c0); s(c0);
{ T(2); } { T(1); }
e(); e();
} }
if (rf2 & RenderFlags.Update) { if (rf2 & RenderFlags.Update) {
sp(1, 0, cell.row % 2 ? '' : 'grey'); sp(0, 0, cell.row % 2 ? '' : 'grey');
t(2, b(cell.value)); t(1, b(cell.value));
} }
} }
v(); v();

View File

@ -41,16 +41,16 @@ export class TreeComponent {
template: function(rf: RenderFlags, ctx: TreeComponent) { template: function(rf: RenderFlags, ctx: TreeComponent) {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {
E(0, 'span'); E(0, 'span');
s(1, c0); s(c0);
{ T(2); } { T(1); }
e(); e();
C(2);
C(3); C(3);
C(4);
} }
if (rf & RenderFlags.Update) { if (rf & RenderFlags.Update) {
sp(1, 0, ctx.data.depth % 2 ? '' : 'grey'); sp(0, 0, ctx.data.depth % 2 ? '' : 'grey');
t(2, i1(' ', ctx.data.value, ' ')); t(1, i1(' ', ctx.data.value, ' '));
cR(3); cR(2);
{ {
if (ctx.data.left != null) { if (ctx.data.left != null) {
let rf0 = V(0); let rf0 = V(0);
@ -67,7 +67,7 @@ export class TreeComponent {
} }
} }
cr(); cr();
cR(4); cR(3);
{ {
if (ctx.data.right != null) { if (ctx.data.right != null) {
let rf0 = V(0); let rf0 = V(0);
@ -114,18 +114,18 @@ export function TreeTpl(rf: RenderFlags, ctx: TreeNode) {
E(0, 'tree'); E(0, 'tree');
{ {
E(1, 'span'); E(1, 'span');
s(2, c1); s(c1);
{ T(3); } { T(2); }
e(); e();
C(3);
C(4); C(4);
C(5);
} }
e(); e();
} }
if (rf & RenderFlags.Update) { if (rf & RenderFlags.Update) {
sp(2, 0, ctx.depth % 2 ? '' : 'grey'); sp(1, 0, ctx.depth % 2 ? '' : 'grey');
t(3, i1(' ', ctx.value, ' ')); t(2, i1(' ', ctx.value, ' '));
cR(4); cR(3);
{ {
if (ctx.left != null) { if (ctx.left != null) {
let rf0 = V(0); let rf0 = V(0);
@ -134,7 +134,7 @@ export function TreeTpl(rf: RenderFlags, ctx: TreeNode) {
} }
} }
cr(); cr();
cR(5); cR(4);
{ {
if (ctx.right != null) { if (ctx.right != null) {
let rf0 = V(0); let rf0 = V(0);

View File

@ -380,8 +380,6 @@ export const enum RenderFlags {
Update = 0b10 Update = 0b10
} }
// Note this will expand once `class` is introduced to styling
export const enum InitialStylingFlags { export const enum InitialStylingFlags {
/** Mode for matching initial style values */ VALUES_MODE = 0b1,
INITIAL_STYLES = 0b00,
} }

View File

@ -33,13 +33,11 @@ export class Identifiers {
static elementAttribute: o.ExternalReference = {name: 'ɵa', moduleName: CORE}; static elementAttribute: o.ExternalReference = {name: 'ɵa', moduleName: CORE};
static elementClass: o.ExternalReference = {name: 'ɵk', moduleName: CORE}; static elementClassProp: o.ExternalReference = {name: 'ɵcp', moduleName: CORE};
static elementClassNamed: o.ExternalReference = {name: 'ɵkn', moduleName: CORE};
static elementStyling: o.ExternalReference = {name: 'ɵs', moduleName: CORE}; static elementStyling: o.ExternalReference = {name: 'ɵs', moduleName: CORE};
static elementStyle: o.ExternalReference = {name: 'ɵsm', moduleName: CORE}; static elementStylingMap: o.ExternalReference = {name: 'ɵsm', moduleName: CORE};
static elementStyleProp: o.ExternalReference = {name: 'ɵsp', moduleName: CORE}; static elementStyleProp: o.ExternalReference = {name: 'ɵsp', moduleName: CORE};

View File

@ -40,19 +40,12 @@ function mapBindingToInstruction(type: BindingType): o.ExternalReference|undefin
case BindingType.Attribute: case BindingType.Attribute:
return R3.elementAttribute; return R3.elementAttribute;
case BindingType.Class: case BindingType.Class:
return R3.elementClassNamed; return R3.elementClassProp;
default: default:
return undefined; return undefined;
} }
} }
// `className` is used below instead of `class` because the interception
// code (where this map is used) deals with DOM element property values
// (like elm.propName) and not component bindining properties (like [propName]).
const SPECIAL_CASED_PROPERTIES_INSTRUCTION_MAP: {[index: string]: o.ExternalReference} = {
'className': R3.elementClass
};
export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver { export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver {
private _dataIndex = 0; private _dataIndex = 0;
private _bindingContext = 0; private _bindingContext = 0;
@ -310,33 +303,59 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
const i18nMessages: o.Statement[] = []; const i18nMessages: o.Statement[] = [];
const attributes: o.Expression[] = []; const attributes: o.Expression[] = [];
const initialStyleDeclarations: o.Expression[] = []; const initialStyleDeclarations: o.Expression[] = [];
const initialClassDeclarations: o.Expression[] = [];
const styleInputs: t.BoundAttribute[] = []; const styleInputs: t.BoundAttribute[] = [];
const classInputs: t.BoundAttribute[] = [];
const allOtherInputs: t.BoundAttribute[] = []; const allOtherInputs: t.BoundAttribute[] = [];
element.inputs.forEach((input: t.BoundAttribute) => { element.inputs.forEach((input: t.BoundAttribute) => {
// [attr.style] should not be treated as a styling-based switch (input.type) {
// binding since it is intended to write directly to the attr // [attr.style] or [attr.class] should not be treated as styling-based
// and therefore will skip all style resolution that is present // bindings since they are intended to be written directly to the attr
// with style="", [style]="" and [style.prop]="" assignments // and therefore will skip all style/class resolution that is present
if (input.name == 'style' && input.type == BindingType.Property) { // with style="", [style]="" and [style.prop]="", class="",
// this should always go first in the compilation (for [style]) // [class.prop]="". [class]="" assignments
styleInputs.splice(0, 0, input); case BindingType.Property:
} else if (input.type == BindingType.Style) { if (input.name == 'style') {
styleInputs.push(input); // this should always go first in the compilation (for [style])
} else { styleInputs.splice(0, 0, input);
allOtherInputs.push(input); } else if (isClassBinding(input)) {
// this should always go first in the compilation (for [class])
classInputs.splice(0, 0, input);
} else {
allOtherInputs.push(input);
}
break;
case BindingType.Style:
styleInputs.push(input);
break;
case BindingType.Class:
classInputs.push(input);
break;
default:
allOtherInputs.push(input);
break;
} }
}); });
let currStyleIndex = 0; let currStyleIndex = 0;
let currClassIndex = 0;
let staticStylesMap: {[key: string]: any}|null = null; let staticStylesMap: {[key: string]: any}|null = null;
let staticClassesMap: {[key: string]: boolean}|null = null;
const stylesIndexMap: {[key: string]: number} = {}; const stylesIndexMap: {[key: string]: number} = {};
const classesIndexMap: {[key: string]: number} = {};
Object.getOwnPropertyNames(outputAttrs).forEach(name => { Object.getOwnPropertyNames(outputAttrs).forEach(name => {
const value = outputAttrs[name]; const value = outputAttrs[name];
if (name == 'style') { if (name == 'style') {
staticStylesMap = parseStyle(value); staticStylesMap = parseStyle(value);
Object.keys(staticStylesMap).forEach(prop => { stylesIndexMap[prop] = currStyleIndex++; }); Object.keys(staticStylesMap).forEach(prop => { stylesIndexMap[prop] = currStyleIndex++; });
} else if (name == 'class') {
staticClassesMap = {};
value.split(/\s+/g).forEach(className => {
classesIndexMap[className] = currClassIndex++;
staticClassesMap ![className] = true;
});
} else { } else {
attributes.push(o.literal(name)); attributes.push(o.literal(name));
if (attrI18nMetas.hasOwnProperty(name)) { if (attrI18nMetas.hasOwnProperty(name)) {
@ -357,6 +376,14 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
} }
} }
for (let i = 0; i < classInputs.length; i++) {
const input = classInputs[i];
const isMapBasedClassBinding = i === 0 && isClassBinding(input);
if (!isMapBasedClassBinding && !stylesIndexMap.hasOwnProperty(input.name)) {
classesIndexMap[input.name] = currClassIndex++;
}
}
// this will build the instructions so that they fall into the following syntax // this will build the instructions so that they fall into the following syntax
// => [prop1, prop2, prop3, 0, prop1, value1, prop2, value2] // => [prop1, prop2, prop3, 0, prop1, value1, prop2, value2]
Object.keys(stylesIndexMap).forEach(prop => { Object.keys(stylesIndexMap).forEach(prop => {
@ -364,7 +391,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
}); });
if (staticStylesMap) { if (staticStylesMap) {
initialStyleDeclarations.push(o.literal(core.InitialStylingFlags.INITIAL_STYLES)); initialStyleDeclarations.push(o.literal(core.InitialStylingFlags.VALUES_MODE));
Object.keys(staticStylesMap).forEach(prop => { Object.keys(staticStylesMap).forEach(prop => {
initialStyleDeclarations.push(o.literal(prop)); initialStyleDeclarations.push(o.literal(prop));
@ -373,6 +400,22 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
}); });
} }
Object.keys(classesIndexMap).forEach(prop => {
initialClassDeclarations.push(o.literal(prop));
});
if (staticClassesMap) {
initialClassDeclarations.push(o.literal(core.InitialStylingFlags.VALUES_MODE));
Object.keys(staticClassesMap).forEach(className => {
initialClassDeclarations.push(o.literal(className));
initialClassDeclarations.push(o.literal(true));
});
}
const hasStylingInstructions = initialStyleDeclarations.length || styleInputs.length ||
initialClassDeclarations.length || classInputs.length;
const attrArg: o.Expression = attributes.length > 0 ? const attrArg: o.Expression = attributes.length > 0 ?
this.constantPool.getConstLiteral(o.literalArr(attributes), true) : this.constantPool.getConstLiteral(o.literalArr(attributes), true) :
o.TYPED_NULL_EXPR; o.TYPED_NULL_EXPR;
@ -411,10 +454,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
const implicit = o.variable(CONTEXT_NAME); const implicit = o.variable(CONTEXT_NAME);
const elementStyleIndex =
(initialStyleDeclarations.length || styleInputs.length) ? this.allocateDataSlot() : 0;
const createSelfClosingInstruction = const createSelfClosingInstruction =
elementStyleIndex === 0 && element.children.length === 0 && element.outputs.length === 0; !hasStylingInstructions && element.children.length === 0 && element.outputs.length === 0;
if (createSelfClosingInstruction) { if (createSelfClosingInstruction) {
this.instruction( this.instruction(
@ -429,16 +470,30 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
...trimTrailingNulls(parameters)); ...trimTrailingNulls(parameters));
// initial styling for static style="..." attributes // initial styling for static style="..." attributes
if (elementStyleIndex > 0) { if (hasStylingInstructions) {
let paramsList: (o.Expression)[] = [o.literal(elementStyleIndex)]; const paramsList: (o.Expression)[] = [];
if (initialStyleDeclarations.length) { if (initialStyleDeclarations.length) {
// the template compiler handles initial styling (e.g. style="foo") values // the template compiler handles initial style (e.g. style="foo") values
// in a special command called `elementStyle` so that the initial styles // in a special command called `elementStyle` so that the initial styles
// can be processed during runtime. These initial styles values are bound to // can be processed during runtime. These initial styles values are bound to
// a constant because the inital style values do not change (since they're static). // a constant because the inital style values do not change (since they're static).
paramsList.push( paramsList.push(
this.constantPool.getConstLiteral(o.literalArr(initialStyleDeclarations), true)); this.constantPool.getConstLiteral(o.literalArr(initialStyleDeclarations), true));
} else if (initialClassDeclarations.length) {
// no point in having an extra `null` value unless there are follow-up params
paramsList.push(o.NULL_EXPR);
} }
if (initialClassDeclarations.length) {
// the template compiler handles initial class styling (e.g. class="foo") values
// in a special command called `elementClass` so that the initial class
// can be processed during runtime. These initial class values are bound to
// a constant because the inital class values do not change (since they're static).
paramsList.push(
this.constantPool.getConstLiteral(o.literalArr(initialClassDeclarations), true));
}
this._creationCode.push(o.importExpr(R3.elementStyling).callFn(paramsList).toStmt()); this._creationCode.push(o.importExpr(R3.elementStyling).callFn(paramsList).toStmt());
} }
@ -465,25 +520,64 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
}); });
} }
if (styleInputs.length && elementStyleIndex > 0) { if ((styleInputs.length || classInputs.length) && hasStylingInstructions) {
const indexLiteral = o.literal(elementStyleIndex); const indexLiteral = o.literal(elementIndex);
styleInputs.forEach((input, i) => {
const isMapBasedStyleBinding = i == 0 && input.name == 'style'; const firstStyle = styleInputs[0];
const convertedBinding = this.convertPropertyBinding(implicit, input.value, true); const mapBasedStyleInput = firstStyle && firstStyle.name == 'style' ? firstStyle : null;
if (isMapBasedStyleBinding) {
this.instruction( const firstClass = classInputs[0];
this._bindingCode, input.sourceSpan, R3.elementStyle, indexLiteral, convertedBinding); const mapBasedClassInput = firstClass && isClassBinding(firstClass) ? firstClass : null;
} else {
const stylingInput = mapBasedStyleInput || mapBasedClassInput;
if (stylingInput) {
const params: o.Expression[] = [];
if (mapBasedStyleInput) {
params.push(this.convertPropertyBinding(implicit, mapBasedStyleInput.value, true));
} else if (mapBasedClassInput) {
params.push(o.NULL_EXPR);
}
if (mapBasedClassInput) {
params.push(this.convertPropertyBinding(implicit, mapBasedClassInput.value, true));
}
this.instruction(
this._bindingCode, stylingInput.sourceSpan, R3.elementStylingMap, indexLiteral,
...params);
}
let lastInputCommand: t.BoundAttribute|null = null;
if (styleInputs.length) {
let i = mapBasedStyleInput ? 1 : 0;
for (i; i < styleInputs.length; i++) {
const input = styleInputs[i];
const convertedBinding = this.convertPropertyBinding(implicit, input.value, true);
const key = input.name; const key = input.name;
let styleIndex: number = stylesIndexMap[key] !; const styleIndex: number = stylesIndexMap[key] !;
this.instruction( this.instruction(
this._bindingCode, input.sourceSpan, R3.elementStyleProp, indexLiteral, this._bindingCode, input.sourceSpan, R3.elementStyleProp, indexLiteral,
o.literal(styleIndex), convertedBinding); o.literal(styleIndex), convertedBinding);
} }
});
const spanEnd = styleInputs[styleInputs.length - 1].sourceSpan; lastInputCommand = styleInputs[styleInputs.length - 1];
this.instruction(this._bindingCode, spanEnd, R3.elementStylingApply, indexLiteral); }
if (classInputs.length) {
let i = mapBasedClassInput ? 1 : 0;
for (i; i < classInputs.length; i++) {
const input = classInputs[i];
const convertedBinding = this.convertPropertyBinding(implicit, input.value, true);
const key = input.name;
const classIndex: number = classesIndexMap[key] !;
this.instruction(
this._bindingCode, input.sourceSpan, R3.elementClassProp, indexLiteral,
o.literal(classIndex), convertedBinding);
}
lastInputCommand = classInputs[classInputs.length - 1];
}
this.instruction(
this._bindingCode, lastInputCommand !.sourceSpan, R3.elementStylingApply, indexLiteral);
} }
// Generate element input bindings // Generate element input bindings
@ -494,18 +588,6 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
} }
const convertedBinding = this.convertPropertyBinding(implicit, input.value); const convertedBinding = this.convertPropertyBinding(implicit, input.value);
const specialInstruction = SPECIAL_CASED_PROPERTIES_INSTRUCTION_MAP[input.name];
if (specialInstruction) {
// special case for [style] and [class] bindings since they are not handled as
// standard properties within this implementation. Instead they are
// handed off to special cased instruction handlers which will then
// delegate them as animation sequences (or input bindings for dirs/cmps)
this.instruction(
this._bindingCode, input.sourceSpan, specialInstruction, o.literal(elementIndex),
convertedBinding);
return;
}
const instruction = mapBindingToInstruction(input.type); const instruction = mapBindingToInstruction(input.type);
if (instruction) { if (instruction) {
// TODO(chuckj): runtime: security context? // TODO(chuckj): runtime: security context?
@ -975,3 +1057,7 @@ export function makeBindingParser(): BindingParser {
new Parser(new Lexer()), DEFAULT_INTERPOLATION_CONFIG, new DomElementSchemaRegistry(), null, new Parser(new Lexer()), DEFAULT_INTERPOLATION_CONFIG, new DomElementSchemaRegistry(), null,
[]); []);
} }
function isClassBinding(input: t.BoundAttribute): boolean {
return input.name == 'className' || input.name == 'class';
}

View File

@ -6,9 +6,12 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {InitialStylingFlags} from '../../src/core';
import {MockDirectory, setup} from '../aot/test_util'; import {MockDirectory, setup} from '../aot/test_util';
import {compile, expectEmit} from './mock_compile'; import {compile, expectEmit} from './mock_compile';
/* These tests are codified version of the tests in compiler_canonical_spec.ts. Every /* These tests are codified version of the tests in compiler_canonical_spec.ts. Every
* test in compiler_canonical_spec.ts should have a corresponding test here. * test in compiler_canonical_spec.ts should have a corresponding test here.
*/ */
@ -44,15 +47,17 @@ describe('compiler compliance', () => {
// The template should look like this (where IDENT is a wild card for an identifier): // The template should look like this (where IDENT is a wild card for an identifier):
const template = ` const template = `
const $c1$ = ['class', 'my-app', 'title', 'Hello']; const $c1$ = ['title', 'Hello'];
const $c2$ = ['cx', '20', 'cy', '30', 'r', '50']; const $c2$ = ['my-app', ${InitialStylingFlags.VALUES_MODE}, 'my-app', true];
const $c3$ = ['cx', '20', 'cy', '30', 'r', '50'];
template: function MyComponent_Template(rf: IDENT, ctx: IDENT) { template: function MyComponent_Template(rf: IDENT, ctx: IDENT) {
if (rf & 1) { if (rf & 1) {
$r3$.ɵE(0, 'div', $c1$); $r3$.ɵE(0, 'div', $c1$);
$r3$.ɵs((null as any), $c2$);
$r3$.ɵNS(); $r3$.ɵNS();
$r3$.ɵE(1, 'svg'); $r3$.ɵE(1, 'svg');
$r3$.ɵEe(2, 'circle', $c2$); $r3$.ɵEe(2, 'circle', $c3$);
$r3$.ɵe(); $r3$.ɵe();
$r3$.ɵNH(); $r3$.ɵNH();
$r3$.ɵE(3, 'p'); $r3$.ɵE(3, 'p');
@ -93,11 +98,13 @@ describe('compiler compliance', () => {
// The template should look like this (where IDENT is a wild card for an identifier): // The template should look like this (where IDENT is a wild card for an identifier):
const template = ` const template = `
const $c1$ = ['class', 'my-app', 'title', 'Hello']; const $c1$ = ['title', 'Hello'];
const $c2$ = ['my-app', ${InitialStylingFlags.VALUES_MODE}, 'my-app', true];
template: function MyComponent_Template(rf: IDENT, ctx: IDENT) { template: function MyComponent_Template(rf: IDENT, ctx: IDENT) {
if (rf & 1) { if (rf & 1) {
$r3$.ɵE(0, 'div', $c1$); $r3$.ɵE(0, 'div', $c1$);
$r3$.ɵs((null as any), $c2$);
$r3$.ɵNM(); $r3$.ɵNM();
$r3$.ɵE(1, 'math'); $r3$.ɵE(1, 'math');
$r3$.ɵEe(2, 'infinity'); $r3$.ɵEe(2, 'infinity');
@ -141,11 +148,13 @@ describe('compiler compliance', () => {
// The template should look like this (where IDENT is a wild card for an identifier): // The template should look like this (where IDENT is a wild card for an identifier):
const template = ` const template = `
const $c1$ = ['class', 'my-app', 'title', 'Hello']; const $c1$ = ['title', 'Hello'];
const $c2$ = ['my-app', ${InitialStylingFlags.VALUES_MODE}, 'my-app', true];
template: function MyComponent_Template(rf: IDENT, ctx: IDENT) { template: function MyComponent_Template(rf: IDENT, ctx: IDENT) {
if (rf & 1) { if (rf & 1) {
$r3$.ɵE(0, 'div', $c1$); $r3$.ɵE(0, 'div', $c1$);
$r3$.ɵs((null as any), $c2$);
$r3$.ɵT(1, 'Hello '); $r3$.ɵT(1, 'Hello ');
$r3$.ɵE(2, 'b'); $r3$.ɵE(2, 'b');
$r3$.ɵT(3, 'World'); $r3$.ɵT(3, 'World');
@ -322,6 +331,7 @@ describe('compiler compliance', () => {
const factory = 'factory: function MyComponent_Factory() { return new MyComponent(); }'; const factory = 'factory: function MyComponent_Factory() { return new MyComponent(); }';
const template = ` const template = `
const _c0 = ['background-color']; const _c0 = ['background-color'];
const _c1 = ['error'];
class MyComponent { class MyComponent {
static ngComponentDef = i0.ɵdefineComponent({type:MyComponent,selectors:[['my-component']], static ngComponentDef = i0.ɵdefineComponent({type:MyComponent,selectors:[['my-component']],
factory:function MyComponent_Factory(){ factory:function MyComponent_Factory(){
@ -329,13 +339,13 @@ describe('compiler compliance', () => {
},template:function MyComponent_Template(rf:number,ctx:any){ },template:function MyComponent_Template(rf:number,ctx:any){
if (rf & 1) { if (rf & 1) {
$r3$.ɵE(0, 'div'); $r3$.ɵE(0, 'div');
$r3$.ɵs(1, _c0); $r3$.ɵs(_c0, _c1);
$r3$.ɵe(); $r3$.ɵe();
} }
if (rf & 2) { if (rf & 2) {
$r3$.ɵsp(1, 0, ctx.color); $r3$.ɵsp(0, 0, ctx.color);
$r3$.ɵsa(1); $r3$.ɵcp(0, 0, ctx.error);
$r3$.ɵkn(0, 'error', $r3$.ɵb(ctx.error)); $r3$.ɵsa(0);
} }
} }
`; `;

View File

@ -43,12 +43,12 @@ describe('compiler compliance: styling', () => {
template: function MyComponent_Template(rf: $RenderFlags$, $ctx$: $MyComponent$) { template: function MyComponent_Template(rf: $RenderFlags$, $ctx$: $MyComponent$) {
if (rf & 1) { if (rf & 1) {
$r3$.ɵE(0, 'div'); $r3$.ɵE(0, 'div');
$r3$.ɵs(1); $r3$.ɵs();
$r3$.ɵe(); $r3$.ɵe();
} }
if (rf & 2) { if (rf & 2) {
$r3$.ɵsm(1, $ctx$.myStyleExp); $r3$.ɵsm(0, $ctx$.myStyleExp);
$r3$.ɵsa(1); $r3$.ɵsa(0);
} }
} }
`; `;
@ -57,7 +57,7 @@ describe('compiler compliance: styling', () => {
expectEmit(result.source, template, 'Incorrect template'); expectEmit(result.source, template, 'Incorrect template');
}); });
it('should place initial, multi, singular and application followed by attribute styling instructions in the template code in that order', it('should place initial, multi, singular and application followed by attribute style instructions in the template code in that order',
() => { () => {
const files = { const files = {
app: { app: {
@ -85,7 +85,7 @@ describe('compiler compliance: styling', () => {
}; };
const template = ` const template = `
const _c0 = ['opacity','width','height',${InitialStylingFlags.INITIAL_STYLES},'opacity','1']; const _c0 = ['opacity','width','height',${InitialStylingFlags.VALUES_MODE},'opacity','1'];
class MyComponent { class MyComponent {
static ngComponentDef = i0.ɵdefineComponent({ static ngComponentDef = i0.ɵdefineComponent({
type: MyComponent, type: MyComponent,
@ -96,14 +96,14 @@ describe('compiler compliance: styling', () => {
template: function MyComponent_Template(rf: $RenderFlags$, $ctx$: $MyComponent$) { template: function MyComponent_Template(rf: $RenderFlags$, $ctx$: $MyComponent$) {
if (rf & 1) { if (rf & 1) {
$r3$.ɵE(0, 'div'); $r3$.ɵE(0, 'div');
$r3$.ɵs(1, _c0); $r3$.ɵs(_c0);
$r3$.ɵe(); $r3$.ɵe();
} }
if (rf & 2) { if (rf & 2) {
$r3$.ɵsm(1, $ctx$.myStyleExp); $r3$.ɵsm(0, $ctx$.myStyleExp);
$r3$.ɵsp(1, 1, $ctx$.myWidth); $r3$.ɵsp(0, 1, $ctx$.myWidth);
$r3$.ɵsp(1, 2, $ctx$.myHeight); $r3$.ɵsp(0, 2, $ctx$.myHeight);
$r3$.ɵsa(1); $r3$.ɵsa(0);
$r3$.ɵa(0, 'style', $r3$.ɵb('border-width: 10px')); $r3$.ɵa(0, 'style', $r3$.ɵb('border-width: 10px'));
} }
} }
@ -127,7 +127,7 @@ describe('compiler compliance: styling', () => {
template: \`<div [class]="myClassExp"></div>\` template: \`<div [class]="myClassExp"></div>\`
}) })
export class MyComponent { export class MyComponent {
myClassExp = [{color:'orange'}, {color:'green', duration:1000}] myClassExp = {'foo':true}
} }
@NgModule({declarations: [MyComponent]}) @NgModule({declarations: [MyComponent]})
@ -139,10 +139,13 @@ describe('compiler compliance: styling', () => {
const template = ` const template = `
template: function MyComponent_Template(rf: $RenderFlags$, $ctx$: $MyComponent$) { template: function MyComponent_Template(rf: $RenderFlags$, $ctx$: $MyComponent$) {
if (rf & 1) { if (rf & 1) {
$r3$.ɵEe(0, 'div'); $r3$.ɵE(0, 'div');
$r3$.ɵs();
$r3$.ɵe();
} }
if (rf & 2) { if (rf & 2) {
$r3$.ɵk(0,$r3$.ɵb($ctx$.myClassExp)); $r3$.ɵsm(0,(null as any),$ctx$.myClassExp);
$r3$.ɵsa(0);
} }
} }
`; `;
@ -150,5 +153,112 @@ describe('compiler compliance: styling', () => {
const result = compile(files, angularFiles); const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template'); expectEmit(result.source, template, 'Incorrect template');
}); });
it('should place initial, multi, singular and application followed by attribute class instructions in the template code in that order',
() => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'my-component',
template: \`<div class="grape"
[attr.class]="'banana'"
[class.apple]="yesToApple"
[class]="myClassExp"
[class.orange]="yesToOrange"></div>\`
})
export class MyComponent {
myClassExp = {a:true, b:true};
yesToApple = true;
yesToOrange = true;
}
@NgModule({declarations: [MyComponent]})
export class MyModule {}
`
}
};
const template = `
const _c0 = ['grape','apple','orange',${InitialStylingFlags.VALUES_MODE},'grape',true];
class MyComponent {
static ngComponentDef = i0.ɵdefineComponent({
type: MyComponent,
selectors:[['my-component']],
factory:function MyComponent_Factory(){
return new MyComponent();
},
template: function MyComponent_Template(rf: $RenderFlags$, $ctx$: $MyComponent$) {
if (rf & 1) {
$r3$.ɵE(0, 'div');
$r3$.ɵs((null as any), _c0);
$r3$.ɵe();
}
if (rf & 2) {
$r3$.ɵsm(0, (null as any), $ctx$.myClassExp);
$r3$.ɵcp(0, 1, $ctx$.yesToApple);
$r3$.ɵcp(0, 2, $ctx$.yesToOrange);
$r3$.ɵsa(0);
$r3$.ɵa(0, 'class', $r3$.ɵb('banana'));
}
}
});
`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});
it('should not generate the styling apply instruction if there are only static style/class attributes',
() => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'my-component',
template: \`<div class="foo"
style="width:100px"
[attr.class]="'round'"
[attr.style]="'height:100px'"></div>\`
})
export class MyComponent {}
@NgModule({declarations: [MyComponent]})
export class MyModule {}
`
}
};
const template = `
const _c0 = ['width',${InitialStylingFlags.VALUES_MODE},'width','100px'];
const _c1 = ['foo',${InitialStylingFlags.VALUES_MODE},'foo',true];
class MyComponent {
static ngComponentDef = i0.ɵdefineComponent({
type: MyComponent,
selectors:[['my-component']],
factory:function MyComponent_Factory(){
return new MyComponent();
},
template: function MyComponent_Template(rf: $RenderFlags$, $ctx$: $MyComponent$) {
if (rf & 1) {
$r3$.ɵE(0, 'div');
$r3$.ɵs(_c0, _c1);
$r3$.ɵe();
}
if (rf & 2) {
$r3$.ɵa(0, 'class', $r3$.ɵb('round'));
$r3$.ɵa(0, 'style', $r3$.ɵb('height:100px'));
}
}
});
`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});
}); });
}); });

View File

@ -80,8 +80,7 @@ export {
sm as ɵsm, sm as ɵsm,
sp as ɵsp, sp as ɵsp,
sa as ɵsa, sa as ɵsa,
k as ɵk, cp as ɵcp,
kn as ɵkn,
t as ɵt, t as ɵt,
v as ɵv, v as ɵv,
st as ɵst, st as ɵst,

View File

@ -63,6 +63,7 @@ export function assertComponentType(
msg: string = msg: string =
'Type passed in is not ComponentType, it does not have \'ngComponentDef\' property.') { 'Type passed in is not ComponentType, it does not have \'ngComponentDef\' property.') {
if (!actual.ngComponentDef) { if (!actual.ngComponentDef) {
debugger;
throwError(msg); throwError(msg);
} }
} }
@ -70,4 +71,4 @@ export function assertComponentType(
function throwError(msg: string): never { function throwError(msg: string): never {
debugger; // Left intentionally for better debugger experience. debugger; // Left intentionally for better debugger experience.
throw new Error(`ASSERTION ERROR: ${msg}`); throw new Error(`ASSERTION ERROR: ${msg}`);
} }

View File

@ -51,14 +51,13 @@ export {
element as Ee, element as Ee,
elementAttribute as a, elementAttribute as a,
elementClass as k, elementClassProp as cp,
elementClassNamed as kn,
elementEnd as e, elementEnd as e,
elementProperty as p, elementProperty as p,
elementStart as E, elementStart as E,
elementStyling as s, elementStyling as s,
elementStyle as sm, elementStylingMap as sm,
elementStyleProp as sp, elementStyleProp as sp,
elementStylingApply as sa, elementStylingApply as sa,

View File

@ -15,7 +15,7 @@ import {assertDefined, assertEqual, assertLessThan, assertNotDefined, assertNotE
import {throwCyclicDependencyError, throwErrorIfNoChangesMode, throwMultipleComponentError} from './errors'; import {throwCyclicDependencyError, throwErrorIfNoChangesMode, throwMultipleComponentError} from './errors';
import {executeHooks, executeInitHooks, queueInitHooks, queueLifecycleHooks} from './hooks'; import {executeHooks, executeInitHooks, queueInitHooks, queueLifecycleHooks} from './hooks';
import {ACTIVE_INDEX, LContainer, RENDER_PARENT, VIEWS} from './interfaces/container'; import {ACTIVE_INDEX, LContainer, RENDER_PARENT, VIEWS} from './interfaces/container';
import {ComponentDefInternal, ComponentQuery, ComponentTemplate, DirectiveDefInternal, DirectiveDefListOrFactory, PipeDefListOrFactory, RenderFlags} from './interfaces/definition'; import {ComponentDefInternal, ComponentQuery, ComponentTemplate, DirectiveDefInternal, DirectiveDefListOrFactory, InitialStylingFlags, PipeDefListOrFactory, RenderFlags} from './interfaces/definition';
import {LInjector} from './interfaces/injector'; import {LInjector} from './interfaces/injector';
import {AttributeMarker, InitialInputData, InitialInputs, LContainerNode, LElementNode, LNode, LProjectionNode, LTextNode, LViewNode, PropertyAliasValue, PropertyAliases, TAttributes, TContainerNode, TElementNode, TNode, TNodeFlags, TNodeType} from './interfaces/node'; import {AttributeMarker, InitialInputData, InitialInputs, LContainerNode, LElementNode, LNode, LProjectionNode, LTextNode, LViewNode, PropertyAliasValue, PropertyAliases, TAttributes, TContainerNode, TElementNode, TNode, TNodeFlags, TNodeType} from './interfaces/node';
import {CssSelectorList, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection'; import {CssSelectorList, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection';
@ -25,8 +25,8 @@ import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTENT_QUERIES, CONTEXT, Curre
import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert'; import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert';
import {appendChild, appendProjectedNode, canInsertNativeNode, createTextNode, findComponentHost, getChildLNode, getLViewChild, getNextLNode, getParentLNode, insertView, removeView} from './node_manipulation'; import {appendChild, appendProjectedNode, canInsertNativeNode, createTextNode, findComponentHost, getChildLNode, getLViewChild, getNextLNode, getParentLNode, insertView, removeView} from './node_manipulation';
import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher'; import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher';
import {StylingContext, allocStylingContext, createStylingContextTemplate, renderStyles as renderElementStyles, updateStyleMap as updateElementStyleMap, updateStyleProp as updateElementStyleProp} from './styling'; import {StylingContext, StylingIndex, allocStylingContext, createStylingContextTemplate, renderStyling as renderElementStyles, updateClassProp as updateElementClassProp, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling';
import {isDifferent, stringify} from './util'; import {assertDataInRangeInternal, isDifferent, loadElementInternal, loadInternal, stringify} from './util';
import {ViewRef} from './view_ref'; import {ViewRef} from './view_ref';
@ -96,6 +96,7 @@ export const CIRCULAR = '__CIRCULAR__';
*/ */
let renderer: Renderer3; let renderer: Renderer3;
let rendererFactory: RendererFactory3; let rendererFactory: RendererFactory3;
let currentElementNode: LElementNode|null = null;
export function getRenderer(): Renderer3 { export function getRenderer(): Renderer3 {
// top level variables should not be exported for performance reasons (PERF_NOTES.md) // top level variables should not be exported for performance reasons (PERF_NOTES.md)
@ -668,6 +669,7 @@ export function elementStart(
const node: LElementNode = const node: LElementNode =
createLNode(index, TNodeType.Element, native !, name, attrs || null, null); createLNode(index, TNodeType.Element, native !, name, attrs || null, null);
currentElementNode = node;
if (attrs) { if (attrs) {
setUpAttributes(native, attrs); setUpAttributes(native, attrs);
@ -1104,6 +1106,7 @@ export function elementEnd() {
const queries = previousOrParentNode.queries; const queries = previousOrParentNode.queries;
queries && queries.addNode(previousOrParentNode); queries && queries.addNode(previousOrParentNode);
queueLifecycleHooks(previousOrParentNode.tNode.flags, tView); queueLifecycleHooks(previousOrParentNode.tNode.flags, tView);
currentElementNode = null;
} }
/** /**
@ -1118,7 +1121,7 @@ export function elementEnd() {
export function elementAttribute( export function elementAttribute(
index: number, name: string, value: any, sanitizer?: SanitizerFn): void { index: number, name: string, value: any, sanitizer?: SanitizerFn): void {
if (value !== NO_CHANGE) { if (value !== NO_CHANGE) {
const element: LElementNode = load(index); const element = loadElement(index);
if (value == null) { if (value == null) {
ngDevMode && ngDevMode.rendererRemoveAttribute++; ngDevMode && ngDevMode.rendererRemoveAttribute++;
isProceduralRenderer(renderer) ? renderer.removeAttribute(element.native, name) : isProceduralRenderer(renderer) ? renderer.removeAttribute(element.native, name) :
@ -1149,7 +1152,7 @@ export function elementAttribute(
export function elementProperty<T>( export function elementProperty<T>(
index: number, propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn): void { index: number, propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn): void {
if (value === NO_CHANGE) return; if (value === NO_CHANGE) return;
const node = load(index) as LElementNode; const node = loadElement(index) as LElementNode;
const tNode = node.tNode; const tNode = node.tNode;
// if tNode.inputs is undefined, a listener has created outputs, but inputs haven't // if tNode.inputs is undefined, a listener has created outputs, but inputs haven't
// yet been checked // yet been checked
@ -1268,44 +1271,9 @@ function generatePropertyAliases(
* renaming as part of minification. * renaming as part of minification.
* @param value A value indicating if a given class should be added or removed. * @param value A value indicating if a given class should be added or removed.
*/ */
export function elementClassNamed<T>(index: number, className: string, value: T | NO_CHANGE): void { export function elementClassProp<T>(
if (value !== NO_CHANGE) { index: number, stylingIndex: number, value: T | NO_CHANGE): void {
const lElement = load(index) as LElementNode; updateElementClassProp(getStylingContext(index), stylingIndex, value ? true : false);
if (value) {
ngDevMode && ngDevMode.rendererAddClass++;
isProceduralRenderer(renderer) ? renderer.addClass(lElement.native, className) :
lElement.native.classList.add(className);
} else {
ngDevMode && ngDevMode.rendererRemoveClass++;
isProceduralRenderer(renderer) ? renderer.removeClass(lElement.native, className) :
lElement.native.classList.remove(className);
}
}
}
/**
* Set the `className` property on a DOM element.
*
* This instruction is meant to handle the `[class]="exp"` usage.
*
* `elementClass` instruction writes the value to the "element's" `className` property.
*
* @param index The index of the element to update in the data array
* @param value A value indicating a set of classes which should be applied. The method overrides
* any existing classes. The value is stringified (`toString`) before it is applied to the
* element.
*/
export function elementClass<T>(index: number, value: T | NO_CHANGE): void {
if (value !== NO_CHANGE) {
// TODO: This is a naive implementation which simply writes value to the `className`. In the
// future
// we will add logic here which would work with the animation code.
const lElement: LElementNode = load(index);
ngDevMode && ngDevMode.rendererSetClassName++;
isProceduralRenderer(renderer) ? renderer.setProperty(lElement.native, 'className', value) :
lElement.native['className'] = stringify(value);
}
} }
/** /**
@ -1323,22 +1291,29 @@ export function elementClass<T>(index: number, value: T | NO_CHANGE): void {
* (Note that this is not the element index, but rather an index value allocated * (Note that this is not the element index, but rather an index value allocated
* specifically for element styling--the index must be the next index after the element * specifically for element styling--the index must be the next index after the element
* index.) * index.)
* @param styles A key/value map of CSS styles that will be registered on the element. * @param styleDeclarations A key/value array of CSS styles that will be registered on the element.
* Each individual style will be used on the element as long as it is not overridden * Each individual style will be used on the element as long as it is not overridden
* by any styles placed on the element by multiple (`[style]`) or singular (`[style.prop]`) * by any styles placed on the element by multiple (`[style]`) or singular (`[style.prop]`)
* bindings. If a style binding changes its value to null then the initial styling * bindings. If a style binding changes its value to null then the initial styling
* values that are passed in here will be applied to the element (if matched). * values that are passed in here will be applied to the element (if matched).
* @param classDeclarations A key/value array of CSS classes that will be registered on the element.
* Each individual style will be used on the element as long as it is not overridden
* by any classes placed on the element by multiple (`[class]`) or singular (`[class.named]`)
* bindings. If a class binding changes its value to a falsy value then the matching initial
* class value that are passed in here will be applied to the element (if matched).
*/ */
export function elementStyling<T>(index: number, styles?: (string | number)[] | null): void { export function elementStyling<T>(
const tNode = load<LElementNode>(index - 1).tNode; styleDeclarations?: (string | boolean | InitialStylingFlags)[] | null,
classDeclarations?: (string | boolean | InitialStylingFlags)[] | null): void {
const lElement = currentElementNode !;
const tNode = lElement.tNode;
if (!tNode.stylingTemplate) { if (!tNode.stylingTemplate) {
// initialize the styling template. // initialize the styling template.
tNode.stylingTemplate = createStylingContextTemplate(styles); tNode.stylingTemplate = createStylingContextTemplate(styleDeclarations, classDeclarations);
} }
// Allocate space but leave null for lazy creation. if (styleDeclarations && styleDeclarations.length ||
viewData[index + HEADER_OFFSET] = null; classDeclarations && classDeclarations.length) {
if (styles && styles.length) { elementStylingApply(tNode.index - HEADER_OFFSET);
elementStylingApply(index);
} }
} }
@ -1354,12 +1329,13 @@ export function elementStyling<T>(index: number, styles?: (string | number)[] |
*/ */
function getStylingContext(index: number): StylingContext { function getStylingContext(index: number): StylingContext {
let stylingContext = load<StylingContext>(index); let stylingContext = load<StylingContext>(index);
if (!stylingContext) { if (!Array.isArray(stylingContext)) {
const lElement: LElementNode = load(index - 1); const lElement = stylingContext as any as LElementNode;
const tNode = lElement.tNode; const tNode = lElement.tNode;
ngDevMode && ngDevMode &&
assertDefined(tNode.stylingTemplate, 'getStylingContext() called before elementStyling()'); assertDefined(tNode.stylingTemplate, 'getStylingContext() called before elementStyling()');
stylingContext = viewData[index + HEADER_OFFSET] = allocStylingContext(tNode.stylingTemplate !); stylingContext = viewData[index + HEADER_OFFSET] =
allocStylingContext(lElement, tNode.stylingTemplate !);
} }
return stylingContext; return stylingContext;
} }
@ -1379,7 +1355,7 @@ function getStylingContext(index: number): StylingContext {
* index.) * index.)
*/ */
export function elementStylingApply<T>(index: number): void { export function elementStylingApply<T>(index: number): void {
renderElementStyles(load<LElementNode>(index - 1), getStylingContext(index), renderer); renderElementStyles(getStylingContext(index), renderer);
} }
/** /**
@ -1436,12 +1412,17 @@ export function elementStyleProp<T>(
* (Note that this is not the element index, but rather an index value allocated * (Note that this is not the element index, but rather an index value allocated
* specifically for element styling--the index must be the next index after the element * specifically for element styling--the index must be the next index after the element
* index.) * index.)
* @param value A value indicating if a given style should be added or removed. * @param styles A key/value style map of the styles that will be applied to the given element.
* The expected shape of `value` is an object where keys are style names and the values * Any missing styles (that have already been applied to the element beforehand) will be
* are their corresponding values to set. If value is null, then the style is removed. * removed (unset) from the element's styling.
* @param classes A key/value style map of CSS classes that will be added to the given element.
* Any missing classes (that have already been applied to the element beforehand) will be
* removed (unset) from the element's list of CSS classes.
*/ */
export function elementStyle<T>(index: number, value: {[styleName: string]: any} | null): void { export function elementStylingMap<T>(
updateElementStyleMap(getStylingContext(index), value); index: number, styles: {[styleName: string]: any} | null,
classes?: {[key: string]: any} | string | null): void {
updateStylingMap(getStylingContext(index), styles, classes);
} }
////////////////////////// //////////////////////////
@ -1476,7 +1457,7 @@ export function text(index: number, value?: any): void {
export function textBinding<T>(index: number, value: T | NO_CHANGE): void { export function textBinding<T>(index: number, value: T | NO_CHANGE): void {
if (value !== NO_CHANGE) { if (value !== NO_CHANGE) {
ngDevMode && assertDataInRange(index + HEADER_OFFSET); ngDevMode && assertDataInRange(index + HEADER_OFFSET);
const existingNode = load(index) as LTextNode; const existingNode = loadElement(index) as any as LTextNode;
ngDevMode && assertDefined(existingNode, 'LNode should exist'); ngDevMode && assertDefined(existingNode, 'LNode should exist');
ngDevMode && assertDefined(existingNode.native, 'native element should exist'); ngDevMode && assertDefined(existingNode.native, 'native element should exist');
ngDevMode && ngDevMode.rendererSetText++; ngDevMode && ngDevMode.rendererSetText++;
@ -1758,7 +1739,7 @@ export function container(
* @param index The index of the container in the data array * @param index The index of the container in the data array
*/ */
export function containerRefreshStart(index: number): void { export function containerRefreshStart(index: number): void {
previousOrParentNode = load(index) as LNode; previousOrParentNode = loadElement(index) as LNode;
ngDevMode && assertNodeType(previousOrParentNode, TNodeType.Container); ngDevMode && assertNodeType(previousOrParentNode, TNodeType.Container);
isParent = true; isParent = true;
(previousOrParentNode as LContainerNode).data[ACTIVE_INDEX] = 0; (previousOrParentNode as LContainerNode).data[ACTIVE_INDEX] = 0;
@ -2541,17 +2522,6 @@ export function store<T>(index: number, value: T): void {
viewData[adjustedIndex] = value; viewData[adjustedIndex] = value;
} }
/** Retrieves a value from current `viewData`. */
export function load<T>(index: number): T {
return loadInternal<T>(index, viewData);
}
/** Retrieves a value from any `LViewData`. */
export function loadInternal<T>(index: number, arr: LViewData): T {
ngDevMode && assertDataInRange(index + HEADER_OFFSET, arr);
return arr[index + HEADER_OFFSET];
}
/** Retrieves a value from the `directives` array. */ /** Retrieves a value from the `directives` array. */
export function loadDirective<T>(index: number): T { export function loadDirective<T>(index: number): T {
ngDevMode && assertDefined(directives, 'Directives array should be defined if reading a dir.'); ngDevMode && assertDefined(directives, 'Directives array should be defined if reading a dir.');
@ -2568,6 +2538,15 @@ export function loadQueryList<T>(queryListIdx: number): QueryList<T> {
return viewData[CONTENT_QUERIES] ![queryListIdx]; return viewData[CONTENT_QUERIES] ![queryListIdx];
} }
/** Retrieves a value from current `viewData`. */
export function load<T>(index: number): T {
return loadInternal<T>(index, viewData);
}
export function loadElement(index: number): LElementNode {
return loadElementInternal(index, viewData);
}
/** Gets the current binding value and increments the binding index. */ /** Gets the current binding value and increments the binding index. */
export function consumeBinding(): any { export function consumeBinding(): any {
ngDevMode && assertDataInRange(viewData[BINDING_INDEX]); ngDevMode && assertDataInRange(viewData[BINDING_INDEX]);
@ -2645,7 +2624,7 @@ function assertHasParent() {
function assertDataInRange(index: number, arr?: any[]) { function assertDataInRange(index: number, arr?: any[]) {
if (arr == null) arr = viewData; if (arr == null) arr = viewData;
assertLessThan(index, arr ? arr.length : 0, 'index expected to be a valid data index'); assertDataInRangeInternal(index, arr || viewData);
} }
function assertDataNext(index: number, arr?: any[]) { function assertDataNext(index: number, arr?: any[]) {

View File

@ -299,8 +299,6 @@ export type PipeTypeList =
// failure based on types. // failure based on types.
export const unusedValueExportToPlacateAjd = 1; export const unusedValueExportToPlacateAjd = 1;
// Note this will expand once `class` is introduced to styling
export const enum InitialStylingFlags { export const enum InitialStylingFlags {
/** Mode for matching initial style values */ VALUES_MODE = 0b1,
INITIAL_STYLES = 0b00,
} }

View File

@ -63,8 +63,7 @@ export const angularCoreEnv: {[name: string]: Function} = {
'ɵi7': r3.i7, 'ɵi7': r3.i7,
'ɵi8': r3.i8, 'ɵi8': r3.i8,
'ɵiV': r3.iV, 'ɵiV': r3.iV,
'ɵk': r3.k, 'ɵcp': r3.cp,
'ɵkn': r3.kn,
'ɵL': r3.L, 'ɵL': r3.L,
'ɵld': r3.ld, 'ɵld': r3.ld,
'ɵP': r3.P, 'ɵP': r3.P,

View File

@ -14,7 +14,7 @@ import {unusedValueExportToPlacateAjd as unused3} from './interfaces/projection'
import {ProceduralRenderer3, RComment, RElement, RNode, RText, Renderer3, isProceduralRenderer, unusedValueExportToPlacateAjd as unused4} from './interfaces/renderer'; import {ProceduralRenderer3, RComment, RElement, RNode, RText, Renderer3, isProceduralRenderer, unusedValueExportToPlacateAjd as unused4} from './interfaces/renderer';
import {CLEANUP, CONTAINER_INDEX, DIRECTIVES, FLAGS, HEADER_OFFSET, HOST_NODE, HookData, LViewData, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, TVIEW, unusedValueExportToPlacateAjd as unused5} from './interfaces/view'; import {CLEANUP, CONTAINER_INDEX, DIRECTIVES, FLAGS, HEADER_OFFSET, HOST_NODE, HookData, LViewData, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, TVIEW, unusedValueExportToPlacateAjd as unused5} from './interfaces/view';
import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert'; import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert';
import {stringify} from './util'; import {readElementValue, stringify} from './util';
const unusedValueToPlacateAjd = unused1 + unused2 + unused3 + unused4 + unused5; const unusedValueToPlacateAjd = unused1 + unused2 + unused3 + unused4 + unused5;
@ -32,7 +32,7 @@ export function getNextLNode(node: LNode): LNode|null {
export function getChildLNode(node: LNode): LNode|null { export function getChildLNode(node: LNode): LNode|null {
if (node.tNode.child) { if (node.tNode.child) {
const viewData = node.tNode.type === TNodeType.View ? node.data as LViewData : node.view; const viewData = node.tNode.type === TNodeType.View ? node.data as LViewData : node.view;
return viewData[node.tNode.child.index]; return readElementValue(viewData[node.tNode.child.index]);
} }
return null; return null;
} }
@ -50,7 +50,7 @@ export function getParentLNode(node: LNode): LElementNode|LContainerNode|LViewNo
return containerHostIndex === -1 ? null : node.view[containerHostIndex].dynamicLContainerNode; return containerHostIndex === -1 ? null : node.view[containerHostIndex].dynamicLContainerNode;
} }
const parent = node.tNode.parent; const parent = node.tNode.parent;
return parent ? node.view[parent.index] : node.view[HOST_NODE]; return readElementValue(parent ? node.view[parent.index] : node.view[HOST_NODE]);
} }
const enum WalkLNodeTreeAction { const enum WalkLNodeTreeAction {
@ -463,7 +463,7 @@ function removeListeners(viewData: LViewData): void {
for (let i = 0; i < cleanup.length - 1; i += 2) { for (let i = 0; i < cleanup.length - 1; i += 2) {
if (typeof cleanup[i] === 'string') { if (typeof cleanup[i] === 'string') {
// This is a listener with the native renderer // This is a listener with the native renderer
const native = viewData[cleanup[i + 1]].native; const native = readElementValue(viewData[cleanup[i + 1]]).native;
const listener = viewData[CLEANUP] ![cleanup[i + 2]]; const listener = viewData[CLEANUP] ![cleanup[i + 2]];
native.removeEventListener(cleanup[i], listener, cleanup[i + 3]); native.removeEventListener(cleanup[i], listener, cleanup[i + 3]);
i += 2; i += 2;

View File

@ -12,66 +12,81 @@ import {Renderer3, RendererStyleFlags3, isProceduralRenderer} from './interfaces
/** /**
* The styling context acts as a styling manifest (shaped as an array) for determining which * The styling context acts as a styling manifest (shaped as an array) for determining which
* styling properties have been assigned via the provided `updateStyleMap` and `updateStyleProp` * styling properties have been assigned via the provided `updateStylingMap`, `updateStyleProp`
* functions. There are also two initialization functions `allocStylingContext` and * and `updateClassProp` functions. There are also two initialization functions
* `createStylingContextTemplate` which are used to initialize and/or clone the context. * `allocStylingContext` and `createStylingContextTemplate` which are used to initialize
* and/or clone the context.
* *
* The context is an array where the first two cells are used for static data (initial styling) * The context is an array where the first two cells are used for static data (initial styling)
* and dirty flags / index offsets). The remaining set of cells is used for multi (map) and single * and dirty flags / index offsets). The remaining set of cells is used for multi (map) and single
* (prop) style values. * (prop) style values.
* *
* each value from here onwards is mapped as so: * each value from here onwards is mapped as so:
* [i] = mutation/type flag for the style value * [i] = mutation/type flag for the style/class value
* [i + 1] = prop string (or null incase it has been removed) * [i + 1] = prop string (or null incase it has been removed)
* [i + 2] = value string (or null incase it has been removed) * [i + 2] = value string (or null incase it has been removed)
* *
* There are three types of styling types stored in this context: * There are three types of styling types stored in this context:
* initial: any styles that are passed in once the context is created * initial: any styles that are passed in once the context is created
* (these are stored in the first cell of the array and the first * (these are stored in the first cell of the array and the first
* value of this array is always `null` even if no initial styles exist. * value of this array is always `null` even if no initial styling exists.
* the `null` value is there so that any new styles have a parent to point * the `null` value is there so that any new styles have a parent to point
* to. This way we can always assume that there is a parent.) * to. This way we can always assume that there is a parent.)
* *
* single: any styles that are updated using `updateStyleProp` (fixed set) * single: any styles that are updated using `updateStyleProp` or `updateClassProp` (fixed set)
* *
* multi: any styles that are updated using `updateStyleMap` (dynamic set) * multi: any styles that are updated using `updateStylingMap` (dynamic set)
* *
* Note that context is only used to collect style information. Only when `renderStyles` * Note that context is only used to collect style information. Only when `renderStyling`
* is called is when the styling payload will be rendered (or built as a key/value map). * is called is when the styling payload will be rendered (or built as a key/value map).
* *
* When the context is created, depending on what initial styles are passed in, the context itself * When the context is created, depending on what initial styling values are passed in, the
* will be pre-filled with slots based on the initial style properties. Say for example we have a * context itself will be pre-filled with slots based on the initial style properties. Say
* series of initial styles that look like so: * for example we have a series of initial styles that look like so:
* *
* style="width:100px; height:200px;" * style="width:100px; height:200px;"
* class="foo"
* *
* Then the initial state of the context (once initialized) will look like so: * Then the initial state of the context (once initialized) will look like so:
* *
* ``` * ```
* context = [ * context = [
* [null, '100px', '200px'], // property names are not needed since they have already been * [null, '100px', '200px', true], // property names are not needed since they have already been
* written to DOM. * written to DOM.
* *
* 1, // this instructs how many `style` values there are so that class index values can be
* offsetted
*
* configMasterVal, * configMasterVal,
* *
* // 2 * // 3
* 'width', * 'width',
* pointers(1, 8); // Point to static `width`: `100px` and multi `width`. * pointers(1, 12); // Point to static `width`: `100px` and multi `width`.
* null, * null,
* *
* // 5 * // 6
* 'height', * 'height',
* pointers(2, 11); // Point to static `height`: `200px` and multi `height`. * pointers(2, 15); // Point to static `height`: `200px` and multi `height`.
* null, * null,
* *
* // 8 * // 9
* 'foo',
* pointers(1, 18); // Point to static `foo`: `true` and multi `foo`.
* null,
*
* // 12
* 'width', * 'width',
* pointers(1, 2); // Point to static `width`: `100px` and single `width`. * pointers(1, 3); // Point to static `width`: `100px` and single `width`.
* null, * null,
* *
* // 11 * // 15
* 'height', * 'height',
* pointers(2, 5); // Point to static `height`: `200px` and single `height`. * pointers(2, 6); // Point to static `height`: `200px` and single `height`.
* null,
*
* // 18
* 'foo',
* pointers(3, 9); // Point to static `foo`: `true` and single `foo`.
* null, * null,
* ] * ]
* *
@ -82,30 +97,50 @@ import {Renderer3, RendererStyleFlags3, isProceduralRenderer} from './interfaces
* } * }
* ``` * ```
* *
* The values are duplicated so that space is set aside for both multi ([style]) * The values are duplicated so that space is set aside for both multi ([style] and [class])
* and single ([style.prop]) values. The respective config values (configValA, configValB, etc...) * and single ([style.prop] or [class.named]) values. The respective config values
* are a combination of the StylingFlags with two index values: the `initialIndex` (which points to * (configValA, configValB, etc...) are a combination of the StylingFlags with two index
* the index location of the style value in the initial styles array in slot 0) and the * values: the `initialIndex` (which points to the index location of the style value in
* `dynamicIndex` (which points to the matching single/multi index position in the context array * the initial styles array in slot 0) and the `dynamicIndex` (which points to the
* for the same prop). * matching single/multi index position in the context array for the same prop).
* *
* This means that every time `updateStyleProp` is called it must be called using an index value * This means that every time `updateStyleProp` or `updateClassProp` are called then they
* (not a property string) which references the index value of the initial style when the context * must be called using an index value (not a property string) which references the index
* was created. This also means that `updateStyleProp` cannot be called with a new property * value of the initial style prop/class when the context was created. This also means that
* (only `updateStyleMap` can include new CSS properties that will be added to the context). * `updateStyleProp` or `updateClassProp` cannot be called with a new property (only
* `updateStylingMap` can include new CSS properties that will be added to the context).
*/ */
export interface StylingContext extends Array<InitialStyles|number|string|null> { export interface StylingContext extends
Array<InitialStyles|number|string|boolean|LElementNode|null> {
/**
* Location of element that is used as a target for this context.
*/
[0]: LElementNode|null;
/** /**
* Location of initial data shared by all instances of this style. * Location of initial data shared by all instances of this style.
*/ */
[0]: InitialStyles; [1]: InitialStyles;
/** /**
* A numeric value representing the configuration status (whether the context is dirty or not) * A numeric value representing the configuration status (whether the context is dirty or not)
* mixed together (using bit shifting) with a index value which tells the starting index value * mixed together (using bit shifting) with a index value which tells the starting index value
* of where the multi style entries begin. * of where the multi style entries begin.
*/ */
[1]: number; [2]: number;
/**
* A numeric value representing the class index offset value. Whenever a single class is
* applied (using `elementClassProp`) it should have an styling index value that doesn't
* need to take into account any style values that exist in the context.
*/
[3]: number;
/**
* The last CLASS STRING VALUE that was interpreted by elementStylingMap. This is cached
* So that the algorithm can exit early incase the string has not changed.
*/
[4]: string|null;
} }
/** /**
@ -116,7 +151,7 @@ export interface StylingContext extends Array<InitialStyles|number|string|null>
* All other entries in this array are of `string` value and correspond to the values that * All other entries in this array are of `string` value and correspond to the values that
* were extracted from the `style=""` attribute in the HTML code for the provided template. * were extracted from the `style=""` attribute in the HTML code for the provided template.
*/ */
export interface InitialStyles extends Array<string|null> { [0]: null; } export interface InitialStyles extends Array<string|null|boolean> { [0]: null; }
/** /**
* Used to set the context to be dirty or not both on the master flag (position 1) * Used to set the context to be dirty or not both on the master flag (position 1)
@ -124,21 +159,31 @@ export interface InitialStyles extends Array<string|null> { [0]: null; }
*/ */
export const enum StylingFlags { export const enum StylingFlags {
// Implies no configurations // Implies no configurations
None = 0b0, None = 0b00,
// Whether or not the entry or context itself is dirty // Whether or not the entry or context itself is dirty
Dirty = 0b1, Dirty = 0b01,
// Whether or not this is a class-based assignment
Class = 0b10,
// The max amount of bits used to represent these configuration values // The max amount of bits used to represent these configuration values
BitCountSize = 1, BitCountSize = 2,
// There are only two bits here
BitMask = 0b11
} }
/** Used as numeric pointer values to determine what cells to update in the `StylingContext` */ /** Used as numeric pointer values to determine what cells to update in the `StylingContext` */
export const enum StylingIndex { export const enum StylingIndex {
// Position of where the initial styles are stored in the styling context // Position of where the initial styles are stored in the styling context
InitialStylesPosition = 0, ElementPosition = 0,
// Position of where the initial styles are stored in the styling context
InitialStylesPosition = 1,
// Index of location where the start of single properties are stored. (`updateStyleProp`) // Index of location where the start of single properties are stored. (`updateStyleProp`)
MasterFlagPosition = 1, MasterFlagPosition = 2,
// Index of location where the class index offset value is located
ClassOffsetPosition = 3,
// Position of where the last string-based CSS class value was stored
CachedCssClassString = 4,
// Location of single (prop) value entries are stored within the context // Location of single (prop) value entries are stored within the context
SingleStylesStartPosition = 2, SingleStylesStartPosition = 5,
// Multi and single entries are stored in `StylingContext` as: Flag; PropertyName; PropertyValue // Multi and single entries are stored in `StylingContext` as: Flag; PropertyName; PropertyValue
FlagsOffset = 0, FlagsOffset = 0,
PropertyOffset = 1, PropertyOffset = 1,
@ -157,9 +202,12 @@ export const enum StylingIndex {
* A pre-computed template is designed to be computed once for a given element * A pre-computed template is designed to be computed once for a given element
* (instructions.ts has logic for caching this). * (instructions.ts has logic for caching this).
*/ */
export function allocStylingContext(templateStyleContext: StylingContext): StylingContext { export function allocStylingContext(
lElement: LElementNode | null, templateStyleContext: StylingContext): StylingContext {
// each instance gets a copy // each instance gets a copy
return templateStyleContext.slice() as any as StylingContext; const context = templateStyleContext.slice() as any as StylingContext;
context[StylingIndex.ElementPosition] = lElement;
return context;
} }
/** /**
@ -176,38 +224,74 @@ export function allocStylingContext(templateStyleContext: StylingContext): Styli
* -> ['width', 'height', SPECIAL_ENUM_VAL, 'width', '100px'] * -> ['width', 'height', SPECIAL_ENUM_VAL, 'width', '100px']
* This implies that `width` and `height` will be later styled and that the `width` * This implies that `width` and `height` will be later styled and that the `width`
* property has an initial value of `100px`. * property has an initial value of `100px`.
*
* @param initialClassDeclarations a list of class declarations and initial class values
* that are used later within the styling context.
*
* -> ['foo', 'bar', SPECIAL_ENUM_VAL, 'foo', true]
* This implies that `foo` and `bar` will be later styled and that the `foo`
* class will be applied to the element as an initial class since it's true
*/ */
export function createStylingContextTemplate( export function createStylingContextTemplate(
initialStyleDeclarations?: (string | InitialStylingFlags)[] | null): StylingContext { initialStyleDeclarations?: (string | boolean | InitialStylingFlags)[] | null,
const initialStyles: InitialStyles = [null]; initialClassDeclarations?: (string | boolean | InitialStylingFlags)[] | null): StylingContext {
const context: StylingContext = [initialStyles, 0]; const initialStylingValues: InitialStyles = [null];
const context: StylingContext = [null, initialStylingValues, 0, 0, null];
const indexLookup: {[key: string]: number} = {}; // we use two maps since a class name might collide with a CSS style prop
const stylesLookup: {[key: string]: number} = {};
const classesLookup: {[key: string]: number} = {};
let totalStyleDeclarations = 0;
if (initialStyleDeclarations) { if (initialStyleDeclarations) {
let hasPassedDeclarations = false; let hasPassedDeclarations = false;
for (let i = 0; i < initialStyleDeclarations.length; i++) { for (let i = 0; i < initialStyleDeclarations.length; i++) {
const v = initialStyleDeclarations[i] as string | InitialStylingFlags; const v = initialStyleDeclarations[i] as string | InitialStylingFlags;
// this flag value marks where the declarations end the initial values begin // this flag value marks where the declarations end the initial values begin
if (v === InitialStylingFlags.INITIAL_STYLES) { if (v === InitialStylingFlags.VALUES_MODE) {
hasPassedDeclarations = true; hasPassedDeclarations = true;
} else { } else {
const prop = v as string; const prop = v as string;
if (hasPassedDeclarations) { if (hasPassedDeclarations) {
const value = initialStyleDeclarations[++i] as string; const value = initialStyleDeclarations[++i] as string;
initialStyles.push(value); initialStylingValues.push(value);
indexLookup[prop] = initialStyles.length - 1; stylesLookup[prop] = initialStylingValues.length - 1;
} else { } else {
// it's safe to use `0` since the default initial value for totalStyleDeclarations++;
// each property will always be null (which is at position 0) stylesLookup[prop] = 0;
indexLookup[prop] = 0;
} }
} }
} }
} }
const allProps = Object.keys(indexLookup); // make where the class offsets begin
const totalProps = allProps.length; context[StylingIndex.ClassOffsetPosition] = totalStyleDeclarations;
if (initialClassDeclarations) {
let hasPassedDeclarations = false;
for (let i = 0; i < initialClassDeclarations.length; i++) {
const v = initialClassDeclarations[i] as string | boolean | InitialStylingFlags;
// this flag value marks where the declarations end the initial values begin
if (v === InitialStylingFlags.VALUES_MODE) {
hasPassedDeclarations = true;
} else {
const className = v as string;
if (hasPassedDeclarations) {
const value = initialClassDeclarations[++i] as boolean;
initialStylingValues.push(value);
classesLookup[className] = initialStylingValues.length - 1;
} else {
classesLookup[className] = 0;
}
}
}
}
const styleProps = Object.keys(stylesLookup);
const classNames = Object.keys(classesLookup);
const classNamesIndexStart = styleProps.length;
const totalProps = styleProps.length + classNames.length;
// *2 because we are filling for both single and multi style spaces // *2 because we are filling for both single and multi style spaces
const maxLength = totalProps * StylingIndex.Size * 2 + StylingIndex.SingleStylesStartPosition; const maxLength = totalProps * StylingIndex.Size * 2 + StylingIndex.SingleStylesStartPosition;
@ -222,86 +306,140 @@ export function createStylingContextTemplate(
const multiStart = totalProps * StylingIndex.Size + StylingIndex.SingleStylesStartPosition; const multiStart = totalProps * StylingIndex.Size + StylingIndex.SingleStylesStartPosition;
// fill single and multi-level styles // fill single and multi-level styles
for (let i = 0; i < allProps.length; i++) { for (let i = 0; i < totalProps; i++) {
const prop = allProps[i]; const isClassBased = i >= classNamesIndexStart;
const prop = isClassBased ? classNames[i - classNamesIndexStart] : styleProps[i];
const indexForInitial = isClassBased ? classesLookup[prop] : stylesLookup[prop];
const initialValue = initialStylingValues[indexForInitial];
const indexForInitial = indexLookup[prop];
const indexForMulti = i * StylingIndex.Size + multiStart; const indexForMulti = i * StylingIndex.Size + multiStart;
const indexForSingle = i * StylingIndex.Size + singleStart; const indexForSingle = i * StylingIndex.Size + singleStart;
const initialFlag = isClassBased ? StylingFlags.Class : StylingFlags.None;
setFlag(context, indexForSingle, pointers(StylingFlags.None, indexForInitial, indexForMulti)); setFlag(context, indexForSingle, pointers(initialFlag, indexForInitial, indexForMulti));
setProp(context, indexForSingle, prop); setProp(context, indexForSingle, prop);
setValue(context, indexForSingle, null); setValue(context, indexForSingle, null);
setFlag(context, indexForMulti, pointers(StylingFlags.Dirty, indexForInitial, indexForSingle)); const flagForMulti =
initialFlag | (initialValue !== null ? StylingFlags.Dirty : StylingFlags.None);
setFlag(context, indexForMulti, pointers(flagForMulti, indexForInitial, indexForSingle));
setProp(context, indexForMulti, prop); setProp(context, indexForMulti, prop);
setValue(context, indexForMulti, null); setValue(context, indexForMulti, null);
} }
// there is no initial value flag for the master index since it doesn't reference an initial style // there is no initial value flag for the master index since it doesn't
// value // reference an initial style value
setFlag(context, StylingIndex.MasterFlagPosition, pointers(0, 0, multiStart)); setFlag(context, StylingIndex.MasterFlagPosition, pointers(0, 0, multiStart));
setContextDirty(context, initialStyles.length > 1); setContextDirty(context, initialStylingValues.length > 1);
return context; return context;
} }
const EMPTY_ARR: any[] = []; const EMPTY_ARR: any[] = [];
const EMPTY_OBJ: {[key: string]: any} = {};
/** /**
* Sets and resolves all `multi` styles on an `StylingContext` so that they can be * Sets and resolves all `multi` styling on an `StylingContext` so that they can be
* applied to the element once `renderStyles` is called. * applied to the element once `renderStyling` is called.
* *
* All missing styles (any values that are not provided in the new `styles` param) * All missing styles/class (any values that are not provided in the new `styles`
* will resolve to `null` within their respective positions in the context. * or `classes` params) will resolve to `null` within their respective positions
* in the context.
* *
* @param context The styling context that will be updated with the * @param context The styling context that will be updated with the
* newly provided style values. * newly provided style values.
* @param styles The key/value map of CSS styles that will be used for the update. * @param styles The key/value map of CSS styles that will be used for the update.
* @param classes The key/value map of CSS class names that will be used for the update.
*/ */
export function updateStyleMap(context: StylingContext, styles: {[key: string]: any} | null): void { export function updateStylingMap(
const propsToApply = styles ? Object.keys(styles) : EMPTY_ARR; context: StylingContext, styles: {[key: string]: any} | null,
classes?: {[key: string]: any} | string | null): void {
let classNames: string[] = EMPTY_ARR;
let applyAllClasses = false;
let ignoreAllClassUpdates = false;
// each time a string-based value pops up then it shouldn't require a deep
// check of what's changed.
if (typeof classes == 'string') {
const cachedClassString = context[StylingIndex.CachedCssClassString] as string | null;
if (cachedClassString && cachedClassString === classes) {
ignoreAllClassUpdates = true;
} else {
context[StylingIndex.CachedCssClassString] = classes;
classNames = classes.split(/\s+/);
// this boolean is used to avoid having to create a key/value map of `true` values
// since a classname string implies that all those classes are added
applyAllClasses = true;
}
} else {
classNames = classes ? Object.keys(classes) : EMPTY_ARR;
context[StylingIndex.CachedCssClassString] = null;
}
classes = (classes || EMPTY_OBJ) as{[key: string]: any};
const styleProps = styles ? Object.keys(styles) : EMPTY_ARR;
styles = styles || EMPTY_OBJ;
const classesStartIndex = styleProps.length;
const multiStartIndex = getMultiStartIndex(context); const multiStartIndex = getMultiStartIndex(context);
let dirty = false; let dirty = false;
let ctxIndex = multiStartIndex; let ctxIndex = multiStartIndex;
let propIndex = 0; let propIndex = 0;
const propLimit = styleProps.length + classNames.length;
// the main loop here will try and figure out how the shape of the provided // the main loop here will try and figure out how the shape of the provided
// styles differ with respect to the context. Later if the context/styles are // styles differ with respect to the context. Later if the context/styles/classes
// off-balance then they will be dealt in another loop after this one // are off-balance then they will be dealt in another loop after this one
while (ctxIndex < context.length && propIndex < propsToApply.length) { while (ctxIndex < context.length && propIndex < propLimit) {
const flag = getPointers(context, ctxIndex); const isClassBased = propIndex >= classesStartIndex;
const prop = getProp(context, ctxIndex);
const value = getValue(context, ctxIndex);
const newProp = propsToApply[propIndex]; // when there is a cache-hit for a string-based class then we should
const newValue = styles ![newProp]; // avoid doing any work diffing any of the changes
if (prop === newProp) { if (!ignoreAllClassUpdates || !isClassBased) {
if (value !== newValue) { const adjustedPropIndex = isClassBased ? propIndex - classesStartIndex : propIndex;
setValue(context, ctxIndex, newValue); const newProp: string =
const initialValue = getInitialValue(context, flag); isClassBased ? classNames[adjustedPropIndex] : styleProps[adjustedPropIndex];
const newValue: string|boolean =
isClassBased ? (applyAllClasses ? true : classes[newProp]) : styles[newProp];
// there is no point in setting this to dirty if the previously const prop = getProp(context, ctxIndex);
// rendered value was being referenced by the initial style (or null) if (prop === newProp) {
if (initialValue !== newValue) { const value = getValue(context, ctxIndex);
setDirty(context, ctxIndex, true);
dirty = true;
}
}
} else {
const indexOfEntry = findEntryPositionByProp(context, newProp, ctxIndex);
if (indexOfEntry > 0) {
// it was found at a later point ... just swap the values
swapMultiContextEntries(context, ctxIndex, indexOfEntry);
if (value !== newValue) { if (value !== newValue) {
setValue(context, ctxIndex, newValue); setValue(context, ctxIndex, newValue);
dirty = true;
const flag = getPointers(context, ctxIndex);
const initialValue = getInitialValue(context, flag);
// there is no point in setting this to dirty if the previously
// rendered value was being referenced by the initial style (or null)
if (initialValue !== newValue) {
setDirty(context, ctxIndex, true);
dirty = true;
}
} }
} else { } else {
// we only care to do this if the insertion is in the middle const indexOfEntry = findEntryPositionByProp(context, newProp, ctxIndex);
const doShift = ctxIndex < context.length; if (indexOfEntry > 0) {
insertNewMultiProperty(context, ctxIndex, newProp, newValue); // it was found at a later point ... just swap the values
dirty = true; const valueToCompare = getValue(context, indexOfEntry);
const flagToCompare = getPointers(context, indexOfEntry);
swapMultiContextEntries(context, ctxIndex, indexOfEntry);
if (valueToCompare !== newValue) {
const initialValue = getInitialValue(context, flagToCompare);
setValue(context, ctxIndex, newValue);
if (initialValue !== newValue) {
setDirty(context, ctxIndex, true);
dirty = true;
}
}
} else {
// we only care to do this if the insertion is in the middle
insertNewMultiProperty(context, ctxIndex, isClassBased, newProp, newValue);
dirty = true;
}
} }
} }
@ -310,11 +448,16 @@ export function updateStyleMap(context: StylingContext, styles: {[key: string]:
} }
// this means that there are left-over values in the context that // this means that there are left-over values in the context that
// were not included in the provided styles and in this case the // were not included in the provided styles/classes and in this
// goal is to "remove" them from the context (by nullifying) // case the goal is to "remove" them from the context (by nullifying)
while (ctxIndex < context.length) { while (ctxIndex < context.length) {
const value = context[ctxIndex + StylingIndex.ValueOffset]; const flag = getPointers(context, ctxIndex);
if (value !== null) { const isClassBased = (flag & StylingFlags.Class) === StylingFlags.Class;
if (ignoreAllClassUpdates && isClassBased) break;
const value = getValue(context, ctxIndex);
const doRemoveValue = valueExists(value, isClassBased);
if (doRemoveValue) {
setDirty(context, ctxIndex, true); setDirty(context, ctxIndex, true);
setValue(context, ctxIndex, null); setValue(context, ctxIndex, null);
dirty = true; dirty = true;
@ -322,13 +465,19 @@ export function updateStyleMap(context: StylingContext, styles: {[key: string]:
ctxIndex += StylingIndex.Size; ctxIndex += StylingIndex.Size;
} }
// this means that there are left-over property in the context that // this means that there are left-over properties in the context that
// were not detected in the context during the loop above. In that // were not detected in the context during the loop above. In that
// case we want to add the new entries into the list // case we want to add the new entries into the list
while (propIndex < propsToApply.length) { while (propIndex < propLimit) {
const prop = propsToApply[propIndex]; const isClassBased = propIndex >= classesStartIndex;
const value = styles ![prop]; if (ignoreAllClassUpdates && isClassBased) break;
context.push(StylingFlags.Dirty, prop, value);
const adjustedPropIndex = isClassBased ? propIndex - classesStartIndex : propIndex;
const prop = isClassBased ? classNames[adjustedPropIndex] : styleProps[adjustedPropIndex];
const value: string|boolean =
isClassBased ? (applyAllClasses ? true : classes[prop]) : styles[prop];
const flag = StylingFlags.Dirty | (isClassBased ? StylingFlags.Class : StylingFlags.None);
context.push(flag, prop, value);
propIndex++; propIndex++;
dirty = true; dirty = true;
} }
@ -339,13 +488,13 @@ export function updateStyleMap(context: StylingContext, styles: {[key: string]:
} }
/** /**
* Sets and resolves a single CSS style on a property on an `StylingContext` so that they * Sets and resolves a single styling property/value on the provided `StylingContext` so
* can be applied to the element once `renderElementStyles` is called. * that they can be applied to the element once `renderStyling` is called.
* *
* Note that prop-level styles are considered higher priority than styles that are applied * Note that prop-level styling values are considered higher priority than any styling that
* using `updateStyleMap`, therefore, when styles are rendered then any styles that * has been applied using `updateStylingMap`, therefore, when styling values are rendered
* have been applied using this function will be considered first (then multi values second * then any styles/classes that have been applied using this function will be considered first
* and then initial values as a backup). * (then multi values second and then initial values as a backup).
* *
* @param context The styling context that will be updated with the * @param context The styling context that will be updated with the
* newly provided style value. * newly provided style value.
@ -353,7 +502,7 @@ export function updateStyleMap(context: StylingContext, styles: {[key: string]:
* @param value The CSS style value that will be assigned * @param value The CSS style value that will be assigned
*/ */
export function updateStyleProp( export function updateStyleProp(
context: StylingContext, index: number, value: string | null): void { context: StylingContext, index: number, value: string | boolean | null): void {
const singleIndex = StylingIndex.SingleStylesStartPosition + index * StylingIndex.Size; const singleIndex = StylingIndex.SingleStylesStartPosition + index * StylingIndex.Size;
const currValue = getValue(context, singleIndex); const currValue = getValue(context, singleIndex);
const currFlag = getPointers(context, singleIndex); const currFlag = getPointers(context, singleIndex);
@ -370,8 +519,10 @@ export function updateStyleProp(
let multiDirty = false; let multiDirty = false;
let singleDirty = true; let singleDirty = true;
const isClassBased = (currFlag & StylingFlags.Class) === StylingFlags.Class;
// only when the value is set to `null` should the multi-value get flagged // only when the value is set to `null` should the multi-value get flagged
if (value == null && valueForMulti) { if (!valueExists(value, isClassBased) && valueExists(valueForMulti, isClassBased)) {
multiDirty = true; multiDirty = true;
singleDirty = false; singleDirty = false;
} }
@ -384,12 +535,28 @@ export function updateStyleProp(
} }
/** /**
* Renders all queued styles using a renderer onto the given element. * This method will toggle the referenced CSS class (by the provided index)
* within the given context.
*
* @param context The styling context that will be updated with the
* newly provided class value.
* @param index The index of the CSS class which is being updated.
* @param addOrRemove Whether or not to add or remove the CSS class
*/
export function updateClassProp(
context: StylingContext, index: number, addOrRemove: boolean): void {
const adjustedIndex = index + context[StylingIndex.ClassOffsetPosition];
updateStyleProp(context, adjustedIndex, addOrRemove);
}
/**
* Renders all queued styling using a renderer onto the given element.
* *
* This function works by rendering any styles (that have been applied * This function works by rendering any styles (that have been applied
* using `updateStyleMap` and `updateStyleProp`) onto the * using `updateStylingMap`) and any classes (that have been applied using
* provided element using the provided renderer. Just before the styles * `updateStyleProp`) onto the provided element using the provided renderer.
* are rendered a final key/value style map will be assembled. * Just before the styles/classes are rendered a final key/value style map
* will be assembled (if `styleStore` or `classStore` are provided).
* *
* @param lElement the element that the styles will be rendered on * @param lElement the element that the styles will be rendered on
* @param context The styling context that will be used to determine * @param context The styling context that will be used to determine
@ -397,13 +564,14 @@ export function updateStyleProp(
* @param renderer the renderer that will be used to apply the styling * @param renderer the renderer that will be used to apply the styling
* @param styleStore if provided, the updated style values will be applied * @param styleStore if provided, the updated style values will be applied
* to this key/value map instead of being renderered via the renderer. * to this key/value map instead of being renderered via the renderer.
* @returns an object literal. `{ color: 'red', height: 'auto'}`. * @param classStore if provided, the updated class values will be applied
* to this key/value map instead of being renderered via the renderer.
*/ */
export function renderStyles( export function renderStyling(
lElement: LElementNode, context: StylingContext, renderer: Renderer3, context: StylingContext, renderer: Renderer3, styleStore?: {[key: string]: any},
styleStore?: {[key: string]: any}) { classStore?: {[key: string]: boolean}) {
if (isContextDirty(context)) { if (isContextDirty(context)) {
const native = lElement.native; const native = context[StylingIndex.ElementPosition] !.native;
const multiStartIndex = getMultiStartIndex(context); const multiStartIndex = getMultiStartIndex(context);
for (let i = StylingIndex.SingleStylesStartPosition; i < context.length; for (let i = StylingIndex.SingleStylesStartPosition; i < context.length;
i += StylingIndex.Size) { i += StylingIndex.Size) {
@ -412,27 +580,35 @@ export function renderStyles(
const prop = getProp(context, i); const prop = getProp(context, i);
const value = getValue(context, i); const value = getValue(context, i);
const flag = getPointers(context, i); const flag = getPointers(context, i);
const isClassBased = flag & StylingFlags.Class ? true : false;
const isInSingleRegion = i < multiStartIndex; const isInSingleRegion = i < multiStartIndex;
let styleToApply: string|null = value; let valueToApply: string|boolean|null = value;
// STYLE DEFER CASE 1: Use a multi value instead of a null single value // VALUE DEFER CASE 1: Use a multi value instead of a null single value
// this check implies that a single value was removed and we // this check implies that a single value was removed and we
// should now defer to a multi value and use that (if set). // should now defer to a multi value and use that (if set).
if (isInSingleRegion && styleToApply == null) { if (isInSingleRegion && !valueExists(valueToApply, isClassBased)) {
// single values ALWAYS have a reference to a multi index // single values ALWAYS have a reference to a multi index
const multiIndex = getMultiOrSingleIndex(flag); const multiIndex = getMultiOrSingleIndex(flag);
styleToApply = getValue(context, multiIndex); valueToApply = getValue(context, multiIndex);
} }
// STYLE DEFER CASE 2: Use the initial value if all else fails (is null) // VALUE DEFER CASE 2: Use the initial value if all else fails (is falsy)
// the initial value will always be a string or null, // the initial value will always be a string or null,
// therefore we can safely adopt it incase there's nothing else // therefore we can safely adopt it incase there's nothing else
if (styleToApply == null) { // note that this should always be a falsy check since `false` is used
styleToApply = getInitialValue(context, flag); // for both class and style comparisons (styles can't be false and false
// classes are turned off and should therefore defer to their initial values)
if (!valueExists(valueToApply, isClassBased)) {
valueToApply = getInitialValue(context, flag);
} }
setStyle(native, prop, styleToApply, renderer, styleStore); if (isClassBased) {
setClass(native, prop, valueToApply ? true : false, renderer, classStore);
} else {
setStyle(native, prop, valueToApply as string | null, renderer, styleStore);
}
setDirty(context, i, false); setDirty(context, i, false);
} }
} }
@ -443,7 +619,7 @@ export function renderStyles(
/** /**
* This function renders a given CSS prop/value entry using the * This function renders a given CSS prop/value entry using the
* provided renderer. If a `styleStore` value is provided then * provided renderer. If a `store` value is provided then
* that will be used a render context instead of the provided * that will be used a render context instead of the provided
* renderer. * renderer.
* *
@ -451,23 +627,51 @@ export function renderStyles(
* @param prop the CSS style property that will be rendered * @param prop the CSS style property that will be rendered
* @param value the CSS style value that will be rendered * @param value the CSS style value that will be rendered
* @param renderer * @param renderer
* @param styleStore an optional key/value map that will be used as a context to render styles on * @param store an optional key/value map that will be used as a context to render styles on
*/ */
function setStyle( function setStyle(
native: any, prop: string, value: string | null, renderer: Renderer3, native: any, prop: string, value: string | null, renderer: Renderer3,
styleStore?: {[key: string]: any}) { store?: {[key: string]: any}) {
if (styleStore) { if (store) {
styleStore[prop] = value; store[prop] = value;
} else if (value == null) { } else if (value) {
ngDevMode && ngDevMode.rendererRemoveStyle++;
isProceduralRenderer(renderer) ?
renderer.removeStyle(native, prop, RendererStyleFlags3.DashCase) :
native['style'].removeProperty(prop);
} else {
ngDevMode && ngDevMode.rendererSetStyle++; ngDevMode && ngDevMode.rendererSetStyle++;
isProceduralRenderer(renderer) ? isProceduralRenderer(renderer) ?
renderer.setStyle(native, prop, value, RendererStyleFlags3.DashCase) : renderer.setStyle(native, prop, value, RendererStyleFlags3.DashCase) :
native['style'].setProperty(prop, value); native['style'].setProperty(prop, value);
} else {
ngDevMode && ngDevMode.rendererRemoveStyle++;
isProceduralRenderer(renderer) ?
renderer.removeStyle(native, prop, RendererStyleFlags3.DashCase) :
native['style'].removeProperty(prop);
}
}
/**
* This function renders a given CSS class value using the provided
* renderer (by adding or removing it from the provided element).
* If a `store` value is provided then that will be used a render
* context instead of the provided renderer.
*
* @param native the DOM Element
* @param prop the CSS style property that will be rendered
* @param value the CSS style value that will be rendered
* @param renderer
* @param store an optional key/value map that will be used as a context to render styles on
*/
function setClass(
native: any, className: string, add: boolean, renderer: Renderer3,
store?: {[key: string]: boolean}) {
if (store) {
store[className] = add;
} else if (add) {
ngDevMode && ngDevMode.rendererAddClass++;
isProceduralRenderer(renderer) ? renderer.addClass(native, className) :
native['classList'].add(className);
} else {
ngDevMode && ngDevMode.rendererRemoveClass++;
isProceduralRenderer(renderer) ? renderer.removeClass(native, className) :
native['classList'].remove(className);
} }
} }
@ -487,8 +691,14 @@ function isDirty(context: StylingContext, index: number): boolean {
return ((context[adjustedIndex] as number) & StylingFlags.Dirty) == StylingFlags.Dirty; return ((context[adjustedIndex] as number) & StylingFlags.Dirty) == StylingFlags.Dirty;
} }
function isClassBased(context: StylingContext, index: number): boolean {
const adjustedIndex =
index >= StylingIndex.SingleStylesStartPosition ? (index + StylingIndex.FlagsOffset) : index;
return ((context[adjustedIndex] as number) & StylingFlags.Class) == StylingFlags.Class;
}
function pointers(configFlag: number, staticIndex: number, dynamicIndex: number) { function pointers(configFlag: number, staticIndex: number, dynamicIndex: number) {
return (configFlag & StylingFlags.Dirty) | (staticIndex << StylingFlags.BitCountSize) | return (configFlag & StylingFlags.BitMask) | (staticIndex << StylingFlags.BitCountSize) |
(dynamicIndex << (StylingIndex.BitCountSize + StylingFlags.BitCountSize)); (dynamicIndex << (StylingIndex.BitCountSize + StylingFlags.BitCountSize));
} }
@ -515,7 +725,7 @@ function setProp(context: StylingContext, index: number, prop: string) {
context[index + StylingIndex.PropertyOffset] = prop; context[index + StylingIndex.PropertyOffset] = prop;
} }
function setValue(context: StylingContext, index: number, value: string | null) { function setValue(context: StylingContext, index: number, value: string | null | boolean) {
context[index + StylingIndex.ValueOffset] = value; context[index + StylingIndex.ValueOffset] = value;
} }
@ -531,8 +741,8 @@ function getPointers(context: StylingContext, index: number): number {
return context[adjustedIndex] as number; return context[adjustedIndex] as number;
} }
function getValue(context: StylingContext, index: number): string|null { function getValue(context: StylingContext, index: number): string|boolean|null {
return context[index + StylingIndex.ValueOffset] as string | null; return context[index + StylingIndex.ValueOffset] as string | boolean | null;
} }
function getProp(context: StylingContext, index: number): string { function getProp(context: StylingContext, index: number): string {
@ -597,20 +807,23 @@ function updateSinglePointerValues(context: StylingContext, indexStartPosition:
if (singleIndex > 0) { if (singleIndex > 0) {
const singleFlag = getPointers(context, singleIndex); const singleFlag = getPointers(context, singleIndex);
const initialIndexForSingle = getInitialIndex(singleFlag); const initialIndexForSingle = getInitialIndex(singleFlag);
const updatedFlag = pointers( const flagValue = (isDirty(context, singleIndex) ? StylingFlags.Dirty : StylingFlags.None) |
isDirty(context, singleIndex) ? StylingFlags.Dirty : StylingFlags.None, (isClassBased(context, singleIndex) ? StylingFlags.Class : StylingFlags.None);
initialIndexForSingle, i); const updatedFlag = pointers(flagValue, initialIndexForSingle, i);
setFlag(context, singleIndex, updatedFlag); setFlag(context, singleIndex, updatedFlag);
} }
} }
} }
function insertNewMultiProperty( function insertNewMultiProperty(
context: StylingContext, index: number, name: string, value: string): void { context: StylingContext, index: number, classBased: boolean, name: string,
value: string | boolean): void {
const doShift = index < context.length; const doShift = index < context.length;
// prop does not exist in the list, add it in // prop does not exist in the list, add it in
context.splice(index, 0, StylingFlags.Dirty, name, value); context.splice(
index, 0, StylingFlags.Dirty | (classBased ? StylingFlags.Class : StylingFlags.None), name,
value);
if (doShift) { if (doShift) {
// because the value was inserted midway into the array then we // because the value was inserted midway into the array then we
@ -619,3 +832,10 @@ function insertNewMultiProperty(
updateSinglePointerValues(context, index + StylingIndex.Size); updateSinglePointerValues(context, index + StylingIndex.Size);
} }
} }
function valueExists(value: string | null | boolean, isClassBased?: boolean) {
if (isClassBased) {
return value ? true : false;
}
return value !== null;
}

View File

@ -5,6 +5,10 @@
* Use of this source code is governed by an MIT-style license that can be * Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {assertLessThan} from './assert';
import {LElementNode} from './interfaces/node';
import {HEADER_OFFSET, LViewData} from './interfaces/view';
/** /**
* Must use this method for CD (instead of === ) since NaN !== NaN * Must use this method for CD (instead of === ) since NaN !== NaN
@ -56,3 +60,27 @@ export function flatten(list: any[]): any[] {
return result; return result;
} }
/** Retrieves a value from any `LViewData`. */
export function loadInternal<T>(index: number, arr: LViewData): T {
ngDevMode && assertDataInRangeInternal(index + HEADER_OFFSET, arr);
return arr[index + HEADER_OFFSET];
}
export function assertDataInRangeInternal(index: number, arr: any[]) {
assertLessThan(index, arr ? arr.length : 0, 'index expected to be a valid data index');
}
/** Retrieves an element value from the provided `viewData`.
*
* Elements that are read may be wrapped in a style context,
* therefore reading the value may involve unwrapping that.
*/
export function loadElementInternal(index: number, arr: LViewData): LElementNode {
const value = loadInternal<LElementNode>(index, arr);
return readElementValue(value);
}
export function readElementValue(value: LElementNode | any[]): LElementNode {
return (Array.isArray(value) ? (value as any as any[])[0] : value) as LElementNode;
}

View File

@ -191,6 +191,9 @@
{ {
"name": "namespaceHTML" "name": "namespaceHTML"
}, },
{
"name": "readElementValue"
},
{ {
"name": "refreshChildComponents" "name": "refreshChildComponents"
}, },

View File

@ -119,6 +119,9 @@
{ {
"name": "RecordViewTuple" "name": "RecordViewTuple"
}, },
{
"name": "RendererStyleFlags3"
},
{ {
"name": "SANITIZER" "name": "SANITIZER"
}, },
@ -206,6 +209,15 @@
{ {
"name": "_c16" "name": "_c16"
}, },
{
"name": "_c17"
},
{
"name": "_c18"
},
{
"name": "_c19"
},
{ {
"name": "_c2" "name": "_c2"
}, },
@ -260,6 +272,9 @@
{ {
"name": "addToViewTree" "name": "addToViewTree"
}, },
{
"name": "allocStylingContext"
},
{ {
"name": "appendChild" "name": "appendChild"
}, },
@ -332,6 +347,9 @@
{ {
"name": "createRootContext" "name": "createRootContext"
}, },
{
"name": "createStylingContextTemplate"
},
{ {
"name": "createTNode" "name": "createTNode"
}, },
@ -344,6 +362,9 @@
{ {
"name": "createViewQuery" "name": "createViewQuery"
}, },
{
"name": "currentElementNode"
},
{ {
"name": "defineComponent" "name": "defineComponent"
}, },
@ -378,7 +399,7 @@
"name": "domRendererFactory3" "name": "domRendererFactory3"
}, },
{ {
"name": "elementClassNamed" "name": "elementClassProp"
}, },
{ {
"name": "elementEnd" "name": "elementEnd"
@ -389,6 +410,12 @@
{ {
"name": "elementStart" "name": "elementStart"
}, },
{
"name": "elementStyling"
},
{
"name": "elementStylingApply"
},
{ {
"name": "enterView" "name": "enterView"
}, },
@ -446,9 +473,21 @@
{ {
"name": "getCurrentSanitizer" "name": "getCurrentSanitizer"
}, },
{
"name": "getInitialIndex"
},
{
"name": "getInitialValue"
},
{ {
"name": "getLViewChild" "name": "getLViewChild"
}, },
{
"name": "getMultiOrSingleIndex"
},
{
"name": "getMultiStartIndex"
},
{ {
"name": "getNextLNode" "name": "getNextLNode"
}, },
@ -479,12 +518,18 @@
{ {
"name": "getParentState" "name": "getParentState"
}, },
{
"name": "getPointers"
},
{ {
"name": "getPreviousIndex" "name": "getPreviousIndex"
}, },
{ {
"name": "getPreviousOrParentNode" "name": "getPreviousOrParentNode"
}, },
{
"name": "getProp"
},
{ {
"name": "getRenderFlags" "name": "getRenderFlags"
}, },
@ -494,6 +539,9 @@
{ {
"name": "getRootView" "name": "getRootView"
}, },
{
"name": "getStylingContext"
},
{ {
"name": "getSymbolIterator" "name": "getSymbolIterator"
}, },
@ -506,6 +554,9 @@
{ {
"name": "getTypeNameForDebugging$1" "name": "getTypeNameForDebugging$1"
}, },
{
"name": "getValue"
},
{ {
"name": "hostElement" "name": "hostElement"
}, },
@ -536,6 +587,9 @@
{ {
"name": "invertObject" "name": "invertObject"
}, },
{
"name": "isContextDirty"
},
{ {
"name": "isCssClassMatching" "name": "isCssClassMatching"
}, },
@ -545,6 +599,9 @@
{ {
"name": "isDifferent" "name": "isDifferent"
}, },
{
"name": "isDirty"
},
{ {
"name": "isJsObject" "name": "isJsObject"
}, },
@ -575,6 +632,12 @@
{ {
"name": "load" "name": "load"
}, },
{
"name": "loadElement"
},
{
"name": "loadElementInternal"
},
{ {
"name": "loadInternal" "name": "loadInternal"
}, },
@ -602,6 +665,9 @@
{ {
"name": "notImplemented" "name": "notImplemented"
}, },
{
"name": "pointers"
},
{ {
"name": "projectionNodeStack" "name": "projectionNodeStack"
}, },
@ -626,6 +692,9 @@
{ {
"name": "queueViewHooks" "name": "queueViewHooks"
}, },
{
"name": "readElementValue"
},
{ {
"name": "refreshChildComponents" "name": "refreshChildComponents"
}, },
@ -653,6 +722,9 @@
{ {
"name": "renderEmbeddedTemplate" "name": "renderEmbeddedTemplate"
}, },
{
"name": "renderStyling"
},
{ {
"name": "resetApplicationState" "name": "resetApplicationState"
}, },
@ -677,9 +749,21 @@
{ {
"name": "searchMatchesQueuedForCreation" "name": "searchMatchesQueuedForCreation"
}, },
{
"name": "setClass"
},
{
"name": "setContextDirty"
},
{ {
"name": "setCurrentInjector" "name": "setCurrentInjector"
}, },
{
"name": "setDirty"
},
{
"name": "setFlag"
},
{ {
"name": "setHostBindings" "name": "setHostBindings"
}, },
@ -689,9 +773,18 @@
{ {
"name": "setInputsFromAttrs" "name": "setInputsFromAttrs"
}, },
{
"name": "setProp"
},
{
"name": "setStyle"
},
{ {
"name": "setUpAttributes" "name": "setUpAttributes"
}, },
{
"name": "setValue"
},
{ {
"name": "storeCleanupFn" "name": "storeCleanupFn"
}, },
@ -725,9 +818,18 @@
{ {
"name": "trackByIdentity" "name": "trackByIdentity"
}, },
{
"name": "updateClassProp"
},
{
"name": "updateStyleProp"
},
{ {
"name": "updateViewQuery" "name": "updateViewQuery"
}, },
{
"name": "valueExists"
},
{ {
"name": "viewAttached" "name": "viewAttached"
}, },

View File

@ -15,7 +15,6 @@ import {ComponentDefInternal, InitialStylingFlags} from '../../../src/render3/in
import {ComponentFixture, renderComponent, toHtml} from '../render_util'; import {ComponentFixture, renderComponent, toHtml} from '../render_util';
/// See: `normative.md` /// See: `normative.md`
describe('elements', () => { describe('elements', () => {
// Saving type as $any$, etc to simplify testing for compiler, as types aren't saved // Saving type as $any$, etc to simplify testing for compiler, as types aren't saved
@ -271,6 +270,7 @@ describe('elements', () => {
}); });
it('should bind to a specific class', () => { it('should bind to a specific class', () => {
const c1: (string | InitialStylingFlags | boolean)[] = ['foo'];
type $MyComponent$ = MyComponent; type $MyComponent$ = MyComponent;
@Component({selector: 'my-component', template: `<div [class.foo]="someFlag"></div>`}) @Component({selector: 'my-component', template: `<div [class.foo]="someFlag"></div>`})
@ -283,10 +283,13 @@ describe('elements', () => {
factory: function MyComponent_Factory() { return new MyComponent(); }, factory: function MyComponent_Factory() { return new MyComponent(); },
template: function MyComponent_Template(rf: $RenderFlags$, ctx: $MyComponent$) { template: function MyComponent_Template(rf: $RenderFlags$, ctx: $MyComponent$) {
if (rf & 1) { if (rf & 1) {
$r3$.ɵEe(0, 'div'); $r3$.ɵE(0, 'div');
$r3$.ɵs(null, c1);
$r3$.ɵe();
} }
if (rf & 2) { if (rf & 2) {
$r3$.ɵkn(0, 'foo', $r3$.ɵb(ctx.someFlag)); $r3$.ɵcp(0, 0, ctx.someFlag);
$r3$.ɵsa(0);
} }
} }
}); });
@ -320,13 +323,13 @@ describe('elements', () => {
template: function MyComponent_Template(rf: $RenderFlags$, ctx: $MyComponent$) { template: function MyComponent_Template(rf: $RenderFlags$, ctx: $MyComponent$) {
if (rf & 1) { if (rf & 1) {
$r3$.ɵE(0, 'div'); $r3$.ɵE(0, 'div');
$r3$.ɵs(1, c0); $r3$.ɵs(c0);
$r3$.ɵe(); $r3$.ɵe();
} }
if (rf & 2) { if (rf & 2) {
$r3$.ɵsp(1, 0, ctx.someColor); $r3$.ɵsp(0, 0, ctx.someColor);
$r3$.ɵsp(1, 1, ctx.someWidth, 'px'); $r3$.ɵsp(0, 1, ctx.someWidth, 'px');
$r3$.ɵsa(1); $r3$.ɵsa(0);
} }
} }
}); });
@ -353,7 +356,9 @@ describe('elements', () => {
it('should bind to many and keep order', () => { it('should bind to many and keep order', () => {
type $MyComponent$ = MyComponent; type $MyComponent$ = MyComponent;
const c0 = ['color', InitialStylingFlags.INITIAL_STYLES, 'color', 'red']; const c0 = ['color', InitialStylingFlags.VALUES_MODE, 'color', 'red'];
const c1 = ['foo'];
@Component({ @Component({
selector: 'my-component', selector: 'my-component',
template: template:
@ -369,12 +374,13 @@ describe('elements', () => {
template: function MyComponent_Template(rf: $RenderFlags$, ctx: $MyComponent$) { template: function MyComponent_Template(rf: $RenderFlags$, ctx: $MyComponent$) {
if (rf & 1) { if (rf & 1) {
$r3$.ɵE(0, 'div'); $r3$.ɵE(0, 'div');
$r3$.ɵs(1, c0); $r3$.ɵs(c0, c1);
$r3$.ɵe(); $r3$.ɵe();
} }
if (rf & 2) { if (rf & 2) {
$r3$.ɵp(0, 'id', $r3$.ɵb(ctx.someString + 1)); $r3$.ɵp(0, 'id', $r3$.ɵb(ctx.someString + 1));
$r3$.ɵkn(0, 'foo', $r3$.ɵb(ctx.someString == 'initial')); $r3$.ɵcp(0, 0, ctx.someString == 'initial');
$r3$.ɵsa(0);
} }
} }
}); });
@ -406,13 +412,12 @@ describe('elements', () => {
template: function StyleComponent_Template(rf: $RenderFlags$, ctx: $StyleComponent$) { template: function StyleComponent_Template(rf: $RenderFlags$, ctx: $StyleComponent$) {
if (rf & 1) { if (rf & 1) {
$r3$.ɵE(0, 'div'); $r3$.ɵE(0, 'div');
$r3$.ɵs(1); $r3$.ɵs();
$r3$.ɵe(); $r3$.ɵe();
} }
if (rf & 2) { if (rf & 2) {
$r3$.ɵk(0, $r3$.ɵb(ctx.classExp)); $r3$.ɵsm(0, ctx.styleExp, ctx.classExp);
$r3$.ɵsm(1, ctx.styleExp); $r3$.ɵsa(0);
$r3$.ɵsa(1);
} }
} }
}); });

View File

@ -44,17 +44,17 @@ describe('compiler sanitization', () => {
template: function MyComponent_Template(rf: $RenderFlags$, ctx: $MyComponent$) { template: function MyComponent_Template(rf: $RenderFlags$, ctx: $MyComponent$) {
if (rf & 1) { if (rf & 1) {
$r3$.ɵE(0, 'div'); $r3$.ɵE(0, 'div');
$r3$.ɵs(1, ['background-image']); $r3$.ɵs(['background-image']);
$r3$.ɵe(); $r3$.ɵe();
$r3$.ɵEe(2, 'img'); $r3$.ɵEe(1, 'img');
} }
if (rf & 2) { if (rf & 2) {
$r3$.ɵp(0, 'innerHTML', $r3$.ɵb(ctx.innerHTML), $r3$.ɵsanitizeHtml); $r3$.ɵp(0, 'innerHTML', $r3$.ɵb(ctx.innerHTML), $r3$.ɵsanitizeHtml);
$r3$.ɵp(0, 'hidden', $r3$.ɵb(ctx.hidden)); $r3$.ɵp(0, 'hidden', $r3$.ɵb(ctx.hidden));
$r3$.ɵsp(1, 0, ctx.style, $r3$.ɵsanitizeStyle); $r3$.ɵsp(0, 0, ctx.style, $r3$.ɵsanitizeStyle);
$r3$.ɵsa(1); $r3$.ɵsa(0);
$r3$.ɵp(2, 'src', $r3$.ɵb(ctx.url), $r3$.ɵsanitizeUrl); $r3$.ɵp(1, 'src', $r3$.ɵb(ctx.url), $r3$.ɵsanitizeUrl);
$r3$.ɵa(2, 'srcset', $r3$.ɵb(ctx.url), $r3$.ɵsanitizeUrl); $r3$.ɵa(1, 'srcset', $r3$.ɵb(ctx.url), $r3$.ɵsanitizeUrl);
} }
} }
}); });

View File

@ -7,8 +7,9 @@
*/ */
import {defineComponent, defineDirective} from '../../src/render3/index'; import {defineComponent, defineDirective} from '../../src/render3/index';
import {bind, container, containerRefreshEnd, containerRefreshStart, elementAttribute, elementClassNamed, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, load, text, textBinding} from '../../src/render3/instructions'; import {bind, container, containerRefreshEnd, containerRefreshStart, elementAttribute, elementClassProp, elementEnd, elementProperty, elementStart, elementStyling, elementStylingApply, embeddedViewEnd, embeddedViewStart, load, text, textBinding} from '../../src/render3/instructions';
import {RenderFlags} from '../../src/render3/interfaces/definition'; import {InitialStylingFlags, RenderFlags} from '../../src/render3/interfaces/definition';
import {ComponentFixture, createComponent, renderToHtml} from './render_util'; import {ComponentFixture, createComponent, renderToHtml} from './render_util';
describe('exports', () => { describe('exports', () => {
@ -212,13 +213,15 @@ describe('exports', () => {
function Template(rf: RenderFlags, ctx: any) { function Template(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {
elementStart(0, 'div'); elementStart(0, 'div');
elementStyling(null, [InitialStylingFlags.VALUES_MODE, 'red', true]);
elementEnd(); elementEnd();
elementStart(1, 'input', ['type', 'checkbox', 'checked', 'true'], ['myInput', '']); elementStart(1, 'input', ['type', 'checkbox', 'checked', 'true'], ['myInput', '']);
elementEnd(); elementEnd();
} }
const tmp = load(2) as any; const tmp = load(2) as any;
if (rf & RenderFlags.Update) { if (rf & RenderFlags.Update) {
elementClassNamed(0, 'red', bind(tmp.checked)); elementClassProp(0, 0, tmp.checked);
elementStylingApply(0);
} }
} }

View File

@ -10,7 +10,7 @@ import {NgForOfContext} from '@angular/common';
import {RenderFlags, directiveInject} from '../../src/render3'; import {RenderFlags, directiveInject} from '../../src/render3';
import {defineComponent} from '../../src/render3/definition'; import {defineComponent} from '../../src/render3/definition';
import {bind, container, element, elementAttribute, elementClass, elementEnd, elementProperty, elementStart, elementStyle, elementStyleProp, elementStyling, elementStylingApply, interpolation1, renderTemplate, text, textBinding} from '../../src/render3/instructions'; import {bind, container, element, elementAttribute, elementEnd, elementProperty, elementStart, elementStyleProp, elementStyling, elementStylingApply, elementStylingMap, interpolation1, renderTemplate, text, textBinding} from '../../src/render3/instructions';
import {InitialStylingFlags} from '../../src/render3/interfaces/definition'; import {InitialStylingFlags} from '../../src/render3/interfaces/definition';
import {AttributeMarker, LElementNode, LNode} from '../../src/render3/interfaces/node'; import {AttributeMarker, LElementNode, LNode} from '../../src/render3/interfaces/node';
import {RElement, domRendererFactory3} from '../../src/render3/interfaces/renderer'; import {RElement, domRendererFactory3} from '../../src/render3/interfaces/renderer';
@ -23,13 +23,13 @@ import {ComponentFixture, TemplateFixture} from './render_util';
describe('instructions', () => { describe('instructions', () => {
function createAnchor() { function createAnchor() {
elementStart(0, 'a'); elementStart(0, 'a');
elementStyling(1); elementStyling();
elementEnd(); elementEnd();
} }
function createDiv(initialStyles?: (string | number)[]) { function createDiv(initialStyles?: (string | number)[]) {
elementStart(0, 'div'); elementStart(0, 'div');
elementStyling(1, initialStyles && Array.isArray(initialStyles) ? initialStyles : null); elementStyling(initialStyles && Array.isArray(initialStyles) ? initialStyles : null);
elementEnd(); elementEnd();
} }
@ -193,15 +193,15 @@ describe('instructions', () => {
it('should use sanitizer function', () => { it('should use sanitizer function', () => {
const t = new TemplateFixture(() => { return createDiv(['background-image']); }); const t = new TemplateFixture(() => { return createDiv(['background-image']); });
t.update(() => { t.update(() => {
elementStyleProp(1, 0, 'url("http://server")', sanitizeStyle); elementStyleProp(0, 0, 'url("http://server")', sanitizeStyle);
elementStylingApply(1); elementStylingApply(0);
}); });
// nothing is set because sanitizer suppresses it. // nothing is set because sanitizer suppresses it.
expect(t.html).toEqual('<div></div>'); expect(t.html).toEqual('<div></div>');
t.update(() => { t.update(() => {
elementStyleProp(1, 0, bypassSanitizationTrustStyle('url("http://server")'), sanitizeStyle); elementStyleProp(0, 0, bypassSanitizationTrustStyle('url("http://server")'), sanitizeStyle);
elementStylingApply(1); elementStylingApply(0);
}); });
expect((t.hostElement.firstChild as HTMLElement).style.getPropertyValue('background-image')) expect((t.hostElement.firstChild as HTMLElement).style.getPropertyValue('background-image'))
.toEqual('url("http://server")'); .toEqual('url("http://server")');
@ -211,25 +211,33 @@ describe('instructions', () => {
describe('elementStyleMap', () => { describe('elementStyleMap', () => {
function createDivWithStyle() { function createDivWithStyle() {
elementStart(0, 'div'); elementStart(0, 'div');
elementStyling(1, ['height', InitialStylingFlags.INITIAL_STYLES, 'height', '10px']); elementStyling(['height', InitialStylingFlags.VALUES_MODE, 'height', '10px']);
elementEnd(); elementEnd();
} }
it('should add style', () => { it('should add style', () => {
const fixture = new TemplateFixture(createDivWithStyle); const fixture = new TemplateFixture(createDivWithStyle);
fixture.update(() => { fixture.update(() => {
elementStyle(1, {'background-color': 'red'}); elementStylingMap(0, {'background-color': 'red'});
elementStylingApply(1); elementStylingApply(0);
}); });
expect(fixture.html).toEqual('<div style="height: 10px; background-color: red;"></div>'); expect(fixture.html).toEqual('<div style="height: 10px; background-color: red;"></div>');
}); });
}); });
describe('elementClass', () => { describe('elementClass', () => {
function createDivWithStyling() {
elementStart(0, 'div');
elementStyling();
elementEnd();
}
it('should add class', () => { it('should add class', () => {
const fixture = new TemplateFixture(createDiv); const fixture = new TemplateFixture(createDivWithStyling);
fixture.update(() => elementClass(0, 'multiple classes')); fixture.update(() => {
elementStylingMap(0, null, 'multiple classes');
elementStylingApply(0);
});
expect(fixture.html).toEqual('<div class="multiple classes"></div>'); expect(fixture.html).toEqual('<div class="multiple classes"></div>');
}); });
}); });

View File

@ -9,7 +9,8 @@
import {RenderFlags} from '@angular/core/src/render3'; import {RenderFlags} from '@angular/core/src/render3';
import {defineComponent, defineDirective} from '../../src/render3/index'; import {defineComponent, defineDirective} from '../../src/render3/index';
import {NO_CHANGE, bind, container, containerRefreshEnd, containerRefreshStart, elementAttribute, elementClassNamed, elementEnd, elementProperty, elementStart, elementStyleProp, elementStyling, elementStylingApply, embeddedViewEnd, embeddedViewStart, interpolation1, interpolation2, interpolation3, interpolation4, interpolation5, interpolation6, interpolation7, interpolation8, interpolationV, load, loadDirective, projection, projectionDef, text, textBinding} from '../../src/render3/instructions'; import {NO_CHANGE, bind, container, containerRefreshEnd, containerRefreshStart, elementAttribute, elementClassProp, elementEnd, elementProperty, elementStart, elementStyleProp, elementStyling, elementStylingApply, embeddedViewEnd, embeddedViewStart, interpolation1, interpolation2, interpolation3, interpolation4, interpolation5, interpolation6, interpolation7, interpolation8, interpolationV, load, loadDirective, projection, projectionDef, text, textBinding} from '../../src/render3/instructions';
import {InitialStylingFlags} from '../../src/render3/interfaces/definition';
import {HEADER_OFFSET} from '../../src/render3/interfaces/view'; import {HEADER_OFFSET} from '../../src/render3/interfaces/view';
import {sanitizeUrl} from '../../src/sanitization/sanitization'; import {sanitizeUrl} from '../../src/sanitization/sanitization';
import {Sanitizer, SecurityContext} from '../../src/sanitization/security'; import {Sanitizer, SecurityContext} from '../../src/sanitization/security';
@ -747,12 +748,12 @@ describe('render3 integration test', () => {
function Template(rf: RenderFlags, ctx: any) { function Template(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {
elementStart(0, 'span'); elementStart(0, 'span');
elementStyling(1, ['border-color']); elementStyling(['border-color']);
elementEnd(); elementEnd();
} }
if (rf & RenderFlags.Update) { if (rf & RenderFlags.Update) {
elementStyleProp(1, 0, ctx); elementStyleProp(0, 0, ctx);
elementStylingApply(1); elementStylingApply(0);
} }
} }
@ -766,12 +767,12 @@ describe('render3 integration test', () => {
function Template(rf: RenderFlags, ctx: any) { function Template(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {
elementStart(0, 'span'); elementStart(0, 'span');
elementStyling(1, ['font-size']); elementStyling(['font-size']);
elementEnd(); elementEnd();
} }
if (rf & RenderFlags.Update) { if (rf & RenderFlags.Update) {
elementStyleProp(1, 0, ctx, 'px'); elementStyleProp(0, 0, ctx, 'px');
elementStylingApply(1); elementStylingApply(0);
} }
} }
@ -787,10 +788,12 @@ describe('render3 integration test', () => {
function Template(rf: RenderFlags, ctx: any) { function Template(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {
elementStart(0, 'span'); elementStart(0, 'span');
elementStyling(null, ['active']);
elementEnd(); elementEnd();
} }
if (rf & RenderFlags.Update) { if (rf & RenderFlags.Update) {
elementClassNamed(0, 'active', bind(ctx)); elementClassProp(0, 0, ctx);
elementStylingApply(0);
} }
} }
@ -809,11 +812,14 @@ describe('render3 integration test', () => {
it('should work correctly with existing static classes', () => { it('should work correctly with existing static classes', () => {
function Template(rf: RenderFlags, ctx: any) { function Template(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {
elementStart(0, 'span', ['class', 'existing']); elementStart(0, 'span');
elementStyling(
null, ['existing', 'active', InitialStylingFlags.VALUES_MODE, 'existing', true]);
elementEnd(); elementEnd();
} }
if (rf & RenderFlags.Update) { if (rf & RenderFlags.Update) {
elementClassNamed(0, 'active', bind(ctx)); elementClassProp(0, 1, ctx);
elementStylingApply(0);
} }
} }

File diff suppressed because it is too large Load Diff