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) {
E(0, 'td');
s(1, c0);
{ T(2); }
s(c0);
{ T(1); }
e();
}
if (rf2 & RenderFlags.Update) {
sp(1, 0, cell.row % 2 ? '' : 'grey');
t(2, b(cell.value));
sp(0, 0, cell.row % 2 ? '' : 'grey');
t(1, b(cell.value));
}
}
v();

View File

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

View File

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

View File

@ -33,13 +33,11 @@ export class Identifiers {
static elementAttribute: o.ExternalReference = {name: 'ɵa', moduleName: CORE};
static elementClass: o.ExternalReference = {name: 'ɵk', moduleName: CORE};
static elementClassNamed: o.ExternalReference = {name: 'ɵkn', moduleName: CORE};
static elementClassProp: o.ExternalReference = {name: 'ɵcp', 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};

View File

@ -40,19 +40,12 @@ function mapBindingToInstruction(type: BindingType): o.ExternalReference|undefin
case BindingType.Attribute:
return R3.elementAttribute;
case BindingType.Class:
return R3.elementClassNamed;
return R3.elementClassProp;
default:
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 {
private _dataIndex = 0;
private _bindingContext = 0;
@ -310,33 +303,59 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
const i18nMessages: o.Statement[] = [];
const attributes: o.Expression[] = [];
const initialStyleDeclarations: o.Expression[] = [];
const initialClassDeclarations: o.Expression[] = [];
const styleInputs: t.BoundAttribute[] = [];
const classInputs: t.BoundAttribute[] = [];
const allOtherInputs: t.BoundAttribute[] = [];
element.inputs.forEach((input: t.BoundAttribute) => {
// [attr.style] should not be treated as a styling-based
// binding since it is intended to write directly to the attr
// and therefore will skip all style resolution that is present
// with style="", [style]="" and [style.prop]="" assignments
if (input.name == 'style' && input.type == BindingType.Property) {
// this should always go first in the compilation (for [style])
styleInputs.splice(0, 0, input);
} else if (input.type == BindingType.Style) {
styleInputs.push(input);
} else {
allOtherInputs.push(input);
switch (input.type) {
// [attr.style] or [attr.class] should not be treated as styling-based
// bindings since they are intended to be written directly to the attr
// and therefore will skip all style/class resolution that is present
// with style="", [style]="" and [style.prop]="", class="",
// [class.prop]="". [class]="" assignments
case BindingType.Property:
if (input.name == 'style') {
// this should always go first in the compilation (for [style])
styleInputs.splice(0, 0, 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 currClassIndex = 0;
let staticStylesMap: {[key: string]: any}|null = null;
let staticClassesMap: {[key: string]: boolean}|null = null;
const stylesIndexMap: {[key: string]: number} = {};
const classesIndexMap: {[key: string]: number} = {};
Object.getOwnPropertyNames(outputAttrs).forEach(name => {
const value = outputAttrs[name];
if (name == 'style') {
staticStylesMap = parseStyle(value);
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 {
attributes.push(o.literal(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
// => [prop1, prop2, prop3, 0, prop1, value1, prop2, value2]
Object.keys(stylesIndexMap).forEach(prop => {
@ -364,7 +391,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
});
if (staticStylesMap) {
initialStyleDeclarations.push(o.literal(core.InitialStylingFlags.INITIAL_STYLES));
initialStyleDeclarations.push(o.literal(core.InitialStylingFlags.VALUES_MODE));
Object.keys(staticStylesMap).forEach(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 ?
this.constantPool.getConstLiteral(o.literalArr(attributes), true) :
o.TYPED_NULL_EXPR;
@ -411,10 +454,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
const implicit = o.variable(CONTEXT_NAME);
const elementStyleIndex =
(initialStyleDeclarations.length || styleInputs.length) ? this.allocateDataSlot() : 0;
const createSelfClosingInstruction =
elementStyleIndex === 0 && element.children.length === 0 && element.outputs.length === 0;
!hasStylingInstructions && element.children.length === 0 && element.outputs.length === 0;
if (createSelfClosingInstruction) {
this.instruction(
@ -429,16 +470,30 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
...trimTrailingNulls(parameters));
// initial styling for static style="..." attributes
if (elementStyleIndex > 0) {
let paramsList: (o.Expression)[] = [o.literal(elementStyleIndex)];
if (hasStylingInstructions) {
const paramsList: (o.Expression)[] = [];
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
// 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).
paramsList.push(
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());
}
@ -465,25 +520,64 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
});
}
if (styleInputs.length && elementStyleIndex > 0) {
const indexLiteral = o.literal(elementStyleIndex);
styleInputs.forEach((input, i) => {
const isMapBasedStyleBinding = i == 0 && input.name == 'style';
const convertedBinding = this.convertPropertyBinding(implicit, input.value, true);
if (isMapBasedStyleBinding) {
this.instruction(
this._bindingCode, input.sourceSpan, R3.elementStyle, indexLiteral, convertedBinding);
} else {
if ((styleInputs.length || classInputs.length) && hasStylingInstructions) {
const indexLiteral = o.literal(elementIndex);
const firstStyle = styleInputs[0];
const mapBasedStyleInput = firstStyle && firstStyle.name == 'style' ? firstStyle : null;
const firstClass = classInputs[0];
const mapBasedClassInput = firstClass && isClassBinding(firstClass) ? firstClass : null;
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;
let styleIndex: number = stylesIndexMap[key] !;
const styleIndex: number = stylesIndexMap[key] !;
this.instruction(
this._bindingCode, input.sourceSpan, R3.elementStyleProp, indexLiteral,
o.literal(styleIndex), convertedBinding);
}
});
const spanEnd = styleInputs[styleInputs.length - 1].sourceSpan;
this.instruction(this._bindingCode, spanEnd, R3.elementStylingApply, indexLiteral);
lastInputCommand = styleInputs[styleInputs.length - 1];
}
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
@ -494,18 +588,6 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
}
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);
if (instruction) {
// TODO(chuckj): runtime: security context?
@ -975,3 +1057,7 @@ export function makeBindingParser(): BindingParser {
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
*/
import {InitialStylingFlags} from '../../src/core';
import {MockDirectory, setup} from '../aot/test_util';
import {compile, expectEmit} from './mock_compile';
/* 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.
*/
@ -44,15 +47,17 @@ describe('compiler compliance', () => {
// The template should look like this (where IDENT is a wild card for an identifier):
const template = `
const $c1$ = ['class', 'my-app', 'title', 'Hello'];
const $c2$ = ['cx', '20', 'cy', '30', 'r', '50'];
const $c1$ = ['title', 'Hello'];
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) {
if (rf & 1) {
$r3$.ɵE(0, 'div', $c1$);
$r3$.ɵs((null as any), $c2$);
$r3$.ɵNS();
$r3$.ɵE(1, 'svg');
$r3$.ɵEe(2, 'circle', $c2$);
$r3$.ɵEe(2, 'circle', $c3$);
$r3$.ɵe();
$r3$.ɵNH();
$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):
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) {
if (rf & 1) {
$r3$.ɵE(0, 'div', $c1$);
$r3$.ɵs((null as any), $c2$);
$r3$.ɵNM();
$r3$.ɵE(1, 'math');
$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):
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) {
if (rf & 1) {
$r3$.ɵE(0, 'div', $c1$);
$r3$.ɵs((null as any), $c2$);
$r3$.ɵT(1, 'Hello ');
$r3$.ɵE(2, 'b');
$r3$.ɵT(3, 'World');
@ -322,6 +331,7 @@ describe('compiler compliance', () => {
const factory = 'factory: function MyComponent_Factory() { return new MyComponent(); }';
const template = `
const _c0 = ['background-color'];
const _c1 = ['error'];
class MyComponent {
static ngComponentDef = i0.ɵdefineComponent({type:MyComponent,selectors:[['my-component']],
factory:function MyComponent_Factory(){
@ -329,13 +339,13 @@ describe('compiler compliance', () => {
},template:function MyComponent_Template(rf:number,ctx:any){
if (rf & 1) {
$r3$.ɵE(0, 'div');
$r3$.ɵs(1, _c0);
$r3$.ɵs(_c0, _c1);
$r3$.ɵe();
}
if (rf & 2) {
$r3$.ɵsp(1, 0, ctx.color);
$r3$.ɵsa(1);
$r3$.ɵkn(0, 'error', $r3$.ɵb(ctx.error));
$r3$.ɵsp(0, 0, ctx.color);
$r3$.ɵcp(0, 0, ctx.error);
$r3$.ɵsa(0);
}
}
`;

View File

@ -43,12 +43,12 @@ describe('compiler compliance: styling', () => {
template: function MyComponent_Template(rf: $RenderFlags$, $ctx$: $MyComponent$) {
if (rf & 1) {
$r3$.ɵE(0, 'div');
$r3$.ɵs(1);
$r3$.ɵs();
$r3$.ɵe();
}
if (rf & 2) {
$r3$.ɵsm(1, $ctx$.myStyleExp);
$r3$.ɵsa(1);
$r3$.ɵsm(0, $ctx$.myStyleExp);
$r3$.ɵsa(0);
}
}
`;
@ -57,7 +57,7 @@ describe('compiler compliance: styling', () => {
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 = {
app: {
@ -85,7 +85,7 @@ describe('compiler compliance: styling', () => {
};
const template = `
const _c0 = ['opacity','width','height',${InitialStylingFlags.INITIAL_STYLES},'opacity','1'];
const _c0 = ['opacity','width','height',${InitialStylingFlags.VALUES_MODE},'opacity','1'];
class MyComponent {
static ngComponentDef = i0.ɵdefineComponent({
type: MyComponent,
@ -96,14 +96,14 @@ describe('compiler compliance: styling', () => {
template: function MyComponent_Template(rf: $RenderFlags$, $ctx$: $MyComponent$) {
if (rf & 1) {
$r3$.ɵE(0, 'div');
$r3$.ɵs(1, _c0);
$r3$.ɵs(_c0);
$r3$.ɵe();
}
if (rf & 2) {
$r3$.ɵsm(1, $ctx$.myStyleExp);
$r3$.ɵsp(1, 1, $ctx$.myWidth);
$r3$.ɵsp(1, 2, $ctx$.myHeight);
$r3$.ɵsa(1);
$r3$.ɵsm(0, $ctx$.myStyleExp);
$r3$.ɵsp(0, 1, $ctx$.myWidth);
$r3$.ɵsp(0, 2, $ctx$.myHeight);
$r3$.ɵsa(0);
$r3$.ɵa(0, 'style', $r3$.ɵb('border-width: 10px'));
}
}
@ -127,7 +127,7 @@ describe('compiler compliance: styling', () => {
template: \`<div [class]="myClassExp"></div>\`
})
export class MyComponent {
myClassExp = [{color:'orange'}, {color:'green', duration:1000}]
myClassExp = {'foo':true}
}
@NgModule({declarations: [MyComponent]})
@ -139,10 +139,13 @@ describe('compiler compliance: styling', () => {
const template = `
template: function MyComponent_Template(rf: $RenderFlags$, $ctx$: $MyComponent$) {
if (rf & 1) {
$r3$.ɵEe(0, 'div');
$r3$.ɵE(0, 'div');
$r3$.ɵs();
$r3$.ɵe();
}
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);
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,
sp as ɵsp,
sa as ɵsa,
k as ɵk,
kn as ɵkn,
cp as ɵcp,
t as ɵt,
v as ɵv,
st as ɵst,

View File

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

View File

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

View File

@ -15,7 +15,7 @@ import {assertDefined, assertEqual, assertLessThan, assertNotDefined, assertNotE
import {throwCyclicDependencyError, throwErrorIfNoChangesMode, throwMultipleComponentError} from './errors';
import {executeHooks, executeInitHooks, queueInitHooks, queueLifecycleHooks} from './hooks';
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 {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';
@ -25,8 +25,8 @@ import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTENT_QUERIES, CONTEXT, Curre
import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert';
import {appendChild, appendProjectedNode, canInsertNativeNode, createTextNode, findComponentHost, getChildLNode, getLViewChild, getNextLNode, getParentLNode, insertView, removeView} from './node_manipulation';
import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher';
import {StylingContext, allocStylingContext, createStylingContextTemplate, renderStyles as renderElementStyles, updateStyleMap as updateElementStyleMap, updateStyleProp as updateElementStyleProp} from './styling';
import {isDifferent, stringify} from './util';
import {StylingContext, StylingIndex, allocStylingContext, createStylingContextTemplate, renderStyling as renderElementStyles, updateClassProp as updateElementClassProp, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling';
import {assertDataInRangeInternal, isDifferent, loadElementInternal, loadInternal, stringify} from './util';
import {ViewRef} from './view_ref';
@ -96,6 +96,7 @@ export const CIRCULAR = '__CIRCULAR__';
*/
let renderer: Renderer3;
let rendererFactory: RendererFactory3;
let currentElementNode: LElementNode|null = null;
export function getRenderer(): Renderer3 {
// top level variables should not be exported for performance reasons (PERF_NOTES.md)
@ -668,6 +669,7 @@ export function elementStart(
const node: LElementNode =
createLNode(index, TNodeType.Element, native !, name, attrs || null, null);
currentElementNode = node;
if (attrs) {
setUpAttributes(native, attrs);
@ -1104,6 +1106,7 @@ export function elementEnd() {
const queries = previousOrParentNode.queries;
queries && queries.addNode(previousOrParentNode);
queueLifecycleHooks(previousOrParentNode.tNode.flags, tView);
currentElementNode = null;
}
/**
@ -1118,7 +1121,7 @@ export function elementEnd() {
export function elementAttribute(
index: number, name: string, value: any, sanitizer?: SanitizerFn): void {
if (value !== NO_CHANGE) {
const element: LElementNode = load(index);
const element = loadElement(index);
if (value == null) {
ngDevMode && ngDevMode.rendererRemoveAttribute++;
isProceduralRenderer(renderer) ? renderer.removeAttribute(element.native, name) :
@ -1149,7 +1152,7 @@ export function elementAttribute(
export function elementProperty<T>(
index: number, propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn): void {
if (value === NO_CHANGE) return;
const node = load(index) as LElementNode;
const node = loadElement(index) as LElementNode;
const tNode = node.tNode;
// if tNode.inputs is undefined, a listener has created outputs, but inputs haven't
// yet been checked
@ -1268,44 +1271,9 @@ function generatePropertyAliases(
* renaming as part of minification.
* @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 {
if (value !== NO_CHANGE) {
const lElement = load(index) as LElementNode;
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);
}
export function elementClassProp<T>(
index: number, stylingIndex: number, value: T | NO_CHANGE): void {
updateElementClassProp(getStylingContext(index), stylingIndex, value ? true : false);
}
/**
@ -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
* specifically for element styling--the index must be the next index after the element
* 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
* 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
* 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 {
const tNode = load<LElementNode>(index - 1).tNode;
export function elementStyling<T>(
styleDeclarations?: (string | boolean | InitialStylingFlags)[] | null,
classDeclarations?: (string | boolean | InitialStylingFlags)[] | null): void {
const lElement = currentElementNode !;
const tNode = lElement.tNode;
if (!tNode.stylingTemplate) {
// initialize the styling template.
tNode.stylingTemplate = createStylingContextTemplate(styles);
tNode.stylingTemplate = createStylingContextTemplate(styleDeclarations, classDeclarations);
}
// Allocate space but leave null for lazy creation.
viewData[index + HEADER_OFFSET] = null;
if (styles && styles.length) {
elementStylingApply(index);
if (styleDeclarations && styleDeclarations.length ||
classDeclarations && classDeclarations.length) {
elementStylingApply(tNode.index - HEADER_OFFSET);
}
}
@ -1354,12 +1329,13 @@ export function elementStyling<T>(index: number, styles?: (string | number)[] |
*/
function getStylingContext(index: number): StylingContext {
let stylingContext = load<StylingContext>(index);
if (!stylingContext) {
const lElement: LElementNode = load(index - 1);
if (!Array.isArray(stylingContext)) {
const lElement = stylingContext as any as LElementNode;
const tNode = lElement.tNode;
ngDevMode &&
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;
}
@ -1379,7 +1355,7 @@ function getStylingContext(index: number): StylingContext {
* index.)
*/
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
* specifically for element styling--the index must be the next index after the element
* index.)
* @param value A value indicating if a given style should be added or removed.
* The expected shape of `value` is an object where keys are style names and the values
* are their corresponding values to set. If value is null, then the style is removed.
* @param styles A key/value style map of the styles that will be applied to the given element.
* Any missing styles (that have already been applied to the element beforehand) will be
* 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 {
updateElementStyleMap(getStylingContext(index), value);
export function elementStylingMap<T>(
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 {
if (value !== NO_CHANGE) {
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.native, 'native element should exist');
ngDevMode && ngDevMode.rendererSetText++;
@ -1758,7 +1739,7 @@ export function container(
* @param index The index of the container in the data array
*/
export function containerRefreshStart(index: number): void {
previousOrParentNode = load(index) as LNode;
previousOrParentNode = loadElement(index) as LNode;
ngDevMode && assertNodeType(previousOrParentNode, TNodeType.Container);
isParent = true;
(previousOrParentNode as LContainerNode).data[ACTIVE_INDEX] = 0;
@ -2541,17 +2522,6 @@ export function store<T>(index: number, value: T): void {
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. */
export function loadDirective<T>(index: number): T {
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];
}
/** 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. */
export function consumeBinding(): any {
ngDevMode && assertDataInRange(viewData[BINDING_INDEX]);
@ -2645,7 +2624,7 @@ function assertHasParent() {
function assertDataInRange(index: number, arr?: any[]) {
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[]) {

View File

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

View File

@ -63,8 +63,7 @@ export const angularCoreEnv: {[name: string]: Function} = {
'ɵi7': r3.i7,
'ɵi8': r3.i8,
'ɵiV': r3.iV,
'ɵk': r3.k,
'ɵkn': r3.kn,
'ɵcp': r3.cp,
'ɵL': r3.L,
'ɵld': r3.ld,
'ɵ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 {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 {stringify} from './util';
import {readElementValue, stringify} from './util';
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 {
if (node.tNode.child) {
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;
}
@ -50,7 +50,7 @@ export function getParentLNode(node: LNode): LElementNode|LContainerNode|LViewNo
return containerHostIndex === -1 ? null : node.view[containerHostIndex].dynamicLContainerNode;
}
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 {
@ -463,7 +463,7 @@ function removeListeners(viewData: LViewData): void {
for (let i = 0; i < cleanup.length - 1; i += 2) {
if (typeof cleanup[i] === 'string') {
// 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]];
native.removeEventListener(cleanup[i], listener, cleanup[i + 3]);
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
* styling properties have been assigned via the provided `updateStyleMap` and `updateStyleProp`
* functions. There are also two initialization functions `allocStylingContext` and
* `createStylingContextTemplate` which are used to initialize and/or clone the context.
* styling properties have been assigned via the provided `updateStylingMap`, `updateStyleProp`
* and `updateClassProp` functions. There are also two initialization functions
* `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)
* and dirty flags / index offsets). The remaining set of cells is used for multi (map) and single
* (prop) style values.
*
* 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 + 2] = value string (or null incase it has been removed)
*
* There are three types of styling types stored in this context:
* 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
* 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
* 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).
*
* When the context is created, depending on what initial styles are passed in, the context itself
* will be pre-filled with slots based on the initial style properties. Say for example we have a
* series of initial styles that look like so:
* When the context is created, depending on what initial styling values are passed in, the
* context itself will be pre-filled with slots based on the initial style properties. Say
* for example we have a series of initial styles that look like so:
*
* style="width:100px; height:200px;"
* class="foo"
*
* Then the initial state of the context (once initialized) will look like so:
*
* ```
* 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.
*
* 1, // this instructs how many `style` values there are so that class index values can be
* offsetted
*
* configMasterVal,
*
* // 2
* // 3
* 'width',
* pointers(1, 8); // Point to static `width`: `100px` and multi `width`.
* pointers(1, 12); // Point to static `width`: `100px` and multi `width`.
* null,
*
* // 5
* // 6
* 'height',
* pointers(2, 11); // Point to static `height`: `200px` and multi `height`.
* pointers(2, 15); // Point to static `height`: `200px` and multi `height`.
* null,
*
* // 8
* // 9
* 'foo',
* pointers(1, 18); // Point to static `foo`: `true` and multi `foo`.
* null,
*
* // 12
* 'width',
* pointers(1, 2); // Point to static `width`: `100px` and single `width`.
* pointers(1, 3); // Point to static `width`: `100px` and single `width`.
* null,
*
* // 11
* // 15
* '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,
* ]
*
@ -82,30 +97,50 @@ import {Renderer3, RendererStyleFlags3, isProceduralRenderer} from './interfaces
* }
* ```
*
* The values are duplicated so that space is set aside for both multi ([style])
* and single ([style.prop]) values. The respective config values (configValA, configValB, etc...)
* are a combination of the StylingFlags with two index values: the `initialIndex` (which points to
* the index location of the style value in the initial styles array in slot 0) and the
* `dynamicIndex` (which points to the matching single/multi index position in the context array
* for the same prop).
* The values are duplicated so that space is set aside for both multi ([style] and [class])
* and single ([style.prop] or [class.named]) values. The respective config values
* (configValA, configValB, etc...) are a combination of the StylingFlags with two index
* values: the `initialIndex` (which points to the index location of the style value in
* the initial styles array in slot 0) and the `dynamicIndex` (which points to the
* 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
* (not a property string) which references the index value of the initial style when the context
* was created. This also means that `updateStyleProp` cannot be called with a new property
* (only `updateStyleMap` can include new CSS properties that will be added to the context).
* This means that every time `updateStyleProp` or `updateClassProp` are called then they
* must be called using an index value (not a property string) which references the index
* value of the initial style prop/class when the context was created. This also means that
* `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.
*/
[0]: InitialStyles;
[1]: InitialStyles;
/**
* 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
* 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
* 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)
@ -124,21 +159,31 @@ export interface InitialStyles extends Array<string|null> { [0]: null; }
*/
export const enum StylingFlags {
// Implies no configurations
None = 0b0,
None = 0b00,
// 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
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` */
export const enum StylingIndex {
// 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`)
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
SingleStylesStartPosition = 2,
SingleStylesStartPosition = 5,
// Multi and single entries are stored in `StylingContext` as: Flag; PropertyName; PropertyValue
FlagsOffset = 0,
PropertyOffset = 1,
@ -157,9 +202,12 @@ export const enum StylingIndex {
* A pre-computed template is designed to be computed once for a given element
* (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
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']
* This implies that `width` and `height` will be later styled and that the `width`
* 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(
initialStyleDeclarations?: (string | InitialStylingFlags)[] | null): StylingContext {
const initialStyles: InitialStyles = [null];
const context: StylingContext = [initialStyles, 0];
initialStyleDeclarations?: (string | boolean | InitialStylingFlags)[] | null,
initialClassDeclarations?: (string | boolean | InitialStylingFlags)[] | null): StylingContext {
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) {
let hasPassedDeclarations = false;
for (let i = 0; i < initialStyleDeclarations.length; i++) {
const v = initialStyleDeclarations[i] as string | InitialStylingFlags;
// this flag value marks where the declarations end the initial values begin
if (v === InitialStylingFlags.INITIAL_STYLES) {
if (v === InitialStylingFlags.VALUES_MODE) {
hasPassedDeclarations = true;
} else {
const prop = v as string;
if (hasPassedDeclarations) {
const value = initialStyleDeclarations[++i] as string;
initialStyles.push(value);
indexLookup[prop] = initialStyles.length - 1;
initialStylingValues.push(value);
stylesLookup[prop] = initialStylingValues.length - 1;
} else {
// it's safe to use `0` since the default initial value for
// each property will always be null (which is at position 0)
indexLookup[prop] = 0;
totalStyleDeclarations++;
stylesLookup[prop] = 0;
}
}
}
}
const allProps = Object.keys(indexLookup);
const totalProps = allProps.length;
// make where the class offsets begin
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
const maxLength = totalProps * StylingIndex.Size * 2 + StylingIndex.SingleStylesStartPosition;
@ -222,86 +306,140 @@ export function createStylingContextTemplate(
const multiStart = totalProps * StylingIndex.Size + StylingIndex.SingleStylesStartPosition;
// fill single and multi-level styles
for (let i = 0; i < allProps.length; i++) {
const prop = allProps[i];
for (let i = 0; i < totalProps; 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 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);
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);
setValue(context, indexForMulti, null);
}
// there is no initial value flag for the master index since it doesn't reference an initial style
// value
// there is no initial value flag for the master index since it doesn't
// reference an initial style value
setFlag(context, StylingIndex.MasterFlagPosition, pointers(0, 0, multiStart));
setContextDirty(context, initialStyles.length > 1);
setContextDirty(context, initialStylingValues.length > 1);
return context;
}
const EMPTY_ARR: any[] = [];
const EMPTY_OBJ: {[key: string]: any} = {};
/**
* Sets and resolves all `multi` styles on an `StylingContext` so that they can be
* applied to the element once `renderStyles` is called.
* Sets and resolves all `multi` styling on an `StylingContext` so that they can be
* applied to the element once `renderStyling` is called.
*
* All missing styles (any values that are not provided in the new `styles` param)
* will resolve to `null` within their respective positions in the context.
* All missing styles/class (any values that are not provided in the new `styles`
* 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
* newly provided style values.
* @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 {
const propsToApply = styles ? Object.keys(styles) : EMPTY_ARR;
export function updateStylingMap(
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);
let dirty = false;
let ctxIndex = multiStartIndex;
let propIndex = 0;
const propLimit = styleProps.length + classNames.length;
// 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
// off-balance then they will be dealt in another loop after this one
while (ctxIndex < context.length && propIndex < propsToApply.length) {
const flag = getPointers(context, ctxIndex);
const prop = getProp(context, ctxIndex);
const value = getValue(context, ctxIndex);
// styles differ with respect to the context. Later if the context/styles/classes
// are off-balance then they will be dealt in another loop after this one
while (ctxIndex < context.length && propIndex < propLimit) {
const isClassBased = propIndex >= classesStartIndex;
const newProp = propsToApply[propIndex];
const newValue = styles ![newProp];
if (prop === newProp) {
if (value !== newValue) {
setValue(context, ctxIndex, newValue);
const initialValue = getInitialValue(context, flag);
// when there is a cache-hit for a string-based class then we should
// avoid doing any work diffing any of the changes
if (!ignoreAllClassUpdates || !isClassBased) {
const adjustedPropIndex = isClassBased ? propIndex - classesStartIndex : propIndex;
const newProp: string =
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
// rendered value was being referenced by the initial style (or null)
if (initialValue !== newValue) {
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);
const prop = getProp(context, ctxIndex);
if (prop === newProp) {
const value = getValue(context, ctxIndex);
if (value !== 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 {
// we only care to do this if the insertion is in the middle
const doShift = ctxIndex < context.length;
insertNewMultiProperty(context, ctxIndex, newProp, newValue);
dirty = true;
const indexOfEntry = findEntryPositionByProp(context, newProp, ctxIndex);
if (indexOfEntry > 0) {
// it was found at a later point ... just swap the values
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
// were not included in the provided styles and in this case the
// goal is to "remove" them from the context (by nullifying)
// were not included in the provided styles/classes and in this
// case the goal is to "remove" them from the context (by nullifying)
while (ctxIndex < context.length) {
const value = context[ctxIndex + StylingIndex.ValueOffset];
if (value !== null) {
const flag = getPointers(context, ctxIndex);
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);
setValue(context, ctxIndex, null);
dirty = true;
@ -322,13 +465,19 @@ export function updateStyleMap(context: StylingContext, styles: {[key: string]:
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
// case we want to add the new entries into the list
while (propIndex < propsToApply.length) {
const prop = propsToApply[propIndex];
const value = styles ![prop];
context.push(StylingFlags.Dirty, prop, value);
while (propIndex < propLimit) {
const isClassBased = propIndex >= classesStartIndex;
if (ignoreAllClassUpdates && isClassBased) break;
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++;
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
* can be applied to the element once `renderElementStyles` is called.
* Sets and resolves a single styling property/value on the provided `StylingContext` so
* 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
* using `updateStyleMap`, therefore, when styles are rendered then any styles that
* have been applied using this function will be considered first (then multi values second
* and then initial values as a backup).
* Note that prop-level styling values are considered higher priority than any styling that
* has been applied using `updateStylingMap`, therefore, when styling values are rendered
* then any styles/classes that have been applied using this function will be considered first
* (then multi values second and then initial values as a backup).
*
* @param context The styling context that will be updated with the
* 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
*/
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 currValue = getValue(context, singleIndex);
const currFlag = getPointers(context, singleIndex);
@ -370,8 +519,10 @@ export function updateStyleProp(
let multiDirty = false;
let singleDirty = true;
const isClassBased = (currFlag & StylingFlags.Class) === StylingFlags.Class;
// 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;
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
* using `updateStyleMap` and `updateStyleProp`) onto the
* provided element using the provided renderer. Just before the styles
* are rendered a final key/value style map will be assembled.
* using `updateStylingMap`) and any classes (that have been applied using
* `updateStyleProp`) onto the provided element using the provided renderer.
* 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 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 styleStore if provided, the updated style values will be applied
* 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(
lElement: LElementNode, context: StylingContext, renderer: Renderer3,
styleStore?: {[key: string]: any}) {
export function renderStyling(
context: StylingContext, renderer: Renderer3, styleStore?: {[key: string]: any},
classStore?: {[key: string]: boolean}) {
if (isContextDirty(context)) {
const native = lElement.native;
const native = context[StylingIndex.ElementPosition] !.native;
const multiStartIndex = getMultiStartIndex(context);
for (let i = StylingIndex.SingleStylesStartPosition; i < context.length;
i += StylingIndex.Size) {
@ -412,27 +580,35 @@ export function renderStyles(
const prop = getProp(context, i);
const value = getValue(context, i);
const flag = getPointers(context, i);
const isClassBased = flag & StylingFlags.Class ? true : false;
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
// 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
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,
// therefore we can safely adopt it incase there's nothing else
if (styleToApply == null) {
styleToApply = getInitialValue(context, flag);
// note that this should always be a falsy check since `false` is used
// 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);
}
}
@ -443,7 +619,7 @@ export function renderStyles(
/**
* 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
* renderer.
*
@ -451,23 +627,51 @@ export function renderStyles(
* @param prop the CSS style property that will be rendered
* @param value the CSS style value that will be rendered
* @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(
native: any, prop: string, value: string | null, renderer: Renderer3,
styleStore?: {[key: string]: any}) {
if (styleStore) {
styleStore[prop] = value;
} else if (value == null) {
ngDevMode && ngDevMode.rendererRemoveStyle++;
isProceduralRenderer(renderer) ?
renderer.removeStyle(native, prop, RendererStyleFlags3.DashCase) :
native['style'].removeProperty(prop);
} else {
store?: {[key: string]: any}) {
if (store) {
store[prop] = value;
} else if (value) {
ngDevMode && ngDevMode.rendererSetStyle++;
isProceduralRenderer(renderer) ?
renderer.setStyle(native, prop, value, RendererStyleFlags3.DashCase) :
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;
}
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) {
return (configFlag & StylingFlags.Dirty) | (staticIndex << StylingFlags.BitCountSize) |
return (configFlag & StylingFlags.BitMask) | (staticIndex << StylingFlags.BitCountSize) |
(dynamicIndex << (StylingIndex.BitCountSize + StylingFlags.BitCountSize));
}
@ -515,7 +725,7 @@ function setProp(context: StylingContext, index: number, prop: string) {
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;
}
@ -531,8 +741,8 @@ function getPointers(context: StylingContext, index: number): number {
return context[adjustedIndex] as number;
}
function getValue(context: StylingContext, index: number): string|null {
return context[index + StylingIndex.ValueOffset] as string | null;
function getValue(context: StylingContext, index: number): string|boolean|null {
return context[index + StylingIndex.ValueOffset] as string | boolean | null;
}
function getProp(context: StylingContext, index: number): string {
@ -597,20 +807,23 @@ function updateSinglePointerValues(context: StylingContext, indexStartPosition:
if (singleIndex > 0) {
const singleFlag = getPointers(context, singleIndex);
const initialIndexForSingle = getInitialIndex(singleFlag);
const updatedFlag = pointers(
isDirty(context, singleIndex) ? StylingFlags.Dirty : StylingFlags.None,
initialIndexForSingle, i);
const flagValue = (isDirty(context, singleIndex) ? StylingFlags.Dirty : StylingFlags.None) |
(isClassBased(context, singleIndex) ? StylingFlags.Class : StylingFlags.None);
const updatedFlag = pointers(flagValue, initialIndexForSingle, i);
setFlag(context, singleIndex, updatedFlag);
}
}
}
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;
// 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) {
// because the value was inserted midway into the array then we
@ -619,3 +832,10 @@ function insertNewMultiProperty(
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
* 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
@ -56,3 +60,27 @@ export function flatten(list: any[]): any[] {
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": "readElementValue"
},
{
"name": "refreshChildComponents"
},

View File

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

View File

@ -15,7 +15,6 @@ import {ComponentDefInternal, InitialStylingFlags} from '../../../src/render3/in
import {ComponentFixture, renderComponent, toHtml} from '../render_util';
/// See: `normative.md`
describe('elements', () => {
// 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', () => {
const c1: (string | InitialStylingFlags | boolean)[] = ['foo'];
type $MyComponent$ = MyComponent;
@Component({selector: 'my-component', template: `<div [class.foo]="someFlag"></div>`})
@ -283,10 +283,13 @@ describe('elements', () => {
factory: function MyComponent_Factory() { return new MyComponent(); },
template: function MyComponent_Template(rf: $RenderFlags$, ctx: $MyComponent$) {
if (rf & 1) {
$r3$.ɵEe(0, 'div');
$r3$.ɵE(0, 'div');
$r3$.ɵs(null, c1);
$r3$.ɵe();
}
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$) {
if (rf & 1) {
$r3$.ɵE(0, 'div');
$r3$.ɵs(1, c0);
$r3$.ɵs(c0);
$r3$.ɵe();
}
if (rf & 2) {
$r3$.ɵsp(1, 0, ctx.someColor);
$r3$.ɵsp(1, 1, ctx.someWidth, 'px');
$r3$.ɵsa(1);
$r3$.ɵsp(0, 0, ctx.someColor);
$r3$.ɵsp(0, 1, ctx.someWidth, 'px');
$r3$.ɵsa(0);
}
}
});
@ -353,7 +356,9 @@ describe('elements', () => {
it('should bind to many and keep order', () => {
type $MyComponent$ = MyComponent;
const c0 = ['color', InitialStylingFlags.INITIAL_STYLES, 'color', 'red'];
const c0 = ['color', InitialStylingFlags.VALUES_MODE, 'color', 'red'];
const c1 = ['foo'];
@Component({
selector: 'my-component',
template:
@ -369,12 +374,13 @@ describe('elements', () => {
template: function MyComponent_Template(rf: $RenderFlags$, ctx: $MyComponent$) {
if (rf & 1) {
$r3$.ɵE(0, 'div');
$r3$.ɵs(1, c0);
$r3$.ɵs(c0, c1);
$r3$.ɵe();
}
if (rf & 2) {
$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$) {
if (rf & 1) {
$r3$.ɵE(0, 'div');
$r3$.ɵs(1);
$r3$.ɵs();
$r3$.ɵe();
}
if (rf & 2) {
$r3$.ɵk(0, $r3$.ɵb(ctx.classExp));
$r3$.ɵsm(1, ctx.styleExp);
$r3$.ɵsa(1);
$r3$.ɵsm(0, ctx.styleExp, ctx.classExp);
$r3$.ɵsa(0);
}
}
});

View File

@ -44,17 +44,17 @@ describe('compiler sanitization', () => {
template: function MyComponent_Template(rf: $RenderFlags$, ctx: $MyComponent$) {
if (rf & 1) {
$r3$.ɵE(0, 'div');
$r3$.ɵs(1, ['background-image']);
$r3$.ɵs(['background-image']);
$r3$.ɵe();
$r3$.ɵEe(2, 'img');
$r3$.ɵEe(1, 'img');
}
if (rf & 2) {
$r3$.ɵp(0, 'innerHTML', $r3$.ɵb(ctx.innerHTML), $r3$.ɵsanitizeHtml);
$r3$.ɵp(0, 'hidden', $r3$.ɵb(ctx.hidden));
$r3$.ɵsp(1, 0, ctx.style, $r3$.ɵsanitizeStyle);
$r3$.ɵsa(1);
$r3$.ɵp(2, 'src', $r3$.ɵb(ctx.url), $r3$.ɵsanitizeUrl);
$r3$.ɵa(2, 'srcset', $r3$.ɵb(ctx.url), $r3$.ɵsanitizeUrl);
$r3$.ɵsp(0, 0, ctx.style, $r3$.ɵsanitizeStyle);
$r3$.ɵsa(0);
$r3$.ɵp(1, 'src', $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 {bind, container, containerRefreshEnd, containerRefreshStart, elementAttribute, elementClassNamed, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, load, text, textBinding} from '../../src/render3/instructions';
import {RenderFlags} from '../../src/render3/interfaces/definition';
import {bind, container, containerRefreshEnd, containerRefreshStart, elementAttribute, elementClassProp, elementEnd, elementProperty, elementStart, elementStyling, elementStylingApply, embeddedViewEnd, embeddedViewStart, load, text, textBinding} from '../../src/render3/instructions';
import {InitialStylingFlags, RenderFlags} from '../../src/render3/interfaces/definition';
import {ComponentFixture, createComponent, renderToHtml} from './render_util';
describe('exports', () => {
@ -212,13 +213,15 @@ describe('exports', () => {
function Template(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
elementStart(0, 'div');
elementStyling(null, [InitialStylingFlags.VALUES_MODE, 'red', true]);
elementEnd();
elementStart(1, 'input', ['type', 'checkbox', 'checked', 'true'], ['myInput', '']);
elementEnd();
}
const tmp = load(2) as any;
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 {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 {AttributeMarker, LElementNode, LNode} from '../../src/render3/interfaces/node';
import {RElement, domRendererFactory3} from '../../src/render3/interfaces/renderer';
@ -23,13 +23,13 @@ import {ComponentFixture, TemplateFixture} from './render_util';
describe('instructions', () => {
function createAnchor() {
elementStart(0, 'a');
elementStyling(1);
elementStyling();
elementEnd();
}
function createDiv(initialStyles?: (string | number)[]) {
elementStart(0, 'div');
elementStyling(1, initialStyles && Array.isArray(initialStyles) ? initialStyles : null);
elementStyling(initialStyles && Array.isArray(initialStyles) ? initialStyles : null);
elementEnd();
}
@ -193,15 +193,15 @@ describe('instructions', () => {
it('should use sanitizer function', () => {
const t = new TemplateFixture(() => { return createDiv(['background-image']); });
t.update(() => {
elementStyleProp(1, 0, 'url("http://server")', sanitizeStyle);
elementStylingApply(1);
elementStyleProp(0, 0, 'url("http://server")', sanitizeStyle);
elementStylingApply(0);
});
// nothing is set because sanitizer suppresses it.
expect(t.html).toEqual('<div></div>');
t.update(() => {
elementStyleProp(1, 0, bypassSanitizationTrustStyle('url("http://server")'), sanitizeStyle);
elementStylingApply(1);
elementStyleProp(0, 0, bypassSanitizationTrustStyle('url("http://server")'), sanitizeStyle);
elementStylingApply(0);
});
expect((t.hostElement.firstChild as HTMLElement).style.getPropertyValue('background-image'))
.toEqual('url("http://server")');
@ -211,25 +211,33 @@ describe('instructions', () => {
describe('elementStyleMap', () => {
function createDivWithStyle() {
elementStart(0, 'div');
elementStyling(1, ['height', InitialStylingFlags.INITIAL_STYLES, 'height', '10px']);
elementStyling(['height', InitialStylingFlags.VALUES_MODE, 'height', '10px']);
elementEnd();
}
it('should add style', () => {
const fixture = new TemplateFixture(createDivWithStyle);
fixture.update(() => {
elementStyle(1, {'background-color': 'red'});
elementStylingApply(1);
elementStylingMap(0, {'background-color': 'red'});
elementStylingApply(0);
});
expect(fixture.html).toEqual('<div style="height: 10px; background-color: red;"></div>');
});
});
describe('elementClass', () => {
function createDivWithStyling() {
elementStart(0, 'div');
elementStyling();
elementEnd();
}
it('should add class', () => {
const fixture = new TemplateFixture(createDiv);
fixture.update(() => elementClass(0, 'multiple classes'));
const fixture = new TemplateFixture(createDivWithStyling);
fixture.update(() => {
elementStylingMap(0, null, 'multiple classes');
elementStylingApply(0);
});
expect(fixture.html).toEqual('<div class="multiple classes"></div>');
});
});

View File

@ -9,7 +9,8 @@
import {RenderFlags} from '@angular/core/src/render3';
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 {sanitizeUrl} from '../../src/sanitization/sanitization';
import {Sanitizer, SecurityContext} from '../../src/sanitization/security';
@ -747,12 +748,12 @@ describe('render3 integration test', () => {
function Template(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
elementStart(0, 'span');
elementStyling(1, ['border-color']);
elementStyling(['border-color']);
elementEnd();
}
if (rf & RenderFlags.Update) {
elementStyleProp(1, 0, ctx);
elementStylingApply(1);
elementStyleProp(0, 0, ctx);
elementStylingApply(0);
}
}
@ -766,12 +767,12 @@ describe('render3 integration test', () => {
function Template(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
elementStart(0, 'span');
elementStyling(1, ['font-size']);
elementStyling(['font-size']);
elementEnd();
}
if (rf & RenderFlags.Update) {
elementStyleProp(1, 0, ctx, 'px');
elementStylingApply(1);
elementStyleProp(0, 0, ctx, 'px');
elementStylingApply(0);
}
}
@ -787,10 +788,12 @@ describe('render3 integration test', () => {
function Template(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
elementStart(0, 'span');
elementStyling(null, ['active']);
elementEnd();
}
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', () => {
function Template(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
elementStart(0, 'span', ['class', 'existing']);
elementStart(0, 'span');
elementStyling(
null, ['existing', 'active', InitialStylingFlags.VALUES_MODE, 'existing', true]);
elementEnd();
}
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