fix(ivy): merge static style rendering across elements, directives and components (#27661)

PR Close #27661
This commit is contained in:
Matias Niemelä 2018-12-13 15:51:47 -08:00
parent 0b3ae3d70c
commit 13eb57a59f
39 changed files with 3493 additions and 1588 deletions

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AttributeMarker, InitialStylingFlags} from '@angular/compiler/src/core';
import {AttributeMarker} from '@angular/compiler/src/core';
import {setup} from '@angular/compiler/test/aot/test_util';
import {compile, expectEmit} from './mock_compile';
@ -48,17 +48,15 @@ describe('compiler compliance', () => {
// The template should look like this (where IDENT is a wild card for an identifier):
const template = `
const $c1$ = ["title", "Hello"];
const $c2$ = ["my-app", ${InitialStylingFlags.VALUES_MODE}, "my-app", true];
const $c3$ = ["cx", "20", "cy", "30", "r", "50"];
const $c1$ = ["title", "Hello", ${AttributeMarker.Classes}, "my-app"];
const $c2$ = ["cx", "20", "cy", "30", "r", "50"];
template: function MyComponent_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵelementStart(0, "div", $c1$);
$r3$.ɵelementStyling($c2$);
$r3$.ɵnamespaceSVG();
$r3$.ɵelementStart(1, "svg");
$r3$.ɵelement(2, "circle", $c3$);
$r3$.ɵelement(2, "circle", $c2$);
$r3$.ɵelementEnd();
$r3$.ɵnamespaceHTML();
$r3$.ɵelementStart(3, "p");
@ -100,13 +98,11 @@ describe('compiler compliance', () => {
// The template should look like this (where IDENT is a wild card for an identifier):
const template = `
const $c1$ = ["title", "Hello"];
const $c2$ = ["my-app", ${InitialStylingFlags.VALUES_MODE}, "my-app", true];
const $c1$ = ["title", "Hello", ${AttributeMarker.Classes}, "my-app"];
template: function MyComponent_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵelementStart(0, "div", $c1$);
$r3$.ɵelementStyling($c2$);
$r3$.ɵnamespaceMathML();
$r3$.ɵelementStart(1, "math");
$r3$.ɵelement(2, "infinity");
@ -150,13 +146,11 @@ describe('compiler compliance', () => {
// The template should look like this (where IDENT is a wild card for an identifier):
const template = `
const $c1$ = ["title", "Hello"];
const $c2$ = ["my-app", ${InitialStylingFlags.VALUES_MODE}, "my-app", true];
const $c1$ = ["title", "Hello", ${AttributeMarker.Classes}, "my-app"];
template: function MyComponent_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵelementStart(0, "div", $c1$);
$r3$.ɵelementStyling($c2$);
$r3$.ɵtext(1, "Hello ");
$r3$.ɵelementStart(2, "b");
$r3$.ɵtext(3, "World");
@ -486,8 +480,8 @@ describe('compiler compliance', () => {
const factory =
'factory: function MyComponent_Factory(t) { return new (t || MyComponent)(); }';
const template = `
const _c0 = ["error"];
const _c1 = ["background-color"];
const $e0_classBindings$ = ["error"];
const $e0_styleBindings$ = ["background-color"];
MyComponent.ngComponentDef = i0.ɵdefineComponent({type:MyComponent,selectors:[["my-component"]],
factory: function MyComponent_Factory(t){
@ -498,7 +492,7 @@ describe('compiler compliance', () => {
template: function MyComponent_Template(rf,ctx){
if (rf & 1) {
$r3$.ɵelementStart(0, "div");
$r3$.ɵelementStyling(_c0, _c1);
$r3$.ɵelementStyling($e0_classBindings$, $e0_styleBindings$);
$r3$.ɵelementEnd();
}
if (rf & 2) {
@ -1204,7 +1198,7 @@ describe('compiler compliance', () => {
}
};
const output = `
const $_c0$ = [1, "ngIf"];
const $_c0$ = [${AttributeMarker.SelectOnly}, "ngIf"];
const $_c1$ = ["id", "second"];
function Cmp_div_Template_0(rf, ctx) { if (rf & 1) {
$r3$.ɵelementStart(0, "div", $_c1$);
@ -1310,7 +1304,6 @@ describe('compiler compliance', () => {
const {source} = compile(files, angularFiles);
expectEmit(source, output, 'Invalid content projection instructions generated');
});
});
describe('queries', () => {

View File

@ -236,7 +236,7 @@ describe('compiler compliance: directives', () => {
const MyComponentDefinition = `
const $_c0$ = [1, "ngIf"];
const $_c0$ = [${AttributeMarker.SelectOnly}, "ngIf"];
const $_c1$ = ["directiveA", ""];
function MyComponent_ng_container_Template_0(rf, ctx) {
if (rf & 1) {

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AttributeMarker} from '@angular/compiler/src/core';
import {setup} from '@angular/compiler/test/aot/test_util';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../../compiler/src/compiler';
@ -393,7 +394,7 @@ describe('i18n support in the view compiler', () => {
`;
const output = String.raw `
const $_c0$ = ["ngFor", "", 1, "ngForOf"];
const $_c0$ = ["ngFor", "", ${AttributeMarker.SelectOnly}, "ngForOf"];
/**
* @desc d
* @meaning m
@ -522,7 +523,7 @@ describe('i18n support in the view compiler', () => {
`;
const output = String.raw `
const $_c0$ = ["ngFor", "", 1, "ngForOf"];
const $_c0$ = ["ngFor", "", ${AttributeMarker.SelectOnly}, "ngForOf"];
/**
* @desc d
* @meaning m
@ -922,7 +923,7 @@ describe('i18n support in the view compiler', () => {
`;
const output = String.raw `
const $_c0$ = [1, "ngIf"];
const $_c0$ = [${AttributeMarker.SelectOnly}, "ngIf"];
const $MSG_EXTERNAL_7679414751795588050$$APP_SPEC_TS__1$ = goog.getMsg(" Some other content {$interpolation} {$startTagDiv} More nested levels with bindings {$interpolation_1} {$closeTagDiv}", {
"interpolation": "\uFFFD0\uFFFD",
"startTagDiv": "\uFFFD#3\uFFFD",
@ -976,7 +977,7 @@ describe('i18n support in the view compiler', () => {
const output = String.raw `
const $_c0$ = ["src", "logo.png"];
const $_c1$ = [1, "ngIf"];
const $_c1$ = [${AttributeMarker.SelectOnly}, "ngIf"];
function MyComponent_img_Template_1(rf, ctx) {
if (rf & 1) {
$r3$.ɵelement(0, "img", $_c0$);
@ -1043,7 +1044,7 @@ describe('i18n support in the view compiler', () => {
`;
const output = String.raw `
const $_c0$ = [1, "ngIf"];
const $_c0$ = [${AttributeMarker.SelectOnly}, "ngIf"];
function MyComponent_div_div_Template_4(rf, ctx) {
if (rf & 1) {
$r3$.ɵi18nStart(0, $I18N_EXTERNAL_1221890473527419724$$APP_SPEC_TS_0$, 2);
@ -1136,7 +1137,7 @@ describe('i18n support in the view compiler', () => {
`;
const output = String.raw `
const $_c0$ = [1, "ngIf"];
const $_c0$ = [${AttributeMarker.SelectOnly}, "ngIf"];
const $MSG_EXTERNAL_119975189388320493$$APP_SPEC_TS__1$ = goog.getMsg("Some other content {$startTagSpan}{$interpolation}{$closeTagSpan}", {
"startTagSpan": "\uFFFD#2\uFFFD",
"interpolation": "\uFFFD0\uFFFD",
@ -1259,23 +1260,21 @@ describe('i18n support in the view compiler', () => {
`;
const output = String.raw `
const $_c0$ = ["myClass", 1, "myClass", true];
const $_c0$ = [${AttributeMarker.Classes}, "myClass"];
const $MSG_EXTERNAL_5295701706185791735$$APP_SPEC_TS_0$ = goog.getMsg("Text #1");
const $_c1$ = ["padding", 1, "padding", "10px"];
const $_c1$ = [${AttributeMarker.Styles}, "padding", "10px"];
const $MSG_EXTERNAL_4722270221386399294$$APP_SPEC_TS_2$ = goog.getMsg("Text #2");
consts: 4,
vars: 0,
template: function MyComponent_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵelementStart(0, "span");
$r3$.ɵelementStart(0, "span", $_c0$);
$r3$.ɵi18nStart(1, $MSG_EXTERNAL_5295701706185791735$$APP_SPEC_TS_0$);
$r3$.ɵelementStyling($_c0$);
$r3$.ɵi18nEnd();
$r3$.ɵelementEnd();
$r3$.ɵelementStart(2, "span");
$r3$.ɵelementStart(2, "span", $_c1$);
$r3$.ɵi18nStart(3, $MSG_EXTERNAL_4722270221386399294$$APP_SPEC_TS_2$);
$r3$.ɵelementStyling(null, $_c1$);
$r3$.ɵi18nEnd();
$r3$.ɵelementEnd();
}
@ -1701,7 +1700,7 @@ describe('i18n support in the view compiler', () => {
const $I18N_EXTERNAL_7842238767399919809$$APP_SPEC_TS_0$ = $r3$.ɵi18nPostprocess($MSG_EXTERNAL_7842238767399919809$$APP_SPEC_TS_0$, {
"VAR_SELECT": "\uFFFD0\uFFFD"
});
const $_c0$ = [1, "ngIf"];
const $_c0$ = [${AttributeMarker.SelectOnly}, "ngIf"];
const $_c1$ = ["title", "icu only"];
const $MSG_EXTERNAL_8806993169187953163$$APP_SPEC_TS__3$ = goog.getMsg("{VAR_SELECT, select, 10 {ten} 20 {twenty} other {other}}");
const $I18N_EXTERNAL_8806993169187953163$$APP_SPEC_TS__3$ = $r3$.ɵi18nPostprocess($MSG_EXTERNAL_8806993169187953163$$APP_SPEC_TS__3$, {
@ -1942,7 +1941,7 @@ describe('i18n support in the view compiler', () => {
const $I18N_APP_SPEC_TS_2$ = $r3$.ɵi18nPostprocess($MSG_APP_SPEC_TS_2$, {
"VAR_SELECT": "\uFFFD1\uFFFD"
});
const $_c3$ = [1, "ngIf"];
const $_c3$ = [${AttributeMarker.SelectOnly}, "ngIf"];
const $MSG_APP_SPEC_TS__4$ = goog.getMsg("{VAR_SELECT, select, male {male} female {female} other {other}}");
const $I18N_APP_SPEC_TS__4$ = $r3$.ɵi18nPostprocess($MSG_APP_SPEC_TS__4$, {
"VAR_SELECT": "\uFFFD0:1\uFFFD"
@ -2050,7 +2049,7 @@ describe('i18n support in the view compiler', () => {
const $I18N_EXTERNAL_7842238767399919809$$APP_SPEC_TS_1$ = $r3$.ɵi18nPostprocess($MSG_EXTERNAL_7842238767399919809$$APP_SPEC_TS_1$, {
"VAR_SELECT": "\uFFFD0\uFFFD"
});
const $_c0$ = [1, "ngIf"];
const $_c0$ = [${AttributeMarker.SelectOnly}, "ngIf"];
const $MSG_EXTERNAL_7068143081688428291$$APP_SPEC_TS__3$ = goog.getMsg("{VAR_SELECT, select, 10 {ten} 20 {twenty} 30 {thirty} other {other}}");
const $I18N_EXTERNAL_7068143081688428291$$APP_SPEC_TS__3$ = $r3$.ɵi18nPostprocess($MSG_EXTERNAL_7068143081688428291$$APP_SPEC_TS__3$, {
"VAR_SELECT": "\uFFFD0:1\uFFFD"
@ -2113,7 +2112,7 @@ describe('i18n support in the view compiler', () => {
const $I18N_EXTERNAL_7825031864601787094$$APP_SPEC_TS_1$ = $r3$.ɵi18nPostprocess($MSG_EXTERNAL_7825031864601787094$$APP_SPEC_TS_1$, {
"VAR_SELECT": "\uFFFD0\uFFFD"
});
const $_c0$ = [1, "ngIf"];
const $_c0$ = [${AttributeMarker.SelectOnly}, "ngIf"];
const $MSG_EXTERNAL_2310343208266678305$$APP_SPEC_TS__3$ = goog.getMsg("{VAR_SELECT, select, 10 {ten} 20 {twenty} 30 {thirty} other {other: {$interpolation}}}", {
"interpolation": "\uFFFD1:1\uFFFD"
});

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AttributeMarker, InitialStylingFlags, ViewEncapsulation} from '@angular/compiler/src/core';
import {AttributeMarker, ViewEncapsulation} from '@angular/compiler/src/core';
import {setup} from '@angular/compiler/test/aot/test_util';
import {compile, expectEmit} from './mock_compile';
@ -366,8 +366,8 @@ describe('compiler compliance: styling', () => {
};
const template = `
const $e0_attrs$ = [${AttributeMarker.SelectOnly}, "style"];
const $e0_styling$ = ["opacity","width","height",${InitialStylingFlags.VALUES_MODE},"opacity","1"];
const $_c0$ = [${AttributeMarker.Styles}, "opacity", "1", ${AttributeMarker.SelectOnly}, "style"];
const $_c1$ = ["width", "height"];
MyComponent.ngComponentDef = $r3$.ɵdefineComponent({
type: MyComponent,
@ -379,14 +379,14 @@ describe('compiler compliance: styling', () => {
vars: 1,
template: function MyComponent_Template(rf, $ctx$) {
if (rf & 1) {
$r3$.ɵelementStart(0, "div", $e0_attrs$);
$r3$.ɵelementStyling(null, $e0_styling$, $r3$.ɵdefaultStyleSanitizer);
$r3$.ɵelementStart(0, "div", $_c0$);
$r3$.ɵelementStyling(null, $_c1$, $r3$.ɵdefaultStyleSanitizer);
$r3$.ɵelementEnd();
}
if (rf & 2) {
$r3$.ɵelementStylingMap(0, null, $ctx$.myStyleExp);
$r3$.ɵelementStyleProp(0, 1, $ctx$.myWidth);
$r3$.ɵelementStyleProp(0, 2, $ctx$.myHeight);
$r3$.ɵelementStyleProp(0, 0, $ctx$.myWidth);
$r3$.ɵelementStyleProp(0, 1, $ctx$.myHeight);
$r3$.ɵelementStylingApply(0);
$r3$.ɵelementAttribute(0, "style", $r3$.ɵbind("border-width: 10px"), $r3$.ɵsanitizeStyle);
}
@ -421,7 +421,7 @@ describe('compiler compliance: styling', () => {
};
const template = `
const _c0 = ["background-image"];
const $_c0$ = ["background-image"];
export class MyComponent {
constructor() {
this.myImage = 'url(foo.jpg)';
@ -456,7 +456,6 @@ describe('compiler compliance: styling', () => {
});
it('should support [style.foo.suffix] style bindings with a suffix', () => {
const files = {
app: {
'spec.ts': `
@ -476,7 +475,7 @@ describe('compiler compliance: styling', () => {
};
const template = `
const $e0_styles$= ["font-size"];
const $e0_styles$ = ["font-size"];
template: function MyComponent_Template(rf, ctx) {
if (rf & 1) {
@ -564,8 +563,8 @@ describe('compiler compliance: styling', () => {
};
const template = `
const $e0_attrs$ = [${AttributeMarker.SelectOnly}, "class"];
const $e0_cd$ = ["grape","apple","orange",${InitialStylingFlags.VALUES_MODE},"grape",true];
const $e0_attrs$ = [${AttributeMarker.Classes}, "grape", ${AttributeMarker.SelectOnly}, "class"];
const $e0_bindings$ = ["apple", "orange"];
MyComponent.ngComponentDef = $r3$.ɵdefineComponent({
type: MyComponent,
@ -578,13 +577,13 @@ describe('compiler compliance: styling', () => {
template: function MyComponent_Template(rf, $ctx$) {
if (rf & 1) {
$r3$.ɵelementStart(0, "div", $e0_attrs$);
$r3$.ɵelementStyling($e0_cd$);
$r3$.ɵelementStyling($e0_bindings$);
$r3$.ɵelementEnd();
}
if (rf & 2) {
$r3$.ɵelementStylingMap(0, $ctx$.myClassExp);
$r3$.ɵelementClassProp(0, 1, $ctx$.yesToApple);
$r3$.ɵelementClassProp(0, 2, $ctx$.yesToOrange);
$r3$.ɵelementClassProp(0, 0, $ctx$.yesToApple);
$r3$.ɵelementClassProp(0, 1, $ctx$.yesToOrange);
$r3$.ɵelementStylingApply(0);
$r3$.ɵelementAttribute(0, "class", $r3$.ɵbind("banana"));
}
@ -606,7 +605,7 @@ describe('compiler compliance: styling', () => {
@Component({
selector: 'my-component',
template: \`<div class="foo"
template: \`<div class=" foo "
style="width:100px"
[attr.class]="'round'"
[attr.style]="'height:100px'"></div>\`
@ -620,9 +619,7 @@ describe('compiler compliance: styling', () => {
};
const template = `
const $e0_attrs$ = [${AttributeMarker.SelectOnly}, "class", "style"];
const $e0_cd$ = ["foo",${InitialStylingFlags.VALUES_MODE},"foo",true];
const $e0_sd$ = ["width",${InitialStylingFlags.VALUES_MODE},"width","100px"];
const $e0_attrs$ = [${AttributeMarker.Classes}, "foo", ${AttributeMarker.Styles}, "width", "100px", ${AttributeMarker.SelectOnly}, "class", "style"];
MyComponent.ngComponentDef = $r3$.ɵdefineComponent({
type: MyComponent,
@ -635,7 +632,6 @@ describe('compiler compliance: styling', () => {
template: function MyComponent_Template(rf, $ctx$) {
if (rf & 1) {
$r3$.ɵelementStart(0, "div", $e0_attrs$);
$r3$.ɵelementStyling($e0_cd$, $e0_sd$);
$r3$.ɵelementEnd();
}
if (rf & 2) {
@ -765,10 +761,13 @@ describe('compiler compliance: styling', () => {
};
const template = `
const $e0_classBindings$ = ["foo"];
const $e0_styleBindings$ = ["bar", "baz"];
template: function MyComponent_Template(rf, $ctx$) {
if (rf & 1) {
$r3$.ɵelementStart(0, "div");
$r3$.ɵelementStyling($e0_styling$, $e1_styling$, $r3$.ɵdefaultStyleSanitizer);
$r3$.ɵelementStyling($e0_classBindings$, $e0_styleBindings$, $r3$.ɵdefaultStyleSanitizer);
$r3$.ɵpipe(1, "pipe");
$r3$.ɵpipe(2, "pipe");
$r3$.ɵpipe(3, "pipe");
@ -828,16 +827,18 @@ describe('compiler compliance: styling', () => {
};
const template = `
const _c0 = ["foo", "baz", ${InitialStylingFlags.VALUES_MODE}, "foo", true, "baz", true];
const _c1 = ["width", "height", "color", ${InitialStylingFlags.VALUES_MODE}, "width", "200px", "height", "500px"];
const $e0_attrs$ = [${AttributeMarker.Classes}, "foo", "baz", ${AttributeMarker.Styles}, "width", "200px", "height", "500px"];
const $e0_classBindings$ = ["foo"];
const $e0_styleBindings$ = ["color"];
hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) {
if (rf & 1) {
$r3$.ɵelementStyling(_c0, _c1, $r3$.ɵdefaultStyleSanitizer, ctx);
$r3$.ɵelementHostAttrs(ctx, $e0_attrs$);
$r3$.ɵelementStyling($e0_classBindings$, $e0_styleBindings$, $r3$.ɵdefaultStyleSanitizer, ctx);
}
if (rf & 2) {
$r3$.ɵelementStylingMap(elIndex, ctx.myClass, ctx.myStyle, ctx);
$r3$.ɵelementStyleProp(elIndex, 2, ctx.myColorProp, null, ctx);
$r3$.ɵelementStyleProp(elIndex, 0, ctx.myColorProp, null, ctx);
$r3$.ɵelementClassProp(elIndex, 0, ctx.myFooClass, ctx);
$r3$.ɵelementStylingApply(elIndex, ctx);
}
@ -959,10 +960,10 @@ describe('compiler compliance: styling', () => {
};
const template = `
const _c0 = ["foo"];
const _c1 = ["width"];
const _c2 = ["bar"];
const _c3 = ["height"];
const $widthDir_classes$ = ["foo"];
const $widthDir_styles$ = ["width"];
const $heightDir_classes$ = ["bar"];
const $heightDir_styles$ = ["height"];
function ClassDirective_HostBindings(rf, ctx, elIndex) {
if (rf & 1) {
@ -976,7 +977,7 @@ describe('compiler compliance: styling', () => {
function WidthDirective_HostBindings(rf, ctx, elIndex) {
if (rf & 1) {
$r3$.ɵelementStyling(_c0, _c1, null, ctx);
$r3$.ɵelementStyling($widthDir_classes$, $widthDir_styles$, null, ctx);
}
if (rf & 2) {
$r3$.ɵelementStyleProp(elIndex, 0, ctx.myWidth, null, ctx);
@ -987,7 +988,7 @@ describe('compiler compliance: styling', () => {
function HeightDirective_HostBindings(rf, ctx, elIndex) {
if (rf & 1) {
$r3$.ɵelementStyling(_c2, _c3, null, ctx);
$r3$.ɵelementStyling($heightDir_classes$, $heightDir_styles$, null, ctx);
}
if (rf & 2) {
$r3$.ɵelementStyleProp(elIndex, 0, ctx.myHeight, null, ctx);
@ -1014,7 +1015,8 @@ describe('compiler compliance: styling', () => {
template: '',
host: {
'style': 'width:200px; height:500px',
'class': 'foo baz'
'class': 'foo baz',
'title': 'foo title'
}
})
export class MyComponent {
@ -1029,6 +1031,9 @@ describe('compiler compliance: styling', () => {
@HostBinding('title')
title = 'some title';
@Input('name')
name = '';
}
@NgModule({declarations: [MyComponent]})
@ -1038,13 +1043,13 @@ describe('compiler compliance: styling', () => {
};
const template = `
const $_c0$ = ["foo", "baz", ${InitialStylingFlags.VALUES_MODE}, "foo", true, "baz", true];
const $_c1$ = ["width", "height", ${InitialStylingFlags.VALUES_MODE}, "width", "200px", "height", "500px"];
const $_c0$ = [${AttributeMarker.Classes}, "foo", "baz", ${AttributeMarker.Styles}, "width", "200px", "height", "500px"];
hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) {
if (rf & 1) {
$r3$.ɵallocHostVars(2);
$r3$.ɵelementStyling($_c0$, $_c1$, $r3$.ɵdefaultStyleSanitizer, ctx);
$r3$.ɵelementHostAttrs(ctx, $_c0$);
$r3$.ɵelementStyling(null, null, $r3$.ɵdefaultStyleSanitizer, ctx);
}
if (rf & 2) {
$r3$.ɵelementProperty(elIndex, "id", $r3$.ɵbind(ctx.id), null, true);

View File

@ -379,13 +379,9 @@ export const enum RenderFlags {
Update = 0b10
}
export const enum InitialStylingFlags {
VALUES_MODE = 0b1,
}
// Pasted from render3/interfaces/node.ts
/**
* A set of marker values to be used in the attributes arrays. Those markers indicate that some
* A set of marker values to be used in the attributes arrays. These markers indicate that some
* items are not regular attributes and the processing should be adapted accordingly.
*/
export const enum AttributeMarker {
@ -396,11 +392,48 @@ export const enum AttributeMarker {
*/
NamespaceURI = 0,
/**
* Signals class declaration.
*
* Each value following `Classes` designates a class name to include on the element.
* ## Example:
*
* Given:
* ```
* <div class="foo bar baz">...<d/vi>
* ```
*
* the generated code is:
* ```
* var _c1 = [AttributeMarker.Classes, 'foo', 'bar', 'baz'];
* ```
*/
Classes = 1,
/**
* Signals style declaration.
*
* Each pair of values following `Styles` designates a style name and value to include on the
* element.
* ## Example:
*
* Given:
* ```
* <div style="width:100px; height:200px; color:red">...</div>
* ```
*
* the generated code is:
* ```
* var _c1 = [AttributeMarker.Styles, 'width', '100px', 'height'. '200px', 'color', 'red'];
* ```
*/
Styles = 2,
/**
* This marker indicates that the following attribute names were extracted from bindings (ex.:
* [foo]="exp") and / or event handlers (ex. (bar)="doSth()").
* Taking the above bindings and outputs as an example an attributes array could look as follows:
* ['class', 'fade in', AttributeMarker.SelectOnly, 'foo', 'bar']
*/
SelectOnly = 1
}
SelectOnly = 3,
}

View File

@ -43,6 +43,8 @@ export class Identifiers {
static elementStyling: o.ExternalReference = {name: 'ɵelementStyling', moduleName: CORE};
static elementHostAttrs: o.ExternalReference = {name: 'ɵelementHostAttrs', moduleName: CORE};
static elementStylingMap: o.ExternalReference = {name: 'ɵelementStylingMap', moduleName: CORE};
static elementStyleProp: o.ExternalReference = {name: 'ɵelementStyleProp', moduleName: CORE};

View File

@ -28,7 +28,7 @@ import {Render3ParseResult} from '../r3_template_transform';
import {typeWithParameters} from '../util';
import {R3ComponentDef, R3ComponentMetadata, R3DirectiveDef, R3DirectiveMetadata, R3QueryMetadata} from './api';
import {StylingBuilder, StylingInstruction} from './styling';
import {StylingBuilder, StylingInstruction} from './styling_builder';
import {BindingScope, TemplateDefinitionBuilder, ValueConverter, renderFlagCheckIfStmt} from './template';
import {CONTEXT_NAME, DefinitionMap, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, conditionallyCreateMapObjectLiteral, getQueryPredicate, temporaryAllocator} from './util';
@ -709,16 +709,35 @@ function createHostBindingsFunction(
}
}
if (styleBuilder.hasBindingsOrInitialValues) {
const createInstruction = styleBuilder.buildCreateLevelInstruction(null, constantPool);
if (createInstruction) {
const createStmt = createStylingStmt(createInstruction, bindingContext, bindingFn);
createStatements.push(createStmt);
if (styleBuilder.hasBindingsOrInitialValues()) {
// since we're dealing with directives here and directives have a hostBinding
// function, we need to generate special instructions that deal with styling
// (both bindings and initial values). The instruction below will instruct
// all initial styling (styling that is inside of a host binding within a
// directive) to be attached to the host element of the directive.
const hostAttrsInstruction =
styleBuilder.buildDirectiveHostAttrsInstruction(null, constantPool);
if (hostAttrsInstruction) {
createStatements.push(createStylingStmt(hostAttrsInstruction, bindingContext, bindingFn));
}
// singular style/class bindings (things like `[style.prop]` and `[class.name]`)
// MUST be registered on a given element within the component/directive
// templateFn/hostBindingsFn functions. The instruction below will figure out
// what all the bindings are and then generate the statements required to register
// those bindings to the element via `elementStyling`.
const elementStylingInstruction =
styleBuilder.buildElementStylingInstruction(null, constantPool);
if (elementStylingInstruction) {
createStatements.push(
createStylingStmt(elementStylingInstruction, bindingContext, bindingFn));
}
// finally each binding that was registered in the statement above will need to be added to
// the update block of a component/directive templateFn/hostBindingsFn so that the bindings
// are evaluated and updated for the element.
styleBuilder.buildUpdateLevelInstructions(valueConverter).forEach(instruction => {
const updateStmt = createStylingStmt(instruction, bindingContext, bindingFn);
updateStatements.push(updateStmt);
updateStatements.push(createStylingStmt(instruction, bindingContext, bindingFn));
});
}
}

View File

@ -23,10 +23,15 @@ const enum Char {
*
* @param value string representation of style as used in the `style` attribute in HTML.
* Example: `color: red; height: auto`.
* @returns an object literal. `{ color: 'red', height: 'auto'}`.
* @returns An array of style property name and value pairs, e.g. `['color', 'red', 'height',
* 'auto']`
*/
export function parse(value: string): {[key: string]: any} {
const styles: {[key: string]: any} = {};
export function parse(value: string): string[] {
// we use a string array here instead of a string map
// because a string-map is not guaranteed to retain the
// order of the entries whereas a string array can be
// construted in a [key, value, key, value] format.
const styles: string[] = [];
let i = 0;
let parenDepth = 0;
@ -72,7 +77,7 @@ export function parse(value: string): {[key: string]: any} {
case Char.Semicolon:
if (currentProp && valueStart > 0 && parenDepth === 0 && quote === Char.QuoteNone) {
const styleVal = value.substring(valueStart, i - 1).trim();
styles[currentProp] = valueHasQuotes ? stripUnnecessaryQuotes(styleVal) : styleVal;
styles.push(currentProp, valueHasQuotes ? stripUnnecessaryQuotes(styleVal) : styleVal);
propStart = i;
valueStart = 0;
currentProp = null;
@ -84,7 +89,7 @@ export function parse(value: string): {[key: string]: any} {
if (currentProp && valueStart) {
const styleVal = value.substr(valueStart).trim();
styles[currentProp] = valueHasQuotes ? stripUnnecessaryQuotes(styleVal) : styleVal;
styles.push(currentProp, valueHasQuotes ? stripUnnecessaryQuotes(styleVal) : styleVal);
}
return styles;

View File

@ -6,8 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ConstantPool} from '../../constant_pool';
import {InitialStylingFlags} from '../../core';
import {AST, BindingType, ParseSpan} from '../../expression_parser/ast';
import {AttributeMarker} from '../../core';
import {AST, BindingType} from '../../expression_parser/ast';
import * as o from '../../output/output_ast';
import {ParseSourceSpan} from '../../parse_util';
import * as t from '../r3_ast';
@ -40,6 +40,10 @@ interface BoundStylingEntry {
/**
* Produces creation/update instructions for all styling bindings (class and style)
*
* It also produces the creation instruction to register all initial styling values
* (which are all the static class="..." and style="..." attribute values that exist
* on an element within a template).
*
* The builder class below handles producing instructions for the following cases:
*
* - Static style/class attributes (style="..." and class="...")
@ -63,25 +67,57 @@ interface BoundStylingEntry {
* The creation/update methods within the builder class produce these instructions.
*/
export class StylingBuilder {
public readonly hasBindingsOrInitialValues = false;
/** Whether or not there are any static styling values present */
private _hasInitialValues = false;
/**
* Whether or not there are any styling bindings present
* (i.e. `[style]`, `[class]`, `[style.prop]` or `[class.name]`)
*/
private _hasBindings = false;
/** the input for [class] (if it exists) */
private _classMapInput: BoundStylingEntry|null = null;
/** the input for [style] (if it exists) */
private _styleMapInput: BoundStylingEntry|null = null;
/** an array of each [style.prop] input */
private _singleStyleInputs: BoundStylingEntry[]|null = null;
/** an array of each [class.name] input */
private _singleClassInputs: BoundStylingEntry[]|null = null;
private _lastStylingInput: BoundStylingEntry|null = null;
// maps are used instead of hash maps because a Map will
// retain the ordering of the keys
/**
* Represents the location of each style binding in the template
* (e.g. `<div [style.width]="w" [style.height]="h">` implies
* that `width=0` and `height=1`)
*/
private _stylesIndex = new Map<string, number>();
/**
* Represents the location of each class binding in the template
* (e.g. `<div [class.big]="b" [class.hidden]="h">` implies
* that `big=0` and `hidden=1`)
*/
private _classesIndex = new Map<string, number>();
private _initialStyleValues: {[propName: string]: string} = {};
private _initialClassValues: {[className: string]: boolean} = {};
private _initialStyleValues: string[] = [];
private _initialClassValues: string[] = [];
// certain style properties ALWAYS need sanitization
// this is checked each time new styles are encountered
private _useDefaultSanitizer = false;
private _applyFnRequired = false;
constructor(private _elementIndexExpr: o.Expression, private _directiveExpr: o.Expression|null) {}
hasBindingsOrInitialValues() { return this._hasBindings || this._hasInitialValues; }
/**
* Registers a given input to the styling builder to be later used when producing AOT code.
*
* The code below will only accept the input if it is somehow tied to styling (whether it be
* style/class bindings or static style/class attributes).
*/
registerBoundInput(input: t.BoundAttribute): boolean {
// [attr.style] or [attr.class] are skipped in the code below,
// they should not be treated as styling-based bindings since
@ -117,14 +153,12 @@ export class StylingBuilder {
(this._singleStyleInputs = this._singleStyleInputs || []).push(entry);
this._useDefaultSanitizer = this._useDefaultSanitizer || isStyleSanitizable(propertyName);
registerIntoMap(this._stylesIndex, propertyName);
(this as any).hasBindingsOrInitialValues = true;
} else {
this._useDefaultSanitizer = true;
this._styleMapInput = entry;
}
this._lastStylingInput = entry;
(this as any).hasBindingsOrInitialValues = true;
this._applyFnRequired = true;
this._hasBindings = true;
return entry;
}
@ -133,107 +167,152 @@ export class StylingBuilder {
const entry = { name: className, value, sourceSpan } as BoundStylingEntry;
if (className) {
(this._singleClassInputs = this._singleClassInputs || []).push(entry);
(this as any).hasBindingsOrInitialValues = true;
registerIntoMap(this._classesIndex, className);
} else {
this._classMapInput = entry;
}
this._lastStylingInput = entry;
(this as any).hasBindingsOrInitialValues = true;
this._applyFnRequired = true;
this._hasBindings = true;
return entry;
}
/**
* Registers the element's static style string value to the builder.
*
* @param value the style string (e.g. `width:100px; height:200px;`)
*/
registerStyleAttr(value: string) {
this._initialStyleValues = parseStyle(value);
Object.keys(this._initialStyleValues).forEach(prop => {
registerIntoMap(this._stylesIndex, prop);
(this as any).hasBindingsOrInitialValues = true;
});
this._hasInitialValues = true;
}
/**
* Registers the element's static class string value to the builder.
*
* @param value the className string (e.g. `disabled gold zoom`)
*/
registerClassAttr(value: string) {
this._initialClassValues = {};
value.split(/\s+/g).forEach(className => {
this._initialClassValues[className] = true;
registerIntoMap(this._classesIndex, className);
(this as any).hasBindingsOrInitialValues = true;
});
this._initialClassValues = value.trim().split(/\s+/g);
this._hasInitialValues = true;
}
private _buildInitExpr(registry: Map<string, number>, initialValues: {[key: string]: any}):
o.Expression|null {
const exprs: o.Expression[] = [];
const nameAndValueExprs: o.Expression[] = [];
// _c0 = [prop, prop2, prop3, ...]
registry.forEach((value, key) => {
const keyLiteral = o.literal(key);
exprs.push(keyLiteral);
const initialValue = initialValues[key];
if (initialValue) {
nameAndValueExprs.push(keyLiteral, o.literal(initialValue));
/**
* Appends all styling-related expressions to the provided attrs array.
*
* @param attrs an existing array where each of the styling expressions
* will be inserted into.
*/
populateInitialStylingAttrs(attrs: o.Expression[]): void {
// [CLASS_MARKER, 'foo', 'bar', 'baz' ...]
if (this._initialClassValues.length) {
attrs.push(o.literal(AttributeMarker.Classes));
for (let i = 0; i < this._initialClassValues.length; i++) {
attrs.push(o.literal(this._initialClassValues[i]));
}
});
if (nameAndValueExprs.length) {
// _c0 = [... MARKER ...]
exprs.push(o.literal(InitialStylingFlags.VALUES_MODE));
// _c0 = [prop, VALUE, prop2, VALUE2, ...]
exprs.push(...nameAndValueExprs);
}
return exprs.length ? o.literalArr(exprs) : null;
// [STYLE_MARKER, 'width', '200px', 'height', '100px', ...]
if (this._initialStyleValues.length) {
attrs.push(o.literal(AttributeMarker.Styles));
for (let i = 0; i < this._initialStyleValues.length; i += 2) {
attrs.push(
o.literal(this._initialStyleValues[i]), o.literal(this._initialStyleValues[i + 1]));
}
}
}
buildCreateLevelInstruction(sourceSpan: ParseSourceSpan|null, constantPool: ConstantPool):
/**
* Builds an instruction with all the expressions and parameters for `elementHostAttrs`.
*
* The instruction generation code below is used for producing the AOT statement code which is
* responsible for registering initial styles (within a directive hostBindings' creation block)
* to the directive host element.
*/
buildDirectiveHostAttrsInstruction(sourceSpan: ParseSourceSpan|null, constantPool: ConstantPool):
StylingInstruction|null {
if (this.hasBindingsOrInitialValues) {
const initialClasses = this._buildInitExpr(this._classesIndex, this._initialClassValues);
const initialStyles = this._buildInitExpr(this._stylesIndex, this._initialStyleValues);
// in the event that a [style] binding is used then sanitization will
// always be imported because it is not possible to know ahead of time
// whether style bindings will use or not use any sanitizable properties
// that isStyleSanitizable() will detect
const useSanitizer = this._useDefaultSanitizer;
const params: (o.Expression)[] = [];
if (initialClasses) {
// the template compiler handles initial class styling (e.g. class="foo") values
// in a special command called `elementClass` so that the initial class
// can be processed during runtime. These initial class values are bound to
// a constant because the inital class values do not change (since they're static).
params.push(constantPool.getConstLiteral(initialClasses, true));
} else if (initialStyles || useSanitizer || this._directiveExpr) {
// no point in having an extra `null` value unless there are follow-up params
params.push(o.NULL_EXPR);
}
if (initialStyles) {
// the template compiler handles initial style (e.g. style="foo") values
// in a special command called `elementStyle` so that the initial styles
// can be processed during runtime. These initial styles values are bound to
// a constant because the inital style values do not change (since they're static).
params.push(constantPool.getConstLiteral(initialStyles, true));
} else if (useSanitizer || this._directiveExpr) {
// no point in having an extra `null` value unless there are follow-up params
params.push(o.NULL_EXPR);
}
if (useSanitizer || this._directiveExpr) {
params.push(useSanitizer ? o.importExpr(R3.defaultStyleSanitizer) : o.NULL_EXPR);
if (this._directiveExpr) {
params.push(this._directiveExpr);
if (this._hasInitialValues && this._directiveExpr) {
return {
sourceSpan,
reference: R3.elementHostAttrs,
buildParams: () => {
const attrs: o.Expression[] = [];
this.populateInitialStylingAttrs(attrs);
return [this._directiveExpr !, getConstantLiteralFromArray(constantPool, attrs)];
}
}
return {sourceSpan, reference: R3.elementStyling, buildParams: () => params};
};
}
return null;
}
private _buildStylingMap(valueConverter: ValueConverter): StylingInstruction|null {
/**
* Builds an instruction with all the expressions and parameters for `elementStyling`.
*
* The instruction generation code below is used for producing the AOT statement code which is
* responsible for registering style/class bindings to an element.
*/
buildElementStylingInstruction(sourceSpan: ParseSourceSpan|null, constantPool: ConstantPool):
StylingInstruction|null {
if (this._hasBindings) {
return {
sourceSpan,
reference: R3.elementStyling,
buildParams: () => {
// a string array of every style-based binding
const styleBindingProps =
this._singleStyleInputs ? this._singleStyleInputs.map(i => o.literal(i.name)) : [];
// a string array of every class-based binding
const classBindingNames =
this._singleClassInputs ? this._singleClassInputs.map(i => o.literal(i.name)) : [];
// to salvage space in the AOT generated code, there is no point in passing
// in `null` into a param if any follow-up params are not used. Therefore,
// only when a trailing param is used then it will be filled with nulls in between
// (otherwise a shorter amount of params will be filled). The code below helps
// determine how many params are required in the expression code.
//
// min params => elementStyling()
// max params => elementStyling(classBindings, styleBindings, sanitizer, directive)
let expectedNumberOfArgs = 0;
if (this._directiveExpr) {
expectedNumberOfArgs = 4;
} else if (this._useDefaultSanitizer) {
expectedNumberOfArgs = 3;
} else if (styleBindingProps.length) {
expectedNumberOfArgs = 2;
} else if (classBindingNames.length) {
expectedNumberOfArgs = 1;
}
const params: o.Expression[] = [];
addParam(
params, classBindingNames.length > 0,
getConstantLiteralFromArray(constantPool, classBindingNames), 1,
expectedNumberOfArgs);
addParam(
params, styleBindingProps.length > 0,
getConstantLiteralFromArray(constantPool, styleBindingProps), 2,
expectedNumberOfArgs);
addParam(
params, this._useDefaultSanitizer, o.importExpr(R3.defaultStyleSanitizer), 3,
expectedNumberOfArgs);
if (this._directiveExpr) {
params.push(this._directiveExpr);
}
return params;
}
};
}
return null;
}
/**
* Builds an instruction with all the expressions and parameters for `elementStylingMap`.
*
* The instruction data will contain all expressions for `elementStylingMap` to function
* which include the `[style]` and `[class]` expression params (if they exist) as well as
* the sanitizer and directive reference expression.
*/
buildElementStylingMapInstruction(valueConverter: ValueConverter): StylingInstruction|null {
if (this._classMapInput || this._styleMapInput) {
const stylingInput = this._classMapInput ! || this._styleMapInput !;
@ -332,18 +411,20 @@ export class StylingBuilder {
};
}
/**
* Constructs all instructions which contain the expressions that will be placed
* into the update block of a template function or a directive hostBindings function.
*/
buildUpdateLevelInstructions(valueConverter: ValueConverter) {
const instructions: StylingInstruction[] = [];
if (this.hasBindingsOrInitialValues) {
const mapInstruction = this._buildStylingMap(valueConverter);
if (this._hasBindings) {
const mapInstruction = this.buildElementStylingMapInstruction(valueConverter);
if (mapInstruction) {
instructions.push(mapInstruction);
}
instructions.push(...this._buildStyleInputs(valueConverter));
instructions.push(...this._buildClassInputs(valueConverter));
if (this._applyFnRequired) {
instructions.push(this._buildApplyFn());
}
instructions.push(this._buildApplyFn());
}
return instructions;
}
@ -363,3 +444,26 @@ function isStyleSanitizable(prop: string): boolean {
return prop === 'background-image' || prop === 'background' || prop === 'border-image' ||
prop === 'filter' || prop === 'list-style' || prop === 'list-style-image';
}
/**
* Simple helper function to either provide the constant literal that will house the value
* here or a null value if the provided values are empty.
*/
function getConstantLiteralFromArray(
constantPool: ConstantPool, values: o.Expression[]): o.Expression {
return values.length ? constantPool.getConstLiteral(o.literalArr(values), true) : o.NULL_EXPR;
}
/**
* Simple helper function that adds a parameter or does nothing at all depending on the provided
* predicate and totalExpectedArgs values
*/
function addParam(
params: o.Expression[], predicate: boolean, value: o.Expression, argNumber: number,
totalExpectedArgs: number) {
if (predicate) {
params.push(value);
} else if (argNumber < totalExpectedArgs) {
params.push(o.NULL_EXPR);
}
}

View File

@ -35,7 +35,7 @@ import {I18nContext} from './i18n/context';
import {I18nMetaVisitor} from './i18n/meta';
import {getSerializedI18nContent} from './i18n/serializer';
import {I18N_ICU_MAPPING_PREFIX, assembleBoundTextPlaceholders, assembleI18nBoundString, formatI18nPlaceholderName, getTranslationConstPrefix, getTranslationDeclStmts, icuFromI18nMessage, isI18nRootNode, isSingleI18nIcu, metaFromI18nMessage, placeholdersToParams, wrapI18nPlaceholder} from './i18n/util';
import {StylingBuilder, StylingInstruction} from './styling';
import {StylingBuilder, StylingInstruction} from './styling_builder';
import {CONTEXT_NAME, IMPLICIT_REFERENCE, NON_BINDABLE_ATTR, REFERENCE_PREFIX, RENDER_FLAGS, asLiteral, getAttrsForDirectiveMatching, invalid, trimTrailingNulls, unsupported} from './util';
function mapBindingToInstruction(type: BindingType): o.ExternalReference|undefined {
@ -532,7 +532,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
// this will build the instructions so that they fall into the following syntax
// add attributes for directive matching purposes
attributes.push(...this.prepareSyntheticAndSelectOnlyAttrs(allOtherInputs, element.outputs));
attributes.push(...this.prepareSyntheticAndSelectOnlyAttrs(
allOtherInputs, element.outputs, stylingBuilder));
parameters.push(this.toAttrsParam(attributes));
// local refs (ex.: <div #foo #bar="baz">)
@ -562,11 +563,11 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
return element.children.length > 0;
};
const createSelfClosingInstruction = !stylingBuilder.hasBindingsOrInitialValues &&
const createSelfClosingInstruction = !stylingBuilder.hasBindingsOrInitialValues() &&
!isNgContainer && element.outputs.length === 0 && i18nAttrs.length === 0 && !hasChildren();
const createSelfClosingI18nInstruction = !createSelfClosingInstruction &&
!stylingBuilder.hasBindingsOrInitialValues && hasTextChildrenOnly(element.children);
!stylingBuilder.hasBindingsOrInitialValues() && hasTextChildrenOnly(element.children);
if (createSelfClosingInstruction) {
this.creationInstruction(element.sourceSpan, R3.element, trimTrailingNulls(parameters));
@ -616,10 +617,16 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
}
}
// initial styling for static style="..." and class="..." attributes
// The style bindings code is placed into two distinct blocks within the template function AOT
// code: creation and update. The creation code contains the `elementStyling` instructions
// which will apply the collected binding values to the element. `elementStyling` is
// designed to run inside of `elementStart` and `elementEnd`. The update instructions
// (things like `elementStyleProp`, `elementClassProp`, etc..) are applied later on in this
// file
this.processStylingInstruction(
implicit,
stylingBuilder.buildCreateLevelInstruction(element.sourceSpan, this.constantPool), true);
stylingBuilder.buildElementStylingInstruction(element.sourceSpan, this.constantPool),
true);
// Generate Listeners (outputs)
element.outputs.forEach((outputAst: t.BoundEvent) => {
@ -629,6 +636,10 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
});
}
// the code here will collect all update-level styling instructions and add them to the
// update block of the template function AOT code. Instructions like `elementStyleProp`,
// `elementStylingMap`, `elementClassProp` and `elementStylingApply` are all generated
// and assign in the code below.
stylingBuilder.buildUpdateLevelInstructions(this._valueConverter).forEach(instruction => {
this.processStylingInstruction(implicit, instruction, false);
});
@ -934,8 +945,26 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
}
}
private prepareSyntheticAndSelectOnlyAttrs(inputs: t.BoundAttribute[], outputs: t.BoundEvent[]):
o.Expression[] {
/**
* Prepares all attribute expression values for the `TAttributes` array.
*
* The purpose of this function is to properly construct an attributes array that
* is passed into the `elementStart` (or just `element`) functions. Because there
* are many different types of attributes, the array needs to be constructed in a
* special way so that `elementStart` can properly evaluate them.
*
* The format looks like this:
*
* ```
* attrs = [prop, value, prop2, value2,
* CLASSES, class1, class2,
* STYLES, style1, value1, style2, value2,
* SELECT_ONLY, name1, name2, name2, ...]
* ```
*/
private prepareSyntheticAndSelectOnlyAttrs(
inputs: t.BoundAttribute[], outputs: t.BoundEvent[],
styles?: StylingBuilder): o.Expression[] {
const attrExprs: o.Expression[] = [];
const nonSyntheticInputs: t.BoundAttribute[] = [];
@ -954,6 +983,13 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
});
}
// it's important that this occurs before SelectOnly because once `elementStart`
// comes across the SelectOnly marker then it will continue reading each value as
// as single property value cell by cell.
if (styles) {
styles.populateInitialStylingAttrs(attrExprs);
}
if (nonSyntheticInputs.length || outputs.length) {
attrExprs.push(o.literal(core.AttributeMarker.SelectOnly));
nonSyntheticInputs.forEach((i: t.BoundAttribute) => attrExprs.push(asLiteral(i.name)));

View File

@ -10,55 +10,53 @@ import {hyphenate, parse as parseStyle, stripUnnecessaryQuotes} from '../../src/
describe('style parsing', () => {
it('should parse empty or blank strings', () => {
const result1 = parseStyle('');
expect(result1).toEqual({});
expect(result1).toEqual([]);
const result2 = parseStyle(' ');
expect(result2).toEqual({});
expect(result2).toEqual([]);
});
it('should parse a string into a key/value map', () => {
const result = parseStyle('width:100px;height:200px;opacity:0');
expect(result).toEqual({width: '100px', height: '200px', opacity: '0'});
expect(result).toEqual(['width', '100px', 'height', '200px', 'opacity', '0']);
});
it('should trim values and properties', () => {
const result = parseStyle('width :333px ; height:666px ; opacity: 0.5;');
expect(result).toEqual({width: '333px', height: '666px', opacity: '0.5'});
expect(result).toEqual(['width', '333px', 'height', '666px', 'opacity', '0.5']);
});
it('should chomp out start/end quotes', () => {
const result = parseStyle(
'content: "foo"; opacity: \'0.5\'; font-family: "Verdana", Helvetica, "sans-serif"');
expect(result).toEqual(
{content: 'foo', opacity: '0.5', 'font-family': '"Verdana", Helvetica, "sans-serif"'});
['content', 'foo', 'opacity', '0.5', 'font-family', '"Verdana", Helvetica, "sans-serif"']);
});
it('should not mess up with quoted strings that contain [:;] values', () => {
const result = parseStyle('content: "foo; man: guy"; width: 100px');
expect(result).toEqual({content: 'foo; man: guy', width: '100px'});
expect(result).toEqual(['content', 'foo; man: guy', 'width', '100px']);
});
it('should not mess up with quoted strings that contain inner quote values', () => {
const quoteStr = '"one \'two\' three \"four\" five"';
const result = parseStyle(`content: ${quoteStr}; width: 123px`);
expect(result).toEqual({content: quoteStr, width: '123px'});
expect(result).toEqual(['content', quoteStr, 'width', '123px']);
});
it('should respect parenthesis that are placed within a style', () => {
const result = parseStyle('background-image: url("foo.jpg")');
expect(result).toEqual({'background-image': 'url("foo.jpg")'});
expect(result).toEqual(['background-image', 'url("foo.jpg")']);
});
it('should respect multi-level parenthesis that contain special [:;] characters', () => {
const result = parseStyle('color: rgba(calc(50 * 4), var(--cool), :5;); height: 100px;');
expect(result).toEqual({color: 'rgba(calc(50 * 4), var(--cool), :5;)', height: '100px'});
expect(result).toEqual(['color', 'rgba(calc(50 * 4), var(--cool), :5;)', 'height', '100px']);
});
it('should hyphenate style properties from camel case', () => {
const result = parseStyle('borderWidth: 200px');
expect(result).toEqual({
'border-width': '200px',
});
expect(result).toEqual(['border-width', '200px']);
});
describe('quote chomping', () => {

View File

@ -92,6 +92,7 @@ export {
elementContainerStart as ɵelementContainerStart,
elementContainerEnd as ɵelementContainerEnd,
elementStyling as ɵelementStyling,
elementHostAttrs as ɵelementHostAttrs,
elementStylingMap as ɵelementStylingMap,
elementStyleProp as ɵelementStyleProp,
elementStylingApply as ɵelementStylingApply,

View File

@ -12,7 +12,7 @@ import {getComponent, getContext, getInjectionTokens, getInjector, getListeners,
import {TNode} from '../render3/interfaces/node';
import {StylingIndex} from '../render3/interfaces/styling';
import {TVIEW} from '../render3/interfaces/view';
import {getProp, getValue, isClassBased} from '../render3/styling/class_and_style_bindings';
import {getProp, getValue, isClassBasedValue} from '../render3/styling/class_and_style_bindings';
import {getStylingContext} from '../render3/styling/util';
import {DebugContext} from '../view/index';
@ -273,7 +273,7 @@ class DebugElement__POST_R3__ extends DebugNode__POST_R3__ implements DebugEleme
if (stylingContext) {
for (let i = StylingIndex.SingleStylesStartPosition; i < lNode.length;
i += StylingIndex.Size) {
if (isClassBased(lNode, i)) {
if (isClassBasedValue(lNode, i)) {
const className = getProp(lNode, i);
const value = getValue(lNode, i);
if (typeof value == 'boolean') {
@ -303,7 +303,7 @@ class DebugElement__POST_R3__ extends DebugNode__POST_R3__ implements DebugEleme
if (stylingContext) {
for (let i = StylingIndex.SingleStylesStartPosition; i < lNode.length;
i += StylingIndex.Size) {
if (!isClassBased(lNode, i)) {
if (!isClassBasedValue(lNode, i)) {
const styleName = getProp(lNode, i);
const value = getValue(lNode, i) as string | null;
if (value !== null) {

View File

@ -7,12 +7,13 @@
*/
import './ng_dev_mode';
import {assertDomNode} from './assert';
import {EMPTY_ARRAY} from './definition';
import {EMPTY_ARRAY} from './empty';
import {LContext, MONKEY_PATCH_KEY_NAME} from './interfaces/context';
import {TNode, TNodeFlags} from './interfaces/node';
import {RElement} from './interfaces/renderer';
import {CONTEXT, HEADER_OFFSET, HOST, LView, TVIEW} from './interfaces/view';
import {getComponentViewByIndex, getNativeByTNode, getTNode, readElementValue, readPatchedData} from './util';
import {getComponentViewByIndex, getNativeByTNode, readElementValue, readPatchedData} from './util';
/** Returns the matching `LContext` data for a given DOM node, directive or component instance.

View File

@ -9,22 +9,15 @@
import './ng_dev_mode';
import {ChangeDetectionStrategy} from '../change_detection/constants';
import {Provider} from '../di/provider';
import {NgModuleDef} from '../metadata/ng_module';
import {ViewEncapsulation} from '../metadata/view';
import {Mutable, Type} from '../type';
import {noSideEffects, stringify} from '../util';
import {EMPTY_ARRAY, EMPTY_OBJ} from './empty';
import {NG_COMPONENT_DEF, NG_DIRECTIVE_DEF, NG_MODULE_DEF, NG_PIPE_DEF} from './fields';
import {BaseDef, ComponentDef, ComponentDefFeature, ComponentQuery, ComponentTemplate, ComponentType, DirectiveDef, DirectiveDefFeature, DirectiveType, DirectiveTypesOrFactory, HostBindingsFunction, PipeDef, PipeType, PipeTypesOrFactory} from './interfaces/definition';
import {CssSelectorList, SelectorFlags} from './interfaces/projection';
import {CssSelectorList} from './interfaces/projection';
export const EMPTY: {} = {};
export const EMPTY_ARRAY: any[] = [];
if (typeof ngDevMode !== 'undefined' && ngDevMode) {
Object.freeze(EMPTY);
Object.freeze(EMPTY_ARRAY);
}
let _renderCompCount = 0;
/**
@ -389,7 +382,7 @@ export function defineNgModule<T>(def: {type: T} & Partial<NgModuleDef<T>>): nev
*/
function invertObject(obj: any, secondary?: any): any {
if (obj == null) return EMPTY;
if (obj == null) return EMPTY_OBJ;
const newLookup: any = {};
for (const minifiedKey in obj) {
if (obj.hasOwnProperty(minifiedKey)) {

View File

@ -7,6 +7,7 @@
*/
import {Injector} from '../di/injector';
import {assertDefined} from './assert';
import {discoverLocalRefs, getComponentAtNodeIndex, getDirectivesAtNodeIndex, getLContext} from './context_discovery';
import {NodeInjector} from './di';
@ -14,7 +15,8 @@ import {LContext} from './interfaces/context';
import {DirectiveDef} from './interfaces/definition';
import {TElementNode, TNode, TNodeProviderIndexes} from './interfaces/node';
import {CLEANUP, CONTEXT, FLAGS, HOST, LView, LViewFlags, PARENT, RootContext, TVIEW} from './interfaces/view';
import {readPatchedLView, stringify} from './util';
import {readElementValue, readPatchedLView, stringify} from './util';
/**
@ -327,7 +329,7 @@ export function getListeners(element: Element): Listener[] {
const secondParam = tCleanup[i++];
if (typeof firstParam === 'string') {
const name: string = firstParam;
const listenerElement: Element = lView[secondParam];
const listenerElement = readElementValue(lView[secondParam]) as any as Element;
const callback: (value: any) => any = lCleanup[tCleanup[i++]];
const useCaptureOrIndx = tCleanup[i++];
// if useCaptureOrIndx is boolean then report it as is.

View File

@ -0,0 +1,24 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 './ng_dev_mode';
/**
* This file contains reuseable "empty" symbols that can be used as default return values
* in different parts of the rendering code. Because the same symbols are returned, this
* allows for identity checks against these values to be consistently used by the framework
* code.
*/
export const EMPTY_OBJ: {} = {};
export const EMPTY_ARRAY: any[] = [];
// freezing the values prevents any code from accidentally inserting new values in
if (typeof ngDevMode !== 'undefined' && ngDevMode) {
Object.freeze(EMPTY_OBJ);
Object.freeze(EMPTY_ARRAY);
}

View File

@ -8,8 +8,8 @@
import {Type} from '../../type';
import {fillProperties} from '../../util/property';
import {EMPTY, EMPTY_ARRAY} from '../definition';
import {ComponentDef, ComponentTemplate, DirectiveDef, DirectiveDefFeature, RenderFlags} from '../interfaces/definition';
import {EMPTY_ARRAY, EMPTY_OBJ} from '../empty';
import {ComponentDef, DirectiveDef, DirectiveDefFeature, RenderFlags} from '../interfaces/definition';
@ -178,7 +178,7 @@ export function InheritDefinitionFeature(definition: DirectiveDef<any>| Componen
function maybeUnwrapEmpty<T>(value: T[]): T[];
function maybeUnwrapEmpty<T>(value: T): T;
function maybeUnwrapEmpty(value: any): any {
if (value === EMPTY) {
if (value === EMPTY_OBJ) {
return {};
} else if (value === EMPTY_ARRAY) {
return [];

View File

@ -48,8 +48,8 @@ export {
elementContainerStart,
elementContainerEnd,
elementStyling,
elementHostAttrs,
elementStylingMap,
elementStyleProp,
elementStylingApply,
@ -79,7 +79,7 @@ export {
directiveInject,
injectAttribute,
getCurrentView
} from './instructions';

View File

@ -15,6 +15,7 @@ import {Sanitizer} from '../sanitization/security';
import {StyleSanitizeFn} from '../sanitization/style_sanitizer';
import {Type} from '../type';
import {normalizeDebugBindingName, normalizeDebugBindingValue} from '../util/ng_reflect';
import {assertDataInRange, assertDefined, assertEqual, assertHasParent, assertLessThan, assertNotEqual, assertPreviousIsParent} from './assert';
import {bindingUpdated, bindingUpdated2, bindingUpdated3, bindingUpdated4} from './bindings';
import {attachPatchData, getComponentViewByInstance} from './context_discovery';
@ -22,7 +23,7 @@ import {diPublicInInjector, getNodeInjectable, getOrCreateInjectable, getOrCreat
import {throwMultipleComponentError} from './errors';
import {executeHooks, executeInitHooks, queueInitHooks, queueLifecycleHooks} from './hooks';
import {ACTIVE_INDEX, LContainer, VIEWS} from './interfaces/container';
import {ComponentDef, ComponentQuery, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, InitialStylingFlags, PipeDefListOrFactory, RenderFlags} from './interfaces/definition';
import {ComponentDef, ComponentQuery, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, PipeDefListOrFactory, RenderFlags} from './interfaces/definition';
import {INJECTOR_BLOOM_PARENT_SIZE, NodeInjectorFactory} from './interfaces/injector';
import {AttributeMarker, InitialInputData, InitialInputs, LocalRefExtractor, PropertyAliasValue, PropertyAliases, TAttributes, TContainerNode, TElementContainerNode, TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TProjectionNode, TViewNode} from './interfaces/node';
import {PlayerFactory} from './interfaces/player';
@ -30,19 +31,19 @@ import {CssSelectorList, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection'
import {LQueries} from './interfaces/query';
import {ProceduralRenderer3, RComment, RElement, RText, Renderer3, RendererFactory3, isProceduralRenderer} from './interfaces/renderer';
import {SanitizerFn} from './interfaces/sanitization';
import {StylingIndex} from './interfaces/styling';
import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTENT_QUERIES, CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, INJECTOR, LView, LViewFlags, NEXT, OpaqueViewState, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, TAIL, TVIEW, TView} from './interfaces/view';
import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert';
import {appendChild, appendProjectedNode, createTextNode, getLViewChild, getRenderParent, insertView, removeView} from './node_manipulation';
import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher';
import {decreaseElementDepthCount, enterView, getBindingsEnabled, getCheckNoChangesMode, getContextLView, getCurrentDirectiveDef, getElementDepthCount, getFirstTemplatePass, getIsParent, getLView, getPreviousOrParentTNode, increaseElementDepthCount, isCreationMode, leaveView, nextContextImpl, resetComponentState, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setFirstTemplatePass, setIsParent, setPreviousOrParentTNode} from './state';
import {createStylingContextTemplate, renderStyleAndClassBindings, setStyle, updateClassProp as updateElementClassProp, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling/class_and_style_bindings';
import {getInitialClassNameValue, initializeStaticContext as initializeStaticStylingContext, patchContextWithStaticAttrs, renderInitialStylesAndClasses, renderStyling, updateClassProp as updateElementClassProp, updateContextWithBindings, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling/class_and_style_bindings';
import {BoundPlayerFactory} from './styling/player_factory';
import {getStylingContext, isAnimationProp} from './styling/util';
import {createEmptyStylingContext, getStylingContext, hasClassInput, hasStyling, isAnimationProp} from './styling/util';
import {NO_CHANGE} from './tokens';
import {findComponentView, getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getRootContext, getRootView, getTNode, isComponent, isComponentDef, loadInternal, readElementValue, readPatchedLView, stringify} from './util';
/**
* A permanent marker promise which signifies that the current CD tree is
* clean.
@ -456,7 +457,8 @@ export function namespaceHTML() {
*
* @param index Index of the element in the data array
* @param name Name of the DOM Node
* @param attrs Statically bound set of attributes to be written into the DOM element on creation.
* @param attrs Statically bound set of attributes, classes, and styles to be written into the DOM
* element on creation. Use [AttributeMarker] to denote the meaning of this array.
* @param localRefs A set of local reference bindings on the element.
*/
export function element(
@ -526,7 +528,8 @@ export function elementContainerEnd(): void {
*
* @param index Index of the element in the LView array
* @param name Name of the DOM Node
* @param attrs Statically bound set of attributes to be written into the DOM element on creation.
* @param attrs Statically bound set of attributes, classes, and styles to be written into the DOM
* element on creation. Use [AttributeMarker] to denote the meaning of this array.
* @param localRefs A set of local reference bindings on the element.
*
* Attributes and localRefs are passed as an array of strings where elements with an even index
@ -550,6 +553,14 @@ export function elementStart(
const tNode = createNodeAtIndex(index, TNodeType.Element, native !, name, attrs || null);
if (attrs) {
// it's important to only prepare styling-related datastructures once for a given
// tNode and not each time an element is created. Also, the styling code is designed
// to be patched and constructed at various points, but only up until the first element
// is created. Then the styling context is locked and can only be instantiated for each
// successive element that is created.
if (tView.firstTemplatePass && !tNode.stylingTemplate && hasStyling(attrs)) {
tNode.stylingTemplate = initializeStaticStylingContext(attrs);
}
setUpAttributes(native, attrs);
}
@ -563,6 +574,23 @@ export function elementStart(
attachPatchData(native, lView);
}
increaseElementDepthCount();
// if a directive contains a host binding for "class" then all class-based data will
// flow through that (except for `[class.prop]` bindings). This also includes initial
// static class values as well. (Note that this will be fixed once map-based `[style]`
// and `[class]` bindings work for multiple directives.)
if (tView.firstTemplatePass) {
const inputData = initializeTNodeInputs(tNode);
if (inputData && inputData.hasOwnProperty('class')) {
tNode.flags |= TNodeFlags.hasClassInput;
}
}
// There is no point in rendering styles when a class directive is present since
// it will take that over for us (this will be removed once #FW-882 is in).
if (tNode.stylingTemplate && (tNode.flags & TNodeFlags.hasClassInput) === 0) {
renderInitialStylesAndClasses(native, tNode.stylingTemplate, lView[RENDERER]);
}
}
/**
@ -721,25 +749,28 @@ function setUpAttributes(native: RElement, attrs: TAttributes): void {
let i = 0;
while (i < attrs.length) {
const attrName = attrs[i];
if (attrName === AttributeMarker.SelectOnly) break;
if (attrName === NG_PROJECT_AS_ATTR_NAME) {
i += 2;
} else {
ngDevMode && ngDevMode.rendererSetAttribute++;
const attrName = attrs[i++];
if (typeof attrName == 'number') {
if (attrName === AttributeMarker.NamespaceURI) {
// Namespaced attributes
const namespaceURI = attrs[i + 1] as string;
const attrName = attrs[i + 2] as string;
const attrVal = attrs[i + 3] as string;
const namespaceURI = attrs[i++] as string;
const attrName = attrs[i++] as string;
const attrVal = attrs[i++] as string;
ngDevMode && ngDevMode.rendererSetAttribute++;
isProc ?
(renderer as ProceduralRenderer3)
.setAttribute(native, attrName, attrVal, namespaceURI) :
native.setAttributeNS(namespaceURI, attrName, attrVal);
i += 4;
} else {
// All other `AttributeMarker`s are ignored here.
break;
}
} else {
/// attrName is string;
const attrVal = attrs[i++];
if (attrName !== NG_PROJECT_AS_ATTR_NAME) {
// Standard attributes
const attrVal = attrs[i + 1];
ngDevMode && ngDevMode.rendererSetAttribute++;
if (isAnimationProp(attrName)) {
if (isProc) {
(renderer as ProceduralRenderer3).setProperty(native, attrName, attrVal);
@ -750,7 +781,6 @@ function setUpAttributes(native: RElement, attrs: TAttributes): void {
.setAttribute(native, attrName as string, attrVal as string) :
native.setAttribute(attrName as string, attrVal as string);
}
i += 2;
}
}
}
@ -902,6 +932,15 @@ export function elementEnd(): void {
queueLifecycleHooks(getLView()[TVIEW], previousOrParentTNode);
decreaseElementDepthCount();
// this is fired at the end of elementEnd because ALL of the stylingBindings code
// (for directives and the template) have now executed which means the styling
// context can be instantiated properly.
if (hasClassInput(previousOrParentTNode)) {
const stylingContext = getStylingContext(previousOrParentTNode.index, lView);
setInputsForProperty(
lView, previousOrParentTNode.inputs !['class'] !, getInitialClassNameValue(stylingContext));
}
}
/**
@ -1096,119 +1135,84 @@ function generatePropertyAliases(tNode: TNode, direction: BindingDirection): Pro
return propStore;
}
/**
* Add or remove a class in a `classList` on a DOM element.
*
* This instruction is meant to handle the [class.foo]="exp" case
*
* @param index The index of the element to update in the data array
* @param classIndex Index of class to toggle. Because it is going to DOM, this is not subject to
* renaming as part of minification.
* @param value A value indicating if a given class should be added or removed.
* @param directive the ref to the directive that is attempting to change styling.
*/
export function elementClassProp(
index: number, classIndex: number, value: boolean | PlayerFactory, directive?: {}): void {
if (directive != undefined) {
return hackImplementationOfElementClassProp(
index, classIndex, value, directive); // proper supported in next PR
}
const val =
(value instanceof BoundPlayerFactory) ? (value as BoundPlayerFactory<boolean>) : (!!value);
updateElementClassProp(getStylingContext(index + HEADER_OFFSET, getLView()), classIndex, val);
}
/**
* Assign any inline style values to the element during creation mode.
*
* This instruction is meant to be called during creation mode to apply all styling
* (e.g. `style="..."`) values to the element. This is also where the provided index
* value is allocated for the styling details for its corresponding element (the element
* index is the previous index value from this one).
* This instruction is meant to be called during creation mode to register all
* dynamic style and class bindings on the element. Note for static values (no binding)
* see `elementStart` and `elementHostAttrs`.
*
* (Note this function calls `elementStylingApply` immediately when called.)
* @param classBindingNames An array containing bindable class names.
* The `elementClassProp` refers to the class name by index in this array.
* (i.e. `['foo', 'bar']` means `foo=0` and `bar=1`).
* @param styleBindingNames An array containing bindable style properties.
* The `elementStyleProp` refers to the class name by index in this array.
* (i.e. `['width', 'height']` means `width=0` and `height=1`).
* @param styleSanitizer An optional sanitizer function that will be used to sanitize any CSS
* property values that are applied to the element (during rendering).
* Note that the sanitizer instance itself is tied to the `directive` (if provided).
* @param directive A directive instance the styling is associated with. If not provided
* current view's controller instance is assumed.
*
*
* @param index Index value which will be allocated to store styling data for the element.
* (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 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).
* @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 styleSanitizer An optional sanitizer function that will be used (if provided)
* to sanitize the any CSS property values that are applied to the element (during rendering).
* @param directive the ref to the directive that is attempting to change styling.
* @publicApi
*/
export function elementStyling(
classDeclarations?: (string | boolean | InitialStylingFlags)[] | null,
styleDeclarations?: (string | boolean | InitialStylingFlags)[] | null,
classBindingNames?: string[] | null, styleBindingNames?: string[] | null,
styleSanitizer?: StyleSanitizeFn | null, directive?: {}): void {
if (directive != undefined) {
isCreationMode() &&
hackImplementationOfElementStyling(
classDeclarations || null, styleDeclarations || null, styleSanitizer || null,
directive); // supported in next PR
return;
}
const tNode = getPreviousOrParentTNode();
const inputData = initializeTNodeInputs(tNode);
if (!tNode.stylingTemplate) {
const hasClassInput = inputData && inputData.hasOwnProperty('class') ? true : false;
if (hasClassInput) {
tNode.flags |= TNodeFlags.hasClassInput;
}
// initialize the styling template.
tNode.stylingTemplate = createStylingContextTemplate(
classDeclarations, styleDeclarations, styleSanitizer, hasClassInput);
}
if (styleDeclarations && styleDeclarations.length ||
classDeclarations && classDeclarations.length) {
const index = tNode.index;
if (delegateToClassInput(tNode)) {
const lView = getLView();
const stylingContext = getStylingContext(index, lView);
const initialClasses = stylingContext[StylingIndex.PreviousOrCachedMultiClassValue] as string;
setInputsForProperty(lView, tNode.inputs !['class'] !, initialClasses);
}
elementStylingApply(index - HEADER_OFFSET);
tNode.stylingTemplate = createEmptyStylingContext();
}
updateContextWithBindings(
tNode.stylingTemplate !, directive || null, classBindingNames, styleBindingNames,
styleSanitizer, hasClassInput(tNode));
}
/**
* Assign static styling values to a host element.
*
* NOTE: This instruction is meant to used from `hostBindings` function only.
*
* @param directive A directive instance the styling is associated with.
* @param attrs An array containing class and styling information. The values must be marked with
* `AttributeMarker`.
*
* ```
* var attrs = [AttributeMarker.Classes, 'foo', 'bar',
* AttributeMarker.Styles, 'width', '100px', 'height, '200px']
* elementHostAttrs(directive, attrs);
* ```
*
* @publicApi
*/
export function elementHostAttrs(directive: any, attrs: TAttributes) {
const tNode = getPreviousOrParentTNode();
if (!tNode.stylingTemplate) {
tNode.stylingTemplate = initializeStaticStylingContext(attrs);
}
patchContextWithStaticAttrs(tNode.stylingTemplate, attrs, directive);
}
/**
* Apply all styling values to the element which have been queued by any styling instructions.
* Apply styling binding to the element.
*
* This instruction is meant to be run once one or more `elementStyle` and/or `elementStyleProp`
* have been issued against the element. This function will also determine if any styles have
* changed and will then skip the operation if there is nothing new to render.
* This instruction is meant to be run after `elementStyle` and/or `elementStyleProp`.
* if any styling bindings have changed then the changes are flushed to the element.
*
* Once called then all queued styles will be flushed.
*
* @param index Index of the element's styling storage that will be rendered.
* (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 directive the ref to the directive that is attempting to change styling.
* @param index Index of the element's with which styling is associated.
* @param directive Directive instance that is attempting to change styling. (Defaults to the
* component of the current view).
components
*
* @publicApi
*/
export function elementStylingApply(index: number, directive?: {}): void {
if (directive != undefined) {
return hackImplementationOfElementStylingApply(index, directive); // supported in next PR
}
export function elementStylingApply(index: number, directive?: any): void {
const lView = getLView();
const isFirstRender = (lView[FLAGS] & LViewFlags.FirstLViewPass) !== 0;
const totalPlayersQueued = renderStyleAndClassBindings(
getStylingContext(index + HEADER_OFFSET, lView), lView[RENDERER], lView, isFirstRender);
const totalPlayersQueued = renderStyling(
getStylingContext(index + HEADER_OFFSET, lView), lView[RENDERER], lView, isFirstRender, null,
null, directive);
if (totalPlayersQueued > 0) {
const rootContext = getRootContext(lView);
scheduleTick(rootContext, RootContextFlags.FlushPlayers);
@ -1216,29 +1220,34 @@ export function elementStylingApply(index: number, directive?: {}): void {
}
/**
* Queue a given style to be rendered on an Element.
* Update a style bindings value on an element.
*
* If the style value is `null` then it will be removed from the element
* (or assigned a different value depending if there are any styles placed
* on the element with `elementStyle` or any styles that are present
* from when the element was created (with `elementStyling`).
*
* (Note that the styling instruction will not be applied until `elementStylingApply` is called.)
* (Note that the styling element is updated as part of `elementStylingApply`.)
*
* @param index Index of the element's styling storage to change in the data array.
* (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 styleIndex Index of the style property on this element. (Monotonically increasing.)
* @param value New value to write (null to remove).
* @param index Index of the element's with which styling is associated.
* @param styleIndex Index of style to update. This index value refers to the
* index of the style in the style bindings array that was passed into
* `elementStlyingBindings`.
* @param value New value to write (null to remove). Note that if a directive also
* attempts to write to the same binding value then it will only be able to
* do so if the template binding value is `null` (or doesn't exist at all).
* @param suffix Optional suffix. Used with scalar values to add unit such as `px`.
* Note that when a suffix is provided then the underlying sanitizer will
* be ignored.
* @param directive the ref to the directive that is attempting to change styling.
* @param directive Directive instance that is attempting to change styling. (Defaults to the
* component of the current view).
components
*
* @publicApi
*/
export function elementStyleProp(
index: number, styleIndex: number, value: string | number | String | PlayerFactory | null,
suffix?: string, directive?: {}): void {
suffix?: string | null, directive?: {}): void {
let valueToAdd: string|null = null;
if (value !== null) {
if (suffix) {
@ -1253,35 +1262,59 @@ export function elementStyleProp(
valueToAdd = value as any as string;
}
}
if (directive != undefined) {
hackImplementationOfElementStyleProp(index, styleIndex, valueToAdd, suffix, directive);
} else {
updateElementStyleProp(
getStylingContext(index + HEADER_OFFSET, getLView()), styleIndex, valueToAdd);
}
updateElementStyleProp(
getStylingContext(index + HEADER_OFFSET, getLView()), styleIndex, valueToAdd, directive);
}
/**
* Queue a key/value map of styles to be rendered on an Element.
* Add or remove a class via a class binding on a DOM element.
*
* This instruction is meant to handle the `[style]="exp"` usage. When styles are applied to
* the Element they will then be placed with respect to any styles set with `elementStyleProp`.
* If any styles are set to `null` then they will be removed from the element (unless the same
* style properties have been assigned to the element during creation using `elementStyling`).
* This instruction is meant to handle the [class.foo]="exp" case and, therefore,
* the class itself must already be applied using `elementStyling` within
* the creation block.
*
* @param index Index of the element's with which styling is associated.
* @param classIndex Index of class to toggle. This index value refers to the
* index of the class in the class bindings array that was passed into
* `elementStlyingBindings` (which is meant to be called before this
* function is).
* @param value A true/false value which will turn the class on or off.
* @param directive Directive instance that is attempting to change styling. (Defaults to the
* component of the current view).
components
*
* @publicApi
*/
export function elementClassProp(
index: number, classIndex: number, value: boolean | PlayerFactory, directive?: {}): void {
const onOrOffClassValue =
(value instanceof BoundPlayerFactory) ? (value as BoundPlayerFactory<boolean>) : (!!value);
updateElementClassProp(
getStylingContext(index + HEADER_OFFSET, getLView()), classIndex, onOrOffClassValue,
directive);
}
/**
* Update style and/or class bindings using object literal.
*
* This instruction is meant apply styling via the `[style]="exp"` and `[class]="exp"` template
* bindings. When styles are applied to the Element they will then be placed with respect to
* any styles set with `elementStyleProp`. If any styles are set to `null` then they will be
* removed from the element.
*
* (Note that the styling instruction will not be applied until `elementStylingApply` is called.)
*
* @param index Index of the element's styling storage to change in the data array.
* (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 index Index of the element's with which styling is associated.
* @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.
* @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 directive the ref to the directive that is attempting to change styling.
* @param directive Directive instance that is attempting to change styling. (Defaults to the
* component of the current view).
*
* @publicApi
*/
export function elementStylingMap<T>(
index: number, classes: {[key: string]: any} | string | NO_CHANGE | null,
@ -1292,113 +1325,24 @@ export function elementStylingMap<T>(
const lView = getLView();
const tNode = getTNode(index, lView);
const stylingContext = getStylingContext(index + HEADER_OFFSET, lView);
if (delegateToClassInput(tNode) && classes !== NO_CHANGE) {
const initialClasses = stylingContext[StylingIndex.PreviousOrCachedMultiClassValue] as string;
if (hasClassInput(tNode) && classes !== NO_CHANGE) {
const initialClasses = getInitialClassNameValue(stylingContext);
const classInputVal =
(initialClasses.length ? (initialClasses + ' ') : '') + (classes as string);
setInputsForProperty(lView, tNode.inputs !['class'] !, classInputVal);
} else {
updateStylingMap(stylingContext, classes, styles);
}
updateStylingMap(stylingContext, classes, styles);
}
/* START OF HACK BLOCK */
/*
* HACK
* The code below is a quick and dirty implementation of the host style binding so that we can make
* progress on TestBed. Once the correct implementation is created this code should be removed.
*/
interface HostStylingHack {
classDeclarations: string[];
styleDeclarations: string[];
styleSanitizer: StyleSanitizeFn|null;
}
type HostStylingHackMap = Map<{}, HostStylingHack>;
function hackImplementationOfElementStyling(
classDeclarations: (string | boolean | InitialStylingFlags)[] | null,
styleDeclarations: (string | boolean | InitialStylingFlags)[] | null,
styleSanitizer: StyleSanitizeFn | null, directive: {}): void {
const node = getNativeByTNode(getPreviousOrParentTNode(), getLView()) as RElement;
ngDevMode && assertDefined(node, 'expecting parent DOM node');
const hostStylingHackMap: HostStylingHackMap =
((node as any).hostStylingHack || ((node as any).hostStylingHack = new Map()));
const squashedClassDeclarations = hackSquashDeclaration(classDeclarations);
hostStylingHackMap.set(directive, {
classDeclarations: squashedClassDeclarations,
styleDeclarations: hackSquashDeclaration(styleDeclarations), styleSanitizer
});
hackSetStaticClasses(node, squashedClassDeclarations);
}
function hackSetStaticClasses(node: RElement, classDeclarations: (string | boolean)[]) {
// Static classes need to be set here because static classes don't generate
// elementClassProp instructions.
const lView = getLView();
const staticClassStartIndex =
classDeclarations.indexOf(InitialStylingFlags.VALUES_MODE as any) + 1;
const renderer = lView[RENDERER];
for (let i = staticClassStartIndex; i < classDeclarations.length; i += 2) {
const className = classDeclarations[i] as string;
const value = classDeclarations[i + 1];
// if value is true, then this is a static class and we should set it now.
// class bindings are set separately in elementClassProp.
if (value === true) {
if (isProceduralRenderer(renderer)) {
renderer.addClass(node, className);
} else {
const classList = (node as HTMLElement).classList;
classList.add(className);
}
}
}
}
function hackSquashDeclaration(declarations: (string | boolean | InitialStylingFlags)[] | null):
string[] {
// assume the array is correct. This should be fine for View Engine compatibility.
return declarations || [] as any;
}
function hackImplementationOfElementClassProp(
index: number, classIndex: number, value: boolean | PlayerFactory, directive: {}): void {
const lView = getLView();
const node = getNativeByIndex(index, lView);
ngDevMode && assertDefined(node, 'could not locate node');
const hostStylingHack: HostStylingHack = (node as any).hostStylingHack.get(directive);
const className = hostStylingHack.classDeclarations[classIndex];
const renderer = lView[RENDERER];
if (isProceduralRenderer(renderer)) {
value ? renderer.addClass(node, className) : renderer.removeClass(node, className);
} else {
const classList = (node as HTMLElement).classList;
value ? classList.add(className) : classList.remove(className);
}
}
function hackImplementationOfElementStylingApply(index: number, directive?: {}): void {
// Do nothing because the hack implementation is eager.
}
function hackImplementationOfElementStyleProp(
index: number, styleIndex: number, value: string | null, suffix?: string,
directive?: {}): void {
const lView = getLView();
const node = getNativeByIndex(index, lView);
ngDevMode && assertDefined(node, 'could not locate node');
const hostStylingHack: HostStylingHack = (node as any).hostStylingHack.get(directive);
const styleName = hostStylingHack.styleDeclarations[styleIndex];
const renderer = lView[RENDERER];
setStyle(node, styleName, value as string, renderer, null);
}
function hackImplementationOfElementStylingMap<T>(
index: number, classes: {[key: string]: any} | string | NO_CHANGE | null,
styles?: {[styleName: string]: any} | NO_CHANGE | null, directive?: {}): void {
throw new Error('unimplemented. Should not be needed by ViewEngine compatibility');
}
/* END OF HACK BLOCK */
//////////////////////////
//// Text
//////////////////////////
@ -2885,10 +2829,6 @@ function initializeTNodeInputs(tNode: TNode | null) {
return null;
}
export function delegateToClassInput(tNode: TNode) {
return tNode.flags & TNodeFlags.hasClassInput;
}
/**
* Returns the current OpaqueViewState instance.

View File

@ -23,6 +23,14 @@ export const VIEWS = 1;
// As we already have these constants in LView, we don't need to re-create them.
export const NATIVE = 6;
export const RENDER_PARENT = 7;
// Because interfaces in TS/JS cannot be instanceof-checked this means that we
// need to rely on predictable characteristics of data-structures to check if they
// are what we expect for them to be. The `LContainer` interface code below has a
// fixed length and the constant value below references that. Using the length value
// below we can predictably gaurantee that we are dealing with an `LContainer` array.
// This value MUST be kept up to date with the length of the `LContainer` array
// interface below so that runtime type checking can work.
export const LCONTAINER_LENGTH = 8;
/**
* The state associated with a container.

View File

@ -344,8 +344,4 @@ export type PipeTypeList =
// Note: This hack is necessary so we don't erroneously get a circular dependency
// failure based on types.
export const unusedValueExportToPlacateAjd = 1;
export const enum InitialStylingFlags {
VALUES_MODE = 0b1,
}
export const unusedValueExportToPlacateAjd = 1;

View File

@ -54,7 +54,7 @@ export const enum TNodeProviderIndexes {
CptViewProvidersCountShifter = 0b00000000000000010000000000000000,
}
/**
* A set of marker values to be used in the attributes arrays. Those markers indicate that some
* A set of marker values to be used in the attributes arrays. These markers indicate that some
* items are not regular attributes and the processing should be adapted accordingly.
*/
export const enum AttributeMarker {
@ -65,13 +65,50 @@ export const enum AttributeMarker {
*/
NamespaceURI = 0,
/**
* Signals class declaration.
*
* Each value following `Classes` designates a class name to include on the element.
* ## Example:
*
* Given:
* ```
* <div class="foo bar baz">...<d/vi>
* ```
*
* the generated code is:
* ```
* var _c1 = [AttributeMarker.Classes, 'foo', 'bar', 'baz'];
* ```
*/
Classes = 1,
/**
* Signals style declaration.
*
* Each pair of values following `Styles` designates a style name and value to include on the
* element.
* ## Example:
*
* Given:
* ```
* <div style="width:100px; height:200px; color:red">...</div>
* ```
*
* the generated code is:
* ```
* var _c1 = [AttributeMarker.Styles, 'width', '100px', 'height'. '200px', 'color', 'red'];
* ```
*/
Styles = 2,
/**
* This marker indicates that the following attribute names were extracted from bindings (ex.:
* [foo]="exp") and / or event handlers (ex. (bar)="doSth()").
* Taking the above bindings and outputs as an example an attributes array could look as follows:
* ['class', 'fade in', AttributeMarker.SelectOnly, 'foo', 'bar']
*/
SelectOnly = 1
SelectOnly = 3,
}
/**

View File

@ -9,130 +9,199 @@ import {StyleSanitizeFn} from '../../sanitization/style_sanitizer';
import {RElement} from '../interfaces/renderer';
import {PlayerContext} from './player';
/**
* The styling context acts as a styling manifest (shaped as an array) for determining which
* 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.
* and `updateClassProp` functions. It also stores the static style/class values that were
* extracted from the template by the compiler.
*
* 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.
* A context is created by Angular when:
* 1. An element contains static styling values (like style="..." or class="...")
* 2. An element contains single property binding values (like [style.prop]="x" or
* [class.prop]="y")
* 3. An element contains multi property binding values (like [style]="x" or [class]="y")
* 4. A directive contains host bindings for static, single or multi styling properties/bindings.
* 5. An animation player is added to an element via `addPlayer`
*
* each value from here onwards is mapped as so:
* [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 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` or `updateClassProp` (fixed set)
*
* multi: any styles that are updated using `updateStylingMap` (dynamic set)
*
* 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 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:
* Note that even if an element contains static styling then this context will be created and
* attached to it. The reason why this happens (instead of treating styles/classes as regular
* HTML attributes) is because the style/class bindings must be able to default themselves back
* to their respective static values when they are set to null.
*
* Say for example we have this:
* ```
* <!-- when myWidthExp=null then a width of "100px"
* will be used a default value for width -->
* <div style="width:100px" [style.width]="myWidthExp"></div>
* ```
*
* Even in the situation where there are no bindings, the static styling is still placed into the
* context because there may be another directive on the same element that has styling.
*
* When Angular initializes styling data for an element then it will first register the static
* styling values on the element using one of these two instructions:
*
* 1. elementStart or element (within the template function of a component)
* 2. elementHostAttrs (for directive host bindings)
*
* In either case, a styling context will be created and stored within an element's LViewData. Once
* the styling context is created then single and multi properties can stored within it. For this to
* happen, the following function needs to be called:
*
* `elementStyling` (called with style properties, class properties and a sanitizer + a directive
* instance).
*
* When this instruction is called it will populate the styling context with the provided style
* and class names into the context.
*
* The context itself looks like this:
*
* context = [
* element,
* playerContext | null,
* styleSanitizer | null,
* [null, '100px', '200px', true], // property names are not needed since they have already been
* written to DOM.
*
* configMasterVal,
* 1, // this instructs how many `style` values there are so that class index values can be
* offsetted
* { classOne: true, classTwo: false } | 'classOne classTwo' | null // last class value provided
* into updateStylingMap
* { styleOne: '100px', styleTwo: 0 } | null // last style value provided into updateStylingMap
*
* // 8
* 'width',
* pointers(1, 15); // Point to static `width`: `100px` and multi `width`.
* null,
*
* // 11
* 'height',
* pointers(2, 18); // Point to static `height`: `200px` and multi `height`.
* null,
*
* // 14
* 'foo',
* pointers(1, 21); // Point to static `foo`: `true` and multi `foo`.
* null,
*
* // 17
* 'width',
* pointers(1, 6); // Point to static `width`: `100px` and single `width`.
* null,
*
* // 21
* 'height',
* pointers(2, 9); // Point to static `height`: `200px` and single `height`.
* null,
*
* // 24
* 'foo',
* pointers(3, 12); // Point to static `foo`: `true` and single `foo`.
* null,
* // 0-8: header values (about 8 entries of configuration data)
* // 9+: this is where each entry is stored:
* ]
*
* function pointers(staticIndex: number, dynamicIndex: number) {
* // combine the two indices into a single word.
* return (staticIndex << StylingFlags.BitCountSize) |
* (dynamicIndex << (StylingIndex.BitCountSize + StylingFlags.BitCountSize));
* Let's say we have the following template code:
*
* ```
* <div class="foo bar"
* style="width:200px; color:red"
* [style.width]="myWidthExp"
* [style.height]="myHeightExp"
* [class.baz]="myBazExp">
* ```
*
* The context generated from these values will look like this (note that
* for each binding name (the class and style bindings) the values will
* be inserted twice into the array (once for single property entries) and
* another for multi property entries).
*
* context = [
* // 0-8: header values (about 8 entries of configuration data)
* // 9+: this is where each entry is stored:
*
* // SINGLE PROPERTIES
* configForWidth,
* 'width'
* myWidthExp, // the binding value not the binding itself
* 0, // the directive owner
*
* configForHeight,
* 'height'
* myHeightExp, // the binding value not the binding itself
* 0, // the directive owner
*
* configForBazClass,
* 'baz
* myBazClassExp, // the binding value not the binding itself
* 0, // the directive owner
*
* // MULTI PROPERTIES
* configForWidth,
* 'width'
* myWidthExp, // the binding value not the binding itself
* 0, // the directive owner
*
* configForHeight,
* 'height'
* myHeightExp, // the binding value not the binding itself
* 0, // the directive owner
*
* configForBazClass,
* 'baz
* myBazClassExp, // the binding value not the binding itself
* 0, // the directive owner
* ]
*
* The configuration values are left out of the example above because
* the ordering of them could change between code patches. Please read the
* documentation below to get a better understand of what the configuration
* values are and how they work.
*
* Each time a binding property is updated (whether it be through a single
* property instruction like `elementStyleProp`, `elementClassProp` or
* `elementStylingMap`) then the values in the context will be updated as
* well.
*
* If for example `[style.width]` updates to `555px` then its value will be reflected
* in the context as so:
*
* context = [
* // ...
* configForWidth, // this will be marked DIRTY
* 'width'
* '555px',
* 0,
* //..
* ]
*
* The context and directive data will also be marked dirty.
*
* Despite the context being updated, nothing has been rendered on screen (not styles or
* classes have been set on the element). To kick off rendering for an element the following
* function needs to be run `elementStylingApply`.
*
* `elementStylingApply` will run through the context and find each dirty value and render them onto
* the element. Once complete, all styles/classes will be set to clean. Because of this, the render
* function will now know not to rerun itself again if called again unless new style/class values
* have changed.
*
* ## Directives
* Directives style values (which are provided through host bindings) are also supported and
* housed within the same styling context as are template-level style/class properties/bindings.
* Both directive-level and template-level styling bindings share the same context.
*
* Each of the following instructions supports accepting a directive instance as an input parameter:
*
* - `elementHostAttrs`
* - `elementStyling`
* - `elementStyleProp`
* - `elementClassProp`
* - `elementStylingMap`
* - `elementStylingApply`
*
* Each time a directiveRef is passed in, it will be converted into an index by examining the
* directive registry (which lives in the context configuration area). The index is then used
* to help single style properties figure out where a value is located in the context.
*
* If two directives or a directive + a template binding both write to the same style/class
* binding then the styling context code will decide which one wins based on the following
* rule:
*
* 1. If the template binding has a value then it always wins
* 2. If not then whichever first-registered directive that has that value first will win
*
* The code example helps make this clear:
*
* ```
* <div [style.width]="myWidth" [my-width-directive]="'600px">
* @Directive({ selector: '[my-width-directive' ]})
* class MyWidthDirective {
* @Input('my-width-directive')
* @HostBinding('style.width')
* public width = null;
* }
* ```
*
* 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).
* Since there is a style binding for width present on the element (`[style.width]`) then
* it will always win over the width binding that is present as a host binding within
* the `MyWidthDirective`. However, if `[style.width]` renders as `null` (so `myWidth=null`)
* then the `MyWidthDirective` will be able to write to the `width` style within the context.
* Simply put, whichever directive writes to a value ends up having ownership of it.
*
* 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).
* The way in which the ownership is facilitated is through index value. The earliest directives
* get the smallest index values (with 0 being reserved for the template element bindings). Each
* time a value is written from a directive or the template bindings, the value itself gets
* assigned the directive index value in its data. If another directive writes a value again then
* its directive index gets compared against the directive index that exists on the element. Only
* when the new value's directive index is less than the existing directive index then the new
* value will be written to the context.
*
* Each directive also has its own sanitizer and dirty flags. These values are consumed within the
* rendering function.
*/
export interface StylingContext extends Array<InitialStyles|{[key: string]: any}|number|string|
boolean|RElement|StyleSanitizeFn|PlayerContext|null> {
/**
* Location of animation context (which contains the active players) for this element styling
* context.
*/
[StylingIndex.PlayerContext]: PlayerContext|null;
/**
* The style sanitizer that is used within this context
*/
[StylingIndex.StyleSanitizerPosition]: StyleSanitizeFn|null;
/**
* Location of initial data shared by all instances of this style.
*/
[StylingIndex.InitialStylesPosition]: InitialStyles;
export interface StylingContext extends
Array<{[key: string]: any}|number|string|boolean|RElement|StyleSanitizeFn|PlayerContext|null> {
/**
* 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
@ -140,12 +209,27 @@ export interface StylingContext extends Array<InitialStyles|{[key: string]: any}
*/
[StylingIndex.MasterFlagPosition]: number;
/**
* Location of the collection of directives for this context
*/
[StylingIndex.DirectiveRegistryPosition]: DirectiveRegistryValues;
/**
* Location of all static styles values
*/
[StylingIndex.InitialStyleValuesPosition]: InitialStylingValues;
/**
* Location of all static class values
*/
[StylingIndex.InitialClassValuesPosition]: InitialStylingValues;
/**
* 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.
*/
[StylingIndex.ClassOffsetPosition]: number;
[StylingIndex.SinglePropOffsetPositions]: SinglePropOffsetValues;
/**
* Location of element that is used as a target for this context.
@ -156,24 +240,206 @@ export interface StylingContext extends Array<InitialStyles|{[key: string]: any}
* The last class value that was interpreted by elementStylingMap. This is cached
* So that the algorithm can exit early incase the value has not changed.
*/
[StylingIndex.PreviousOrCachedMultiClassValue]: {[key: string]: any}|string|null;
[StylingIndex.CachedClassValueOrInitialClassString]: {[key: string]: any}|string|(string)[]|null;
/**
* The last style value that was interpreted by elementStylingMap. This is cached
* So that the algorithm can exit early incase the value has not changed.
*/
[StylingIndex.PreviousMultiStyleValue]: {[key: string]: any}|null;
[StylingIndex.CachedStyleValue]: {[key: string]: any}|(string)[]|null;
/**
* Location of animation context (which contains the active players) for this element styling
* context.
*/
[StylingIndex.PlayerContext]: PlayerContext|null;
}
/**
* The initial styles is populated whether or not there are any initial styles passed into
* the context during allocation. The 0th value must be null so that index values of `0` within
* the context flags can always point to a null value safely when nothing is set.
* Used as a styling array to house static class and style values that were extracted
* by the compiler and placed in the animation context via `elementStart` and
* `elementHostAttrs`.
*
* 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.
* See [InitialStylingValuesIndex] for a breakdown of how all this works.
*/
export interface InitialStyles extends Array<string|null|boolean> { [0]: null; }
export interface InitialStylingValues extends Array<string|boolean|null> { [0]: null; }
/**
* Used as an offset/position index to figure out where initial styling
* values are located.
*
* Used as a reference point to provide markers to all static styling
* values (the initial style and class values on an element) within an
* array within the StylingContext. This array contains key/value pairs
* where the key is the style property name or className and the value is
* the style value or whether or not a class is present on the elment.
*
* The first value is also always null so that a initial index value of
* `0` will always point to a null value.
*
* If a <div> elements contains a list of static styling values like so:
*
* <div class="foo bar baz" style="width:100px; height:200px;">
*
* Then the initial styles for that will look like so:
*
* Styles:
* StylingContext[InitialStylesIndex] = [
* null, 'width', '100px', height, '200px'
* ]
*
* Classes:
* StylingContext[InitialStylesIndex] = [
* null, 'foo', true, 'bar', true, 'baz', true
* ]
*
* Initial style and class entries have their own arrays. This is because
* it's easier to add to the end of one array and not then have to update
* every context entries' pointer index to the newly offseted values.
*
* When property bindinds are added to a context then initial style/class
* values will also be inserted into the array. This is to create a space
* in the situation when a follow-up directive inserts static styling into
* the array. By default style values are `null` and class values are
* `false` when inserted by property bindings.
*
* For example:
* <div class="foo bar baz"
* [class.car]="myCarExp"
* style="width:100px; height:200px;"
* [style.opacity]="myOpacityExp">
*
* Will construct initial styling values that look like:
*
* Styles:
* StylingContext[InitialStylesIndex] = [
* null, 'width', '100px', height, '200px', 'opacity', null
* ]
*
* Classes:
* StylingContext[InitialStylesIndex] = [
* null, 'foo', true, 'bar', true, 'baz', true, 'car', false
* ]
*
* Now if a directive comes along and introduces `car` as a static
* class value or `opacity` then those values will be filled into
* the initial styles array.
*
* For example:
*
* @Directive({
* selector: 'opacity-car-directive',
* host: {
* 'style': 'opacity:0.5',
* 'class': 'car'
* }
* })
* class OpacityCarDirective {}
*
* This will render itself as:
*
* Styles:
* StylingContext[InitialStylesIndex] = [
* null, 'width', '100px', height, '200px', 'opacity', null
* ]
*
* Classes:
* StylingContext[InitialStylesIndex] = [
* null, 'foo', true, 'bar', true, 'baz', true, 'car', false
* ]
*/
export const enum InitialStylingValuesIndex {
KeyValueStartPosition = 1,
PropOffset = 0,
ValueOffset = 1,
Size = 2
}
/**
* An array located in the StylingContext that houses all directive instances and additional
* data about them.
*
* Each entry in this array represents a source of where style/class binding values could
* come from. By default, there is always at least one directive here with a null value and
* that represents bindings that live directly on an element (not host bindings).
*
* Each successive entry in the array is an actual instance of an array as well as some
* additional info.
*
* An entry within this array has the following values:
* [0] = The instance of the directive (or null when it is not a directive, but a template binding
* source)
* [1] = The pointer that tells where the single styling (stuff like [class.foo] and [style.prop])
* offset values are located. This value will allow for a binding instruction to find exactly
* where a style is located.
* [2] = Whether or not the directive has any styling values that are dirty. This is used as
* reference within the renderClassAndStyleBindings function to decide whether to skip
* iterating through the context when rendering is executed.
* [3] = The styleSanitizer instance that is assigned to the directive. Although it's unlikely,
* a directive could introduce its own special style sanitizer and for this reach each
* directive will get its own space for it (if null then the very first sanitizer is used).
*
* Each time a new directive is added it will insert these four values at the end of the array.
* When this array is examined (using indexOf) then the resulting directiveIndex will be resolved
* by dividing the index value by the size of the array entries (so if DirA is at spot 8 then its
* index will be 2).
*/
export interface DirectiveRegistryValues extends Array<null|{}|boolean|number|StyleSanitizeFn> {
[DirectiveRegistryValuesIndex.DirectiveValueOffset]: null;
[DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset]: number;
[DirectiveRegistryValuesIndex.DirtyFlagOffset]: boolean;
[DirectiveRegistryValuesIndex.StyleSanitizerOffset]: StyleSanitizeFn|null;
}
/**
* An enum that outlines the offset/position values for each directive entry and its data
* that are housed inside of [DirectiveRegistryValues].
*/
export const enum DirectiveRegistryValuesIndex {
DirectiveValueOffset = 0,
SinglePropValuesIndexOffset = 1,
DirtyFlagOffset = 2,
StyleSanitizerOffset = 3,
Size = 4
}
/**
* An array that contains the index pointer values for every single styling property
* that exists in the context and for every directive. It also contains the total
* single styles and single classes that exists in the context as the first two values.
*
* Let's say we have the following template code:
*
* <div [style.width]="myWidth"
* [style.height]="myHeight"
* [class.flipped]="flipClass"
* directive-with-opacity>
* directive-with-foo-bar-classes>
*
* We have two directive and template-binding sources,
* 2 + 1 styles and 1 + 1 classes. When the bindings are
* registered the SinglePropOffsets array will look like so:
*
* s_0/c_0 = template directive value
* s_1/c_1 = directive one (directive-with-opacity)
* s_2/c_2 = directive two (directive-with-foo-bar-classes)
*
* [3, 2, 2, 1, s_00, s01, c_01, 1, 0, s_10, 0, 1, c_20
*/
export interface SinglePropOffsetValues extends Array<number> {
[SinglePropOffsetValuesIndex.StylesCountPosition]: number;
[SinglePropOffsetValuesIndex.ClassesCountPosition]: number;
}
/**
* An enum that outlines the offset/position values for each single prop/class entry
* that are housed inside of [SinglePropOffsetValues].
*/
export const enum SinglePropOffsetValuesIndex {
StylesCountPosition = 0,
ClassesCountPosition = 1,
ValueStartPosition = 2
}
/**
* Used to set the context to be dirty or not both on the master flag (position 1)
@ -181,47 +447,49 @@ export interface InitialStyles extends Array<string|null|boolean> { [0]: null; }
*/
export const enum StylingFlags {
// Implies no configurations
None = 0b00000,
None = 0b000000,
// Whether or not the entry or context itself is dirty
Dirty = 0b00001,
Dirty = 0b000001,
// Whether or not this is a class-based assignment
Class = 0b00010,
Class = 0b000010,
// Whether or not a sanitizer was applied to this property
Sanitize = 0b00100,
Sanitize = 0b000100,
// Whether or not any player builders within need to produce new players
PlayerBuildersDirty = 0b01000,
PlayerBuildersDirty = 0b001000,
// If NgClass is present (or some other class handler) then it will handle the map expressions and
// initial classes
OnlyProcessSingleClasses = 0b10000,
OnlyProcessSingleClasses = 0b010000,
// The max amount of bits used to represent these configuration values
BitCountSize = 5,
// There are only five bits here
BitMask = 0b11111
BindingAllocationLocked = 0b100000,
BitCountSize = 6,
// There are only six bits here
BitMask = 0b111111
}
/** 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
PlayerContext = 0,
// Position of where the style sanitizer is stored within the styling context
StyleSanitizerPosition = 1,
// Position of where the initial styles are stored in the styling context
InitialStylesPosition = 2,
// Index of location where the start of single properties are stored. (`updateStyleProp`)
MasterFlagPosition = 3,
MasterFlagPosition = 0,
// Position of where the registered directives exist for this styling context
DirectiveRegistryPosition = 1,
// Position of where the initial styles are stored in the styling context
InitialStyleValuesPosition = 2,
InitialClassValuesPosition = 3,
// Index of location where the class index offset value is located
ClassOffsetPosition = 4,
SinglePropOffsetPositions = 4,
// Position of where the initial styles are stored in the styling context
// This index must align with HOST, see interfaces/view.ts
ElementPosition = 5,
// Position of where the last string-based CSS class value was stored (or a cached version of the
// initial styles when a [class] directive is present)
PreviousOrCachedMultiClassValue = 6,
CachedClassValueOrInitialClassString = 6,
// Position of where the last string-based CSS class value was stored
PreviousMultiStyleValue = 7,
// Location of single (prop) value entries are stored within the context
SingleStylesStartPosition = 8,
CachedStyleValue = 7,
// Multi and single entries are stored in `StylingContext` as: Flag; PropertyName; PropertyValue
// Position of where the initial styles are stored in the styling context
PlayerContext = 8,
// Location of single (prop) value entries are stored within the context
SingleStylesStartPosition = 9,
FlagsOffset = 0,
PropertyOffset = 1,
ValueOffset = 2,
@ -233,3 +501,16 @@ export const enum StylingIndex {
// The binary digit value as a mask
BitMask = 0b11111111111111, // 14 bits
}
/**
* An enum that outlines the bit flag data for directive owner and player index
* values that exist within en entry that lives in the StylingContext.
*
* The values here split a number value into two sets of bits:
* - The first 16 bits are used to store the directiveIndex that owns this style value
* - The other 16 bits are used to store the playerBuilderIndex that is attached to this style
*/
export const enum DirectiveOwnerAndPlayerBuilderIndex {
BitCountSize = 16,
BitMask = 0b1111111111111111
}

View File

@ -12,9 +12,9 @@ import {Component, Directive} from '../../metadata/directives';
import {componentNeedsResolution, maybeQueueResolutionOfComponentResources} from '../../metadata/resource_loading';
import {ViewEncapsulation} from '../../metadata/view';
import {Type} from '../../type';
import {stringify} from '../../util';
import {EMPTY_ARRAY} from '../definition';
import {EMPTY_ARRAY, EMPTY_OBJ} from '../empty';
import {NG_COMPONENT_DEF, NG_DIRECTIVE_DEF} from '../fields';
import {stringify} from '../util';
import {R3DirectiveMetadataFacade, getCompilerFacade} from './compiler_facade';
import {R3ComponentMetadataFacade, R3QueryMetadataFacade} from './compiler_facade_interface';
@ -154,8 +154,6 @@ function directiveMetadata(type: Type<any>, metadata: Directive): R3DirectiveMet
};
}
const EMPTY_OBJ = {};
function convertToR3QueryPredicate(selector: any): any|string[] {
return typeof selector === 'string' ? splitByComma(selector) : selector;
}

View File

@ -90,6 +90,7 @@ export const angularCoreEnv: {[name: string]: Function} = {
'ɵregisterContentQuery': r3.registerContentQuery,
'ɵreference': r3.reference,
'ɵelementStyling': r3.elementStyling,
'ɵelementHostAttrs': r3.elementHostAttrs,
'ɵelementStylingMap': r3.elementStylingMap,
'ɵelementStyleProp': r3.elementStyleProp,
'ɵelementStylingApply': r3.elementStylingApply,

File diff suppressed because it is too large Load Diff

View File

@ -9,12 +9,13 @@ import '../ng_dev_mode';
import {StyleSanitizeFn} from '../../sanitization/style_sanitizer';
import {getLContext} from '../context_discovery';
import {ACTIVE_INDEX, LContainer} from '../interfaces/container';
import {LContainer} from '../interfaces/container';
import {LContext} from '../interfaces/context';
import {AttributeMarker, TAttributes, TNode, TNodeFlags} from '../interfaces/node';
import {PlayState, Player, PlayerContext, PlayerIndex} from '../interfaces/player';
import {RElement} from '../interfaces/renderer';
import {InitialStyles, StylingContext, StylingIndex} from '../interfaces/styling';
import {FLAGS, HEADER_OFFSET, HOST, LView, RootContext} from '../interfaces/view';
import {InitialStylingValues, StylingContext, StylingFlags, StylingIndex} from '../interfaces/styling';
import {HEADER_OFFSET, HOST, LView, RootContext} from '../interfaces/view';
import {getTNode} from '../util';
import {CorePlayerHandler} from './core_player_handler';
@ -23,16 +24,18 @@ const ANIMATION_PROP_PREFIX = '@';
export function createEmptyStylingContext(
element?: RElement | null, sanitizer?: StyleSanitizeFn | null,
initialStylingValues?: InitialStyles): StylingContext {
initialStyles?: InitialStylingValues | null,
initialClasses?: InitialStylingValues | null): StylingContext {
return [
null, // PlayerContext
sanitizer || null, // StyleSanitizer
initialStylingValues || [null], // InitialStyles
0, // MasterFlags
0, // ClassOffset
element || null, // Element
null, // PreviousMultiClassValue
null // PreviousMultiStyleValue
0, // MasterFlags
[null, -1, false, sanitizer || null], // DirectiveRefs
initialStyles || [null], // InitialStyles
initialClasses || [null], // InitialClasses
[0, 0], // SinglePropOffsets
element || null, // Element
null, // PreviousMultiClassValue
null, // PreviousMultiStyleValue
null, // PlayerContext
];
}
@ -47,6 +50,9 @@ export function allocStylingContext(
// each instance gets a copy
const context = templateStyleContext.slice() as any as StylingContext;
context[StylingIndex.ElementPosition] = element;
// this will prevent any other directives from extending the context
context[StylingIndex.MasterFlagPosition] |= StylingFlags.BindingAllocationLocked;
return context;
}
@ -89,8 +95,8 @@ export function getStylingContext(index: number, viewData: LView): StylingContex
export function isStylingContext(value: any): value is StylingContext {
// Not an LView or an LContainer
return Array.isArray(value) && typeof value[FLAGS] !== 'number' &&
typeof value[ACTIVE_INDEX] !== 'number';
return Array.isArray(value) && typeof value[StylingIndex.MasterFlagPosition] === 'number' &&
Array.isArray(value[StylingIndex.InitialStyleValuesPosition]);
}
export function isAnimationProp(name: string): boolean {
@ -182,3 +188,15 @@ export function allocPlayerContext(data: StylingContext): PlayerContext {
export function throwInvalidRefError() {
throw new Error('Only elements that exist in an Angular application can be used for animations');
}
export function hasStyling(attrs: TAttributes): boolean {
for (let i = 0; i < attrs.length; i++) {
const attr = attrs[i];
if (attr == AttributeMarker.Classes || attr == AttributeMarker.Styles) return true;
}
return false;
}
export function hasClassInput(tNode: TNode) {
return tNode.flags & TNodeFlags.hasClassInput ? true : false;
}

View File

@ -9,7 +9,7 @@
import {global} from '../util';
import {assertDataInRange, assertDefined, assertGreaterThan, assertLessThan} from './assert';
import {ACTIVE_INDEX, LContainer} from './interfaces/container';
import {ACTIVE_INDEX, LCONTAINER_LENGTH, LContainer} from './interfaces/container';
import {LContext, MONKEY_PATCH_KEY_NAME} from './interfaces/context';
import {ComponentDef, DirectiveDef} from './interfaces/definition';
import {NO_PARENT_INJECTOR, RelativeInjectorLocation, RelativeInjectorLocationFlags} from './interfaces/injector';
@ -18,8 +18,6 @@ import {RComment, RElement, RText} from './interfaces/renderer';
import {StylingContext} from './interfaces/styling';
import {CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, LView, LViewFlags, PARENT, RootContext, TData, TVIEW, TView} from './interfaces/view';
/**
* Returns whether the values are different from a change detection stand point.
*
@ -127,7 +125,7 @@ export function isComponentDef<T>(def: DirectiveDef<T>): def is ComponentDef<T>
export function isLContainer(value: RElement | RComment | LContainer | StylingContext): boolean {
// Styling contexts are also arrays, but their first index contains an element node
return Array.isArray(value) && typeof value[ACTIVE_INDEX] === 'number';
return Array.isArray(value) && value.length === LCONTAINER_LENGTH;
}
export function isRootView(target: LView): boolean {

View File

@ -9,7 +9,29 @@
import '@angular/core/test/bundling/util/src/reflect_metadata';
import {CommonModule} from '@angular/common';
import {Component, ElementRef, NgModule, ɵPlayState as PlayState, ɵPlayer as Player, ɵPlayerHandler as PlayerHandler, ɵaddPlayer as addPlayer, ɵbindPlayerFactory as bindPlayerFactory, ɵmarkDirty as markDirty, ɵrenderComponent as renderComponent} from '@angular/core';
import {Component, Directive, ElementRef, HostBinding, NgModule, ɵPlayState as PlayState, ɵPlayer as Player, ɵPlayerHandler as PlayerHandler, ɵaddPlayer as addPlayer, ɵbindPlayerFactory as bindPlayerFactory, ɵmarkDirty as markDirty, ɵrenderComponent as renderComponent} from '@angular/core';
@Directive({
selector: '[make-color-grey]',
exportAs: 'makeColorGrey',
host: {'style': 'font-family: Times New Roman;'}
})
class MakeColorGreyDirective {
@HostBinding('style.background-color') private _backgroundColor: string|null = null;
@HostBinding('style.color') private _textColor: string|null = null;
on() {
this._backgroundColor = 'grey';
this._textColor = 'black';
}
off() {
this._backgroundColor = null;
this._textColor = null;
}
toggle() { this._backgroundColor ? this.off() : this.on(); }
}
@Component({
selector: 'animation-world',
@ -20,21 +42,40 @@ import {Component, ElementRef, NgModule, ɵPlayState as PlayState, ɵPlayer as P
</nav>
<div class="list">
<div
*ngFor="let item of items" class="record" [class]="makeClass(item)" style="border-radius: 10px"
[style]="styles">
{{ item }}
#makeColorGrey="makeColorGrey"
make-color-grey
*ngFor="let item of items"
class="record"
[style.transform]="item.active ? 'scale(1.5)' : 'none'"
[class]="makeClass(item)"
style="border-radius: 10px"
[style]="styles"
[style.color]="item.value == 4 ? 'red' : null"
[style.background-color]="item.value == 4 ? 'white' : null"
(click)="toggleActive(item, makeColorGrey)">
{{ item.value }}
</div>
</div>
`,
})
class AnimationWorldComponent {
items: any[] = [1, 2, 3, 4, 5, 6, 7, 8, 9];
items: any[] = [
{value: 1, active: false}, {value: 2, active: false}, {value: 3, active: false},
{value: 4, active: false}, {value: 5, active: false}, {value: 6, active: false},
{value: 7, active: false}, {value: 8, active: false}, {value: 9, active: false}
];
private _hostElement: HTMLElement;
public styles: {[key: string]: any}|null = null;
constructor(element: ElementRef) { this._hostElement = element.nativeElement; }
makeClass(index: number) { return `record-${index}`; }
makeClass(item: any) { return `record-${item.value}`; }
toggleActive(item: any, makeColorGrey: MakeColorGreyDirective) {
item.active = !item.active;
makeColorGrey.toggle();
markDirty(this);
}
animateWithStyles() {
this.styles = animateStyleFactory([{opacity: 0}, {opacity: 1}], 300, 'ease-out');
@ -52,7 +93,8 @@ class AnimationWorldComponent {
}
}
@NgModule({declarations: [AnimationWorldComponent], imports: [CommonModule]})
@NgModule(
{declarations: [AnimationWorldComponent, MakeColorGreyDirective], imports: [CommonModule]})
class AnimationWorldModule {
}

View File

@ -27,10 +27,10 @@
"name": "DECLARATION_VIEW"
},
{
"name": "EMPTY"
"name": "EMPTY_ARRAY"
},
{
"name": "EMPTY_ARRAY"
"name": "EMPTY_OBJ"
},
{
"name": "EmptyErrorImpl"

View File

@ -45,10 +45,10 @@
"name": "DefaultIterableDifferFactory"
},
{
"name": "EMPTY"
"name": "EMPTY_ARRAY"
},
{
"name": "EMPTY_ARRAY"
"name": "EMPTY_OBJ"
},
{
"name": "ElementRef"
@ -86,6 +86,9 @@
{
"name": "IterableDiffers"
},
{
"name": "LCONTAINER_LENGTH"
},
{
"name": "MONKEY_PATCH_KEY_NAME"
},
@ -320,21 +323,9 @@
{
"name": "_c18"
},
{
"name": "_c19"
},
{
"name": "_c2"
},
{
"name": "_c20"
},
{
"name": "_c21"
},
{
"name": "_c22"
},
{
"name": "_c3"
},
@ -371,6 +362,9 @@
{
"name": "_symbolIterator"
},
{
"name": "_updateSingleStylingValue"
},
{
"name": "addComponentLogic"
},
@ -389,6 +383,9 @@
{
"name": "allocStylingContext"
},
{
"name": "allowValueChange"
},
{
"name": "appendChild"
},
@ -491,9 +488,6 @@
{
"name": "createRootContext"
},
{
"name": "createStylingContextTemplate"
},
{
"name": "createTNode"
},
@ -530,9 +524,6 @@
{
"name": "defineInjectable"
},
{
"name": "delegateToClassInput"
},
{
"name": "destroyLView"
},
@ -554,6 +545,9 @@
{
"name": "directiveInject"
},
{
"name": "directiveOwnerPointers"
},
{
"name": "domRendererFactory3"
},
@ -614,6 +608,9 @@
{
"name": "findDirectiveMatches"
},
{
"name": "findOrPatchDirectiveIntoRegistry"
},
{
"name": "findViaComponent"
},
@ -668,6 +665,15 @@
{
"name": "getDirectiveDef"
},
{
"name": "getDirectiveIndexFromEntry"
},
{
"name": "getDirectiveIndexFromRegistry"
},
{
"name": "getDirectiveRegistryValuesIndexOf"
},
{
"name": "getElementDepthCount"
},
@ -686,9 +692,15 @@
{
"name": "getHostTElementNode"
},
{
"name": "getInitialClassNameValue"
},
{
"name": "getInitialIndex"
},
{
"name": "getInitialStylingValuesIndexOf"
},
{
"name": "getInitialValue"
},
@ -710,6 +722,9 @@
{
"name": "getLViewChild"
},
{
"name": "getMatchingBindingIndex"
},
{
"name": "getMultiOrSingleIndex"
},
@ -791,6 +806,9 @@
{
"name": "getRootView"
},
{
"name": "getSinglePropIndexValue"
},
{
"name": "getStyleSanitizer"
},
@ -816,19 +834,7 @@
"name": "getValue"
},
{
"name": "hackImplementationOfElementClassProp"
},
{
"name": "hackImplementationOfElementStyling"
},
{
"name": "hackImplementationOfElementStylingApply"
},
{
"name": "hackSetStaticClasses"
},
{
"name": "hackSquashDeclaration"
"name": "hasClassInput"
},
{
"name": "hasParentInjector"
@ -836,6 +842,9 @@
{
"name": "hasPlayerBuilderChanged"
},
{
"name": "hasStyling"
},
{
"name": "hasTagAndTypeMatch"
},
@ -851,6 +860,9 @@
{
"name": "initNodeFlags"
},
{
"name": "initializeStaticContext"
},
{
"name": "initializeTNodeInputs"
},
@ -911,6 +923,9 @@
{
"name": "isDifferent"
},
{
"name": "isDirectiveDirty"
},
{
"name": "isDirty"
},
@ -1083,7 +1098,13 @@
"name": "renderEmbeddedTemplate"
},
{
"name": "renderStyleAndClassBindings"
"name": "renderInitialStylesAndClasses"
},
{
"name": "renderInitialStylingValues"
},
{
"name": "renderStyling"
},
{
"name": "resetComponentState"
@ -1127,6 +1148,9 @@
{
"name": "setCurrentDirectiveDef"
},
{
"name": "setDirectiveDirty"
},
{
"name": "setDirty"
},
@ -1166,6 +1190,9 @@
{
"name": "setProp"
},
{
"name": "setSanitizeFlag"
},
{
"name": "setStyle"
},
@ -1215,7 +1242,7 @@
"name": "updateClassProp"
},
{
"name": "updateStyleProp"
"name": "updateContextWithBindings"
},
{
"name": "updateViewQuery"

View File

@ -542,7 +542,6 @@ describe('discovery utils deprecated', () => {
});
it('should return a map of local refs for an element with styling context', () => {
class Comp {
static ngComponentDef = defineComponent({
type: Comp,
@ -554,7 +553,6 @@ describe('discovery utils deprecated', () => {
if (rf & RenderFlags.Create) {
// <div #elRef class="fooClass">
elementStart(0, 'div', null, ['elRef', '']);
elementStyling(['fooClass']);
elementEnd();
}
if (rf & RenderFlags.Update) {

View File

@ -8,7 +8,7 @@
import {AttributeMarker, defineComponent, defineDirective} from '../../src/render3/index';
import {bind, container, containerRefreshEnd, containerRefreshStart, element, elementAttribute, elementClassProp, elementEnd, elementProperty, elementStart, elementStyling, elementStylingApply, embeddedViewEnd, embeddedViewStart, interpolation2, nextContext, reference, template, text, textBinding} from '../../src/render3/instructions';
import {InitialStylingFlags, RenderFlags} from '../../src/render3/interfaces/definition';
import {RenderFlags} from '../../src/render3/interfaces/definition';
import {NgIf} from './common_with_def';
import {ComponentFixture, createComponent, renderToHtml} from './render_util';
@ -206,8 +206,8 @@ describe('exports', () => {
/** <div [class.red]="myInput.checked"</div> <input type="checkbox" checked #myInput> */
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
elementStart(0, 'div');
elementStyling([InitialStylingFlags.VALUES_MODE, 'red', true]);
elementStart(0, 'div', [AttributeMarker.Classes, 'red']);
elementStyling(['red']);
elementEnd();
element(1, 'input', ['type', 'checkbox', 'checked', 'true'], ['myInput', '']);
}

View File

@ -6,12 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ElementRef, EventEmitter} from '@angular/core';
import {ElementRef} from '@angular/core';
import {AttributeMarker, defineComponent, template, defineDirective, InheritDefinitionFeature, ProvidersFeature, NgOnChangesFeature, QueryList} from '../../src/render3/index';
import {allocHostVars, bind, directiveInject, element, elementEnd, elementProperty, elementStyleProp, elementStyling, elementStylingApply, elementStart, listener, load, text, textBinding, loadQueryList, registerContentQuery} from '../../src/render3/instructions';
import {allocHostVars, bind, directiveInject, element, elementEnd, elementProperty, elementStyleProp, elementStyling, elementStylingApply, elementStart, listener, load, text, textBinding, loadQueryList, registerContentQuery, elementHostAttrs} from '../../src/render3/instructions';
import {query, queryRefresh} from '../../src/render3/query';
import {RenderFlags, InitialStylingFlags} from '../../src/render3/interfaces/definition';
import {RenderFlags} from '../../src/render3/interfaces/definition';
import {pureFunction1, pureFunction2} from '../../src/render3/pure_function';
import {ComponentFixture, TemplateFixture, createComponent, createDirective} from './render_util';
@ -1141,9 +1141,8 @@ describe('host bindings', () => {
vars: 0,
hostBindings: (rf: RenderFlags, ctx: StaticHostClass, elIndex: number) => {
if (rf & RenderFlags.Create) {
elementStyling(
['mat-toolbar', InitialStylingFlags.VALUES_MODE, 'mat-toolbar', true], null, null,
ctx);
elementHostAttrs(ctx, [AttributeMarker.Classes, 'mat-toolbar']);
elementStyling(['mat-toolbar'], null, null, ctx);
}
if (rf & RenderFlags.Update) {
elementStylingApply(0, ctx);
@ -1164,6 +1163,5 @@ describe('host bindings', () => {
const hostBindingEl = fixture.hostElement.querySelector('static-host-class') as HTMLElement;
expect(hostBindingEl.className).toEqual('mat-toolbar');
});
});
});

View File

@ -11,7 +11,6 @@ import {NgForOfContext} from '@angular/common';
import {RenderFlags} from '../../src/render3';
import {defineComponent} from '../../src/render3/definition';
import {bind, element, elementAttribute, elementEnd, elementProperty, elementStart, elementStyleProp, elementStyling, elementStylingApply, elementStylingMap, interpolation1, renderTemplate, template, text, textBinding} from '../../src/render3/instructions';
import {InitialStylingFlags} from '../../src/render3/interfaces/definition';
import {AttributeMarker} from '../../src/render3/interfaces/node';
import {bypassSanitizationTrustHtml, bypassSanitizationTrustResourceUrl, bypassSanitizationTrustScript, bypassSanitizationTrustStyle, bypassSanitizationTrustUrl} from '../../src/sanitization/bypass';
import {defaultStyleSanitizer, sanitizeHtml, sanitizeResourceUrl, sanitizeScript, sanitizeStyle, sanitizeUrl} from '../../src/sanitization/sanitization';
@ -28,10 +27,19 @@ describe('instructions', () => {
elementEnd();
}
function createDiv(initialStyles?: (string | number)[], styleSanitizer?: StyleSanitizeFn) {
elementStart(0, 'div');
elementStyling(
[], initialStyles && Array.isArray(initialStyles) ? initialStyles : null, styleSanitizer);
function createDiv(
initialClasses?: string[] | null, classBindingNames?: string[] | null,
initialStyles?: string[] | null, styleBindingNames?: string[] | null,
styleSanitizer?: StyleSanitizeFn) {
const attrs: any[] = [];
if (initialClasses) {
attrs.push(AttributeMarker.Classes, ...initialClasses);
}
if (initialStyles) {
attrs.push(AttributeMarker.Styles, ...initialStyles);
}
elementStart(0, 'div', attrs);
elementStyling(classBindingNames || null, styleBindingNames || null, styleSanitizer);
elementEnd();
}
@ -191,8 +199,9 @@ describe('instructions', () => {
describe('elementStyleProp', () => {
it('should automatically sanitize unless a bypass operation is applied', () => {
const t = new TemplateFixture(
() => { return createDiv(['background-image'], defaultStyleSanitizer); }, () => {}, 1);
const t = new TemplateFixture(() => {
return createDiv(null, null, null, ['background-image'], defaultStyleSanitizer);
}, () => {}, 1);
t.update(() => {
elementStyleProp(0, 0, 'url("http://server")');
elementStylingApply(0);
@ -211,8 +220,9 @@ describe('instructions', () => {
it('should not re-apply the style value even if it is a newly bypassed again', () => {
const sanitizerInterceptor = new MockSanitizerInterceptor();
const t = createTemplateFixtureWithSanitizer(
() => createDiv(['background-image'], sanitizerInterceptor.getStyleSanitizer()), 1,
sanitizerInterceptor);
() => createDiv(
null, null, null, ['background-image'], sanitizerInterceptor.getStyleSanitizer()),
1, sanitizerInterceptor);
t.update(() => {
elementStyleProp(0, 0, bypassSanitizationTrustStyle('apple'));
@ -232,8 +242,8 @@ describe('instructions', () => {
describe('elementStyleMap', () => {
function createDivWithStyle() {
elementStart(0, 'div');
elementStyling([], ['height', InitialStylingFlags.VALUES_MODE, 'height', '10px']);
elementStart(0, 'div', [AttributeMarker.Styles, 'height', '10px']);
elementStyling([], ['height']);
elementEnd();
}
@ -251,7 +261,8 @@ describe('instructions', () => {
const sanitizerInterceptor =
new MockSanitizerInterceptor(value => { detectedValues.push(value); });
const fixture = createTemplateFixtureWithSanitizer(
() => createDiv([], sanitizerInterceptor.getStyleSanitizer()), 1, sanitizerInterceptor);
() => createDiv(null, null, null, null, sanitizerInterceptor.getStyleSanitizer()), 1,
sanitizerInterceptor);
fixture.update(() => {
elementStylingMap(0, null, {

View File

@ -7,13 +7,13 @@
*/
import {ElementRef, TemplateRef, ViewContainerRef} from '@angular/core';
import {RendererType2} from '../../src/render/api';
import {AttributeMarker, defineComponent, defineDirective, templateRefExtractor} from '../../src/render3/index';
import {allocHostVars, bind, container, containerRefreshEnd, containerRefreshStart, element, elementAttribute, elementClassProp, elementContainerEnd, elementContainerStart, elementEnd, elementProperty, elementStart, elementStyleProp, elementStyling, elementStylingApply, embeddedViewEnd, embeddedViewStart, interpolation1, interpolation2, interpolation3, interpolation4, interpolation5, interpolation6, interpolation7, interpolation8, interpolationV, load, projection, projectionDef, reference, text, textBinding, template, elementStylingMap, directiveInject} from '../../src/render3/instructions';
import {InitialStylingFlags, RenderFlags} from '../../src/render3/interfaces/definition';
import {allocHostVars, bind, container, containerRefreshEnd, containerRefreshStart, elementStart, elementAttribute, elementClassProp, elementContainerEnd, elementContainerStart, elementEnd, elementProperty, element, elementStyling, elementStylingApply, elementStyleProp, embeddedViewEnd, embeddedViewStart, interpolation1, interpolation2, interpolation3, interpolation4, interpolation5, interpolation6, interpolation7, interpolation8, interpolationV, projection, projectionDef, reference, text, textBinding, template, elementStylingMap, directiveInject, elementHostAttrs} from '../../src/render3/instructions';
import {RenderFlags} from '../../src/render3/interfaces/definition';
import {RElement, Renderer3, RendererFactory3, domRendererFactory3} from '../../src/render3/interfaces/renderer';
import {NO_CHANGE} from '../../src/render3/tokens';
import {HEADER_OFFSET, CONTEXT} from '../../src/render3/interfaces/view';
import {enableBindings, disableBindings} from '../../src/render3/state';
import {sanitizeUrl} from '../../src/sanitization/sanitization';
@ -1412,7 +1412,6 @@ describe('render3 integration test', () => {
});
describe('elementStyle', () => {
it('should support binding to styles', () => {
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
@ -1472,8 +1471,7 @@ describe('render3 integration test', () => {
});
});
describe('elementClass', () => {
describe('class-based styling', () => {
it('should support CSS class toggle', () => {
/** <span [class.active]="class"></span> */
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
@ -1519,9 +1517,8 @@ describe('render3 integration test', () => {
it('should work correctly with existing static classes', () => {
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
elementStart(0, 'span');
elementStyling(
['existing', 'active', InitialStylingFlags.VALUES_MODE, 'existing', true]);
elementStart(0, 'span', [AttributeMarker.Classes, 'existing']);
elementStyling(['existing', 'active']);
elementEnd();
}
if (rf & RenderFlags.Update) {
@ -1553,7 +1550,7 @@ describe('render3 integration test', () => {
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
elementStart(0, 'my-comp');
{ elementStyling(['active']); }
elementStyling(['active']);
elementEnd();
}
if (rf & RenderFlags.Update) {
@ -1572,7 +1569,6 @@ describe('render3 integration test', () => {
expect(fixture.html).toEqual('<my-comp class="">Comp Content</my-comp>');
});
it('should apply classes properly when nodes have LContainers', () => {
let structuralComp !: StructuralComp;
@ -1656,17 +1652,17 @@ describe('render3 integration test', () => {
set klass(value: string) { this.classesVal = value; }
}
it('should delegate all initial classes to a [class] input binding if present on a directive on the same element',
it('should delegate initial classes to a [class] input binding if present on a directive on the same element',
() => {
/**
* <my-comp class="apple orange banana" DirWithClass></my-comp>
* <div class="apple orange banana" DirWithClass></div>
*/
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
elementStart(0, 'div', ['DirWithClass']);
elementStyling([
InitialStylingFlags.VALUES_MODE, 'apple', true, 'orange', true, 'banana', true
]);
elementStart(
0, 'div',
['DirWithClass', AttributeMarker.Classes, 'apple', 'orange', 'banana']);
elementStyling();
elementEnd();
}
if (rf & RenderFlags.Update) {
@ -1681,7 +1677,7 @@ describe('render3 integration test', () => {
it('should update `[class]` and bindings in the provided directive if the input is matched',
() => {
/**
* <my-comp DirWithClass></my-comp>
* <div DirWithClass></div>
*/
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
@ -1698,6 +1694,223 @@ describe('render3 integration test', () => {
const fixture = new ComponentFixture(App);
expect(mockClassDirective !.classesVal).toEqual('cucumber grape');
});
it('should apply initial styling to the element that contains the directive with host styling',
() => {
class DirWithInitialStyling {
static ngDirectiveDef = defineDirective({
type: DirWithInitialStyling,
selectors: [['', 'DirWithInitialStyling', '']],
factory: () => new DirWithInitialStyling(),
hostBindings: function(
rf: RenderFlags, ctx: DirWithInitialStyling, elementIndex: number) {
if (rf & RenderFlags.Create) {
elementHostAttrs(ctx, [
AttributeMarker.Classes, 'heavy', 'golden', AttributeMarker.Styles, 'color',
'purple', 'font-weight', 'bold'
]);
}
}
});
public classesVal: string = '';
}
/**
* <div DirWithInitialStyling
* class="big"
* style="color:black; * font-size:200px"></div>
*/
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
element(0, 'div', [
'DirWithInitialStyling', '', AttributeMarker.Classes, 'big',
AttributeMarker.Styles, 'color', 'black', 'font-size', '200px'
]);
}
}, 1, 0, [DirWithInitialStyling]);
const fixture = new ComponentFixture(App);
const target = fixture.hostElement.querySelector('div') !;
const classes = target.getAttribute('class') !.split(/\s+/).sort();
expect(classes).toEqual(['big', 'golden', 'heavy']);
expect(target.style.getPropertyValue('color')).toEqual('black');
expect(target.style.getPropertyValue('font-size')).toEqual('200px');
expect(target.style.getPropertyValue('font-weight')).toEqual('bold');
});
it('should apply single styling bindings present within a directive onto the same element and defer the element\'s initial styling values when missing',
() => {
let dirInstance: DirWithSingleStylingBindings;
/**
* <DirWithInitialStyling class="def" [class.xyz] style="width:555px;" [style.width]
* [style.height]></my-comp>
*/
class DirWithSingleStylingBindings {
static ngDirectiveDef = defineDirective({
type: DirWithSingleStylingBindings,
selectors: [['', 'DirWithSingleStylingBindings', '']],
factory: () => dirInstance = new DirWithSingleStylingBindings(),
hostBindings: function(
rf: RenderFlags, ctx: DirWithSingleStylingBindings, elementIndex: number) {
if (rf & RenderFlags.Create) {
elementHostAttrs(
ctx,
[AttributeMarker.Classes, 'def', AttributeMarker.Styles, 'width', '555px']);
elementStyling(['xyz'], ['width', 'height'], null, ctx);
}
if (rf & RenderFlags.Update) {
elementStyleProp(elementIndex, 0, ctx.width, null, ctx);
elementStyleProp(elementIndex, 1, ctx.height, null, ctx);
elementClassProp(elementIndex, 0, ctx.activateXYZClass, ctx);
elementStylingApply(elementIndex, ctx);
}
}
});
width: null|string = null;
height: null|string = null;
activateXYZClass: boolean = false;
}
/**
* <div DirWithInitialStyling class="abc" style="width:100px;
* height:200px"></div>
*/
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
element(0, 'div', [
'DirWithSingleStylingBindings', '', AttributeMarker.Classes, 'abc',
AttributeMarker.Styles, 'width', '100px', 'height', '200px'
]);
}
}, 1, 0, [DirWithSingleStylingBindings]);
const fixture = new ComponentFixture(App);
const target = fixture.hostElement.querySelector('div') !;
expect(target.style.getPropertyValue('width')).toEqual('100px');
expect(target.style.getPropertyValue('height')).toEqual('200px');
expect(target.classList.contains('abc')).toBeTruthy();
expect(target.classList.contains('def')).toBeTruthy();
expect(target.classList.contains('xyz')).toBeFalsy();
dirInstance !.width = '444px';
dirInstance !.height = '999px';
dirInstance !.activateXYZClass = true;
fixture.update();
expect(target.style.getPropertyValue('width')).toEqual('444px');
expect(target.style.getPropertyValue('height')).toEqual('999px');
expect(target.classList.contains('abc')).toBeTruthy();
expect(target.classList.contains('def')).toBeTruthy();
expect(target.classList.contains('xyz')).toBeTruthy();
dirInstance !.width = null;
dirInstance !.height = null;
fixture.update();
expect(target.style.getPropertyValue('width')).toEqual('100px');
expect(target.style.getPropertyValue('height')).toEqual('200px');
expect(target.classList.contains('abc')).toBeTruthy();
expect(target.classList.contains('def')).toBeTruthy();
expect(target.classList.contains('xyz')).toBeTruthy();
});
it('should properly prioritize style binding collision when they exist on multiple directives',
() => {
let dir1Instance: Dir1WithStyle;
/**
* Directive with host props:
* [style.width]
*/
class Dir1WithStyle {
static ngDirectiveDef = defineDirective({
type: Dir1WithStyle,
selectors: [['', 'Dir1WithStyle', '']],
factory: () => dir1Instance = new Dir1WithStyle(),
hostBindings: function(rf: RenderFlags, ctx: Dir1WithStyle, elementIndex: number) {
if (rf & RenderFlags.Create) {
elementStyling(null, ['width'], null, ctx);
}
if (rf & RenderFlags.Update) {
elementStyleProp(elementIndex, 0, ctx.width, null, ctx);
elementStylingApply(elementIndex, ctx);
}
}
});
width: null|string = null;
}
let dir2Instance: Dir2WithStyle;
/**
* Directive with host props:
* [style.width]
* style="width:111px"
*/
class Dir2WithStyle {
static ngDirectiveDef = defineDirective({
type: Dir2WithStyle,
selectors: [['', 'Dir2WithStyle', '']],
factory: () => dir2Instance = new Dir2WithStyle(),
hostBindings: function(rf: RenderFlags, ctx: Dir2WithStyle, elementIndex: number) {
if (rf & RenderFlags.Create) {
elementHostAttrs(ctx, [AttributeMarker.Styles, 'width', '111px']);
elementStyling(null, ['width'], null, ctx);
}
if (rf & RenderFlags.Update) {
elementStyleProp(elementIndex, 0, ctx.width, null, ctx);
elementStylingApply(elementIndex, ctx);
}
}
});
width: null|string = null;
}
/**
* <div Dir1WithStyle Dir2WithStyle [style.width]></div>
*/
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
element(0, 'div', ['Dir1WithStyle', '', 'Dir2WithStyle', '']);
elementStyling(null, ['width']);
}
if (rf & RenderFlags.Update) {
elementStyleProp(0, 0, ctx.width);
elementStylingApply(0);
}
}, 1, 0, [Dir1WithStyle, Dir2WithStyle]);
const fixture = new ComponentFixture(App);
const target = fixture.hostElement.querySelector('div') !;
expect(target.style.getPropertyValue('width')).toEqual('111px');
fixture.component.width = '999px';
dir1Instance !.width = '222px';
dir2Instance !.width = '333px';
fixture.update();
expect(target.style.getPropertyValue('width')).toEqual('999px');
fixture.component.width = null;
fixture.update();
expect(target.style.getPropertyValue('width')).toEqual('222px');
dir1Instance !.width = null;
fixture.update();
expect(target.style.getPropertyValue('width')).toEqual('333px');
dir2Instance !.width = null;
fixture.update();
expect(target.style.getPropertyValue('width')).toEqual('111px');
dir1Instance !.width = '666px';
fixture.update();
expect(target.style.getPropertyValue('width')).toEqual('666px');
fixture.component.width = '777px';
fixture.update();
expect(target.style.getPropertyValue('width')).toEqual('777px');
});
});
});
@ -2655,4 +2868,4 @@ class ProxyRenderer3Factory implements RendererFactory3 {
this.lastCapturedType = rendererType;
return domRendererFactory3.createRenderer(hostElement, rendererType);
}
}
}

View File

@ -10,6 +10,7 @@ import {PlayState, Player} from '../../../src/render3/interfaces/player';
export class MockPlayer implements Player {
parent: Player|null = null;
data: any;
log: string[] = [];
state: PlayState = PlayState.Pending;
private _listeners: {[state: string]: (() => any)[]} = {};