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

View File

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

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license * 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 {setup} from '@angular/compiler/test/aot/test_util';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../../compiler/src/compiler'; 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 output = String.raw `
const $_c0$ = ["ngFor", "", 1, "ngForOf"]; const $_c0$ = ["ngFor", "", ${AttributeMarker.SelectOnly}, "ngForOf"];
/** /**
* @desc d * @desc d
* @meaning m * @meaning m
@ -522,7 +523,7 @@ describe('i18n support in the view compiler', () => {
`; `;
const output = String.raw ` const output = String.raw `
const $_c0$ = ["ngFor", "", 1, "ngForOf"]; const $_c0$ = ["ngFor", "", ${AttributeMarker.SelectOnly}, "ngForOf"];
/** /**
* @desc d * @desc d
* @meaning m * @meaning m
@ -922,7 +923,7 @@ describe('i18n support in the view compiler', () => {
`; `;
const output = String.raw ` 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}", { 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", "interpolation": "\uFFFD0\uFFFD",
"startTagDiv": "\uFFFD#3\uFFFD", "startTagDiv": "\uFFFD#3\uFFFD",
@ -976,7 +977,7 @@ describe('i18n support in the view compiler', () => {
const output = String.raw ` const output = String.raw `
const $_c0$ = ["src", "logo.png"]; const $_c0$ = ["src", "logo.png"];
const $_c1$ = [1, "ngIf"]; const $_c1$ = [${AttributeMarker.SelectOnly}, "ngIf"];
function MyComponent_img_Template_1(rf, ctx) { function MyComponent_img_Template_1(rf, ctx) {
if (rf & 1) { if (rf & 1) {
$r3$.ɵelement(0, "img", $_c0$); $r3$.ɵelement(0, "img", $_c0$);
@ -1043,7 +1044,7 @@ describe('i18n support in the view compiler', () => {
`; `;
const output = String.raw ` const output = String.raw `
const $_c0$ = [1, "ngIf"]; const $_c0$ = [${AttributeMarker.SelectOnly}, "ngIf"];
function MyComponent_div_div_Template_4(rf, ctx) { function MyComponent_div_div_Template_4(rf, ctx) {
if (rf & 1) { if (rf & 1) {
$r3$.ɵi18nStart(0, $I18N_EXTERNAL_1221890473527419724$$APP_SPEC_TS_0$, 2); $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 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}", { const $MSG_EXTERNAL_119975189388320493$$APP_SPEC_TS__1$ = goog.getMsg("Some other content {$startTagSpan}{$interpolation}{$closeTagSpan}", {
"startTagSpan": "\uFFFD#2\uFFFD", "startTagSpan": "\uFFFD#2\uFFFD",
"interpolation": "\uFFFD0\uFFFD", "interpolation": "\uFFFD0\uFFFD",
@ -1259,23 +1260,21 @@ describe('i18n support in the view compiler', () => {
`; `;
const output = String.raw ` 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 $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"); const $MSG_EXTERNAL_4722270221386399294$$APP_SPEC_TS_2$ = goog.getMsg("Text #2");
consts: 4, consts: 4,
vars: 0, vars: 0,
template: function MyComponent_Template(rf, ctx) { template: function MyComponent_Template(rf, ctx) {
if (rf & 1) { if (rf & 1) {
$r3$.ɵelementStart(0, "span"); $r3$.ɵelementStart(0, "span", $_c0$);
$r3$.ɵi18nStart(1, $MSG_EXTERNAL_5295701706185791735$$APP_SPEC_TS_0$); $r3$.ɵi18nStart(1, $MSG_EXTERNAL_5295701706185791735$$APP_SPEC_TS_0$);
$r3$.ɵelementStyling($_c0$);
$r3$.ɵi18nEnd(); $r3$.ɵi18nEnd();
$r3$.ɵelementEnd(); $r3$.ɵelementEnd();
$r3$.ɵelementStart(2, "span"); $r3$.ɵelementStart(2, "span", $_c1$);
$r3$.ɵi18nStart(3, $MSG_EXTERNAL_4722270221386399294$$APP_SPEC_TS_2$); $r3$.ɵi18nStart(3, $MSG_EXTERNAL_4722270221386399294$$APP_SPEC_TS_2$);
$r3$.ɵelementStyling(null, $_c1$);
$r3$.ɵi18nEnd(); $r3$.ɵi18nEnd();
$r3$.ɵelementEnd(); $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$, { const $I18N_EXTERNAL_7842238767399919809$$APP_SPEC_TS_0$ = $r3$.ɵi18nPostprocess($MSG_EXTERNAL_7842238767399919809$$APP_SPEC_TS_0$, {
"VAR_SELECT": "\uFFFD0\uFFFD" "VAR_SELECT": "\uFFFD0\uFFFD"
}); });
const $_c0$ = [1, "ngIf"]; const $_c0$ = [${AttributeMarker.SelectOnly}, "ngIf"];
const $_c1$ = ["title", "icu only"]; 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 $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$, { 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$, { const $I18N_APP_SPEC_TS_2$ = $r3$.ɵi18nPostprocess($MSG_APP_SPEC_TS_2$, {
"VAR_SELECT": "\uFFFD1\uFFFD" "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 $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$, { const $I18N_APP_SPEC_TS__4$ = $r3$.ɵi18nPostprocess($MSG_APP_SPEC_TS__4$, {
"VAR_SELECT": "\uFFFD0:1\uFFFD" "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$, { const $I18N_EXTERNAL_7842238767399919809$$APP_SPEC_TS_1$ = $r3$.ɵi18nPostprocess($MSG_EXTERNAL_7842238767399919809$$APP_SPEC_TS_1$, {
"VAR_SELECT": "\uFFFD0\uFFFD" "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 $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$, { const $I18N_EXTERNAL_7068143081688428291$$APP_SPEC_TS__3$ = $r3$.ɵi18nPostprocess($MSG_EXTERNAL_7068143081688428291$$APP_SPEC_TS__3$, {
"VAR_SELECT": "\uFFFD0:1\uFFFD" "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$, { const $I18N_EXTERNAL_7825031864601787094$$APP_SPEC_TS_1$ = $r3$.ɵi18nPostprocess($MSG_EXTERNAL_7825031864601787094$$APP_SPEC_TS_1$, {
"VAR_SELECT": "\uFFFD0\uFFFD" "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}}}", { 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" "interpolation": "\uFFFD1:1\uFFFD"
}); });

View File

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

View File

@ -379,13 +379,9 @@ export const enum RenderFlags {
Update = 0b10 Update = 0b10
} }
export const enum InitialStylingFlags {
VALUES_MODE = 0b1,
}
// Pasted from render3/interfaces/node.ts // 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. * items are not regular attributes and the processing should be adapted accordingly.
*/ */
export const enum AttributeMarker { export const enum AttributeMarker {
@ -396,11 +392,48 @@ export const enum AttributeMarker {
*/ */
NamespaceURI = 0, 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.: * This marker indicates that the following attribute names were extracted from bindings (ex.:
* [foo]="exp") and / or event handlers (ex. (bar)="doSth()"). * [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: * Taking the above bindings and outputs as an example an attributes array could look as follows:
* ['class', 'fade in', AttributeMarker.SelectOnly, 'foo', 'bar'] * ['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 elementStyling: o.ExternalReference = {name: 'ɵelementStyling', moduleName: CORE};
static elementHostAttrs: o.ExternalReference = {name: 'ɵelementHostAttrs', moduleName: CORE};
static elementStylingMap: o.ExternalReference = {name: 'ɵelementStylingMap', moduleName: CORE}; static elementStylingMap: o.ExternalReference = {name: 'ɵelementStylingMap', moduleName: CORE};
static elementStyleProp: o.ExternalReference = {name: 'ɵelementStyleProp', 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 {typeWithParameters} from '../util';
import {R3ComponentDef, R3ComponentMetadata, R3DirectiveDef, R3DirectiveMetadata, R3QueryMetadata} from './api'; 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 {BindingScope, TemplateDefinitionBuilder, ValueConverter, renderFlagCheckIfStmt} from './template';
import {CONTEXT_NAME, DefinitionMap, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, conditionallyCreateMapObjectLiteral, getQueryPredicate, temporaryAllocator} from './util'; import {CONTEXT_NAME, DefinitionMap, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, conditionallyCreateMapObjectLiteral, getQueryPredicate, temporaryAllocator} from './util';
@ -709,16 +709,35 @@ function createHostBindingsFunction(
} }
} }
if (styleBuilder.hasBindingsOrInitialValues) { if (styleBuilder.hasBindingsOrInitialValues()) {
const createInstruction = styleBuilder.buildCreateLevelInstruction(null, constantPool); // since we're dealing with directives here and directives have a hostBinding
if (createInstruction) { // function, we need to generate special instructions that deal with styling
const createStmt = createStylingStmt(createInstruction, bindingContext, bindingFn); // (both bindings and initial values). The instruction below will instruct
createStatements.push(createStmt); // 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 => { styleBuilder.buildUpdateLevelInstructions(valueConverter).forEach(instruction => {
const updateStmt = createStylingStmt(instruction, bindingContext, bindingFn); updateStatements.push(createStylingStmt(instruction, bindingContext, bindingFn));
updateStatements.push(updateStmt);
}); });
} }
} }

View File

@ -23,10 +23,15 @@ const enum Char {
* *
* @param value string representation of style as used in the `style` attribute in HTML. * @param value string representation of style as used in the `style` attribute in HTML.
* Example: `color: red; height: auto`. * 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} { export function parse(value: string): string[] {
const styles: {[key: string]: any} = {}; // 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 i = 0;
let parenDepth = 0; let parenDepth = 0;
@ -72,7 +77,7 @@ export function parse(value: string): {[key: string]: any} {
case Char.Semicolon: case Char.Semicolon:
if (currentProp && valueStart > 0 && parenDepth === 0 && quote === Char.QuoteNone) { if (currentProp && valueStart > 0 && parenDepth === 0 && quote === Char.QuoteNone) {
const styleVal = value.substring(valueStart, i - 1).trim(); const styleVal = value.substring(valueStart, i - 1).trim();
styles[currentProp] = valueHasQuotes ? stripUnnecessaryQuotes(styleVal) : styleVal; styles.push(currentProp, valueHasQuotes ? stripUnnecessaryQuotes(styleVal) : styleVal);
propStart = i; propStart = i;
valueStart = 0; valueStart = 0;
currentProp = null; currentProp = null;
@ -84,7 +89,7 @@ export function parse(value: string): {[key: string]: any} {
if (currentProp && valueStart) { if (currentProp && valueStart) {
const styleVal = value.substr(valueStart).trim(); const styleVal = value.substr(valueStart).trim();
styles[currentProp] = valueHasQuotes ? stripUnnecessaryQuotes(styleVal) : styleVal; styles.push(currentProp, valueHasQuotes ? stripUnnecessaryQuotes(styleVal) : styleVal);
} }
return styles; return styles;

View File

@ -6,8 +6,8 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {ConstantPool} from '../../constant_pool'; import {ConstantPool} from '../../constant_pool';
import {InitialStylingFlags} from '../../core'; import {AttributeMarker} from '../../core';
import {AST, BindingType, ParseSpan} from '../../expression_parser/ast'; import {AST, BindingType} from '../../expression_parser/ast';
import * as o from '../../output/output_ast'; import * as o from '../../output/output_ast';
import {ParseSourceSpan} from '../../parse_util'; import {ParseSourceSpan} from '../../parse_util';
import * as t from '../r3_ast'; import * as t from '../r3_ast';
@ -40,6 +40,10 @@ interface BoundStylingEntry {
/** /**
* Produces creation/update instructions for all styling bindings (class and style) * 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: * The builder class below handles producing instructions for the following cases:
* *
* - Static style/class attributes (style="..." and class="...") * - Static style/class attributes (style="..." and class="...")
@ -63,25 +67,57 @@ interface BoundStylingEntry {
* The creation/update methods within the builder class produce these instructions. * The creation/update methods within the builder class produce these instructions.
*/ */
export class StylingBuilder { 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; private _classMapInput: BoundStylingEntry|null = null;
/** the input for [style] (if it exists) */
private _styleMapInput: BoundStylingEntry|null = null; private _styleMapInput: BoundStylingEntry|null = null;
/** an array of each [style.prop] input */
private _singleStyleInputs: BoundStylingEntry[]|null = null; private _singleStyleInputs: BoundStylingEntry[]|null = null;
/** an array of each [class.name] input */
private _singleClassInputs: BoundStylingEntry[]|null = null; private _singleClassInputs: BoundStylingEntry[]|null = null;
private _lastStylingInput: BoundStylingEntry|null = null; private _lastStylingInput: BoundStylingEntry|null = null;
// maps are used instead of hash maps because a Map will // maps are used instead of hash maps because a Map will
// retain the ordering of the keys // 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>(); 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 _classesIndex = new Map<string, number>();
private _initialStyleValues: {[propName: string]: string} = {}; private _initialStyleValues: string[] = [];
private _initialClassValues: {[className: string]: boolean} = {}; private _initialClassValues: string[] = [];
// certain style properties ALWAYS need sanitization
// this is checked each time new styles are encountered
private _useDefaultSanitizer = false; private _useDefaultSanitizer = false;
private _applyFnRequired = false;
constructor(private _elementIndexExpr: o.Expression, private _directiveExpr: o.Expression|null) {} 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 { registerBoundInput(input: t.BoundAttribute): boolean {
// [attr.style] or [attr.class] are skipped in the code below, // [attr.style] or [attr.class] are skipped in the code below,
// they should not be treated as styling-based bindings since // they should not be treated as styling-based bindings since
@ -117,14 +153,12 @@ export class StylingBuilder {
(this._singleStyleInputs = this._singleStyleInputs || []).push(entry); (this._singleStyleInputs = this._singleStyleInputs || []).push(entry);
this._useDefaultSanitizer = this._useDefaultSanitizer || isStyleSanitizable(propertyName); this._useDefaultSanitizer = this._useDefaultSanitizer || isStyleSanitizable(propertyName);
registerIntoMap(this._stylesIndex, propertyName); registerIntoMap(this._stylesIndex, propertyName);
(this as any).hasBindingsOrInitialValues = true;
} else { } else {
this._useDefaultSanitizer = true; this._useDefaultSanitizer = true;
this._styleMapInput = entry; this._styleMapInput = entry;
} }
this._lastStylingInput = entry; this._lastStylingInput = entry;
(this as any).hasBindingsOrInitialValues = true; this._hasBindings = true;
this._applyFnRequired = true;
return entry; return entry;
} }
@ -133,107 +167,152 @@ export class StylingBuilder {
const entry = { name: className, value, sourceSpan } as BoundStylingEntry; const entry = { name: className, value, sourceSpan } as BoundStylingEntry;
if (className) { if (className) {
(this._singleClassInputs = this._singleClassInputs || []).push(entry); (this._singleClassInputs = this._singleClassInputs || []).push(entry);
(this as any).hasBindingsOrInitialValues = true;
registerIntoMap(this._classesIndex, className); registerIntoMap(this._classesIndex, className);
} else { } else {
this._classMapInput = entry; this._classMapInput = entry;
} }
this._lastStylingInput = entry; this._lastStylingInput = entry;
(this as any).hasBindingsOrInitialValues = true; this._hasBindings = true;
this._applyFnRequired = true;
return entry; 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) { registerStyleAttr(value: string) {
this._initialStyleValues = parseStyle(value); this._initialStyleValues = parseStyle(value);
Object.keys(this._initialStyleValues).forEach(prop => { this._hasInitialValues = true;
registerIntoMap(this._stylesIndex, prop);
(this as any).hasBindingsOrInitialValues = 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) { registerClassAttr(value: string) {
this._initialClassValues = {}; this._initialClassValues = value.trim().split(/\s+/g);
value.split(/\s+/g).forEach(className => { this._hasInitialValues = true;
this._initialClassValues[className] = true;
registerIntoMap(this._classesIndex, className);
(this as any).hasBindingsOrInitialValues = true;
});
} }
private _buildInitExpr(registry: Map<string, number>, initialValues: {[key: string]: any}): /**
o.Expression|null { * Appends all styling-related expressions to the provided attrs array.
const exprs: o.Expression[] = []; *
const nameAndValueExprs: o.Expression[] = []; * @param attrs an existing array where each of the styling expressions
* will be inserted into.
// _c0 = [prop, prop2, prop3, ...] */
registry.forEach((value, key) => { populateInitialStylingAttrs(attrs: o.Expression[]): void {
const keyLiteral = o.literal(key); // [CLASS_MARKER, 'foo', 'bar', 'baz' ...]
exprs.push(keyLiteral); if (this._initialClassValues.length) {
const initialValue = initialValues[key]; attrs.push(o.literal(AttributeMarker.Classes));
if (initialValue) { for (let i = 0; i < this._initialClassValues.length; i++) {
nameAndValueExprs.push(keyLiteral, o.literal(initialValue)); 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 { StylingInstruction|null {
if (this.hasBindingsOrInitialValues) { if (this._hasInitialValues && this._directiveExpr) {
const initialClasses = this._buildInitExpr(this._classesIndex, this._initialClassValues); return {
const initialStyles = this._buildInitExpr(this._stylesIndex, this._initialStyleValues); sourceSpan,
reference: R3.elementHostAttrs,
// in the event that a [style] binding is used then sanitization will buildParams: () => {
// always be imported because it is not possible to know ahead of time const attrs: o.Expression[] = [];
// whether style bindings will use or not use any sanitizable properties this.populateInitialStylingAttrs(attrs);
// that isStyleSanitizable() will detect return [this._directiveExpr !, getConstantLiteralFromArray(constantPool, attrs)];
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);
} }
} };
return {sourceSpan, reference: R3.elementStyling, buildParams: () => params};
} }
return null; 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) { if (this._classMapInput || this._styleMapInput) {
const stylingInput = 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) { buildUpdateLevelInstructions(valueConverter: ValueConverter) {
const instructions: StylingInstruction[] = []; const instructions: StylingInstruction[] = [];
if (this.hasBindingsOrInitialValues) { if (this._hasBindings) {
const mapInstruction = this._buildStylingMap(valueConverter); const mapInstruction = this.buildElementStylingMapInstruction(valueConverter);
if (mapInstruction) { if (mapInstruction) {
instructions.push(mapInstruction); instructions.push(mapInstruction);
} }
instructions.push(...this._buildStyleInputs(valueConverter)); instructions.push(...this._buildStyleInputs(valueConverter));
instructions.push(...this._buildClassInputs(valueConverter)); instructions.push(...this._buildClassInputs(valueConverter));
if (this._applyFnRequired) { instructions.push(this._buildApplyFn());
instructions.push(this._buildApplyFn());
}
} }
return instructions; return instructions;
} }
@ -363,3 +444,26 @@ function isStyleSanitizable(prop: string): boolean {
return prop === 'background-image' || prop === 'background' || prop === 'border-image' || return prop === 'background-image' || prop === 'background' || prop === 'border-image' ||
prop === 'filter' || prop === 'list-style' || prop === 'list-style-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 {I18nMetaVisitor} from './i18n/meta';
import {getSerializedI18nContent} from './i18n/serializer'; 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 {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'; 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 { 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 // this will build the instructions so that they fall into the following syntax
// add attributes for directive matching purposes // 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)); parameters.push(this.toAttrsParam(attributes));
// local refs (ex.: <div #foo #bar="baz">) // local refs (ex.: <div #foo #bar="baz">)
@ -562,11 +563,11 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
return element.children.length > 0; return element.children.length > 0;
}; };
const createSelfClosingInstruction = !stylingBuilder.hasBindingsOrInitialValues && const createSelfClosingInstruction = !stylingBuilder.hasBindingsOrInitialValues() &&
!isNgContainer && element.outputs.length === 0 && i18nAttrs.length === 0 && !hasChildren(); !isNgContainer && element.outputs.length === 0 && i18nAttrs.length === 0 && !hasChildren();
const createSelfClosingI18nInstruction = !createSelfClosingInstruction && const createSelfClosingI18nInstruction = !createSelfClosingInstruction &&
!stylingBuilder.hasBindingsOrInitialValues && hasTextChildrenOnly(element.children); !stylingBuilder.hasBindingsOrInitialValues() && hasTextChildrenOnly(element.children);
if (createSelfClosingInstruction) { if (createSelfClosingInstruction) {
this.creationInstruction(element.sourceSpan, R3.element, trimTrailingNulls(parameters)); 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( this.processStylingInstruction(
implicit, implicit,
stylingBuilder.buildCreateLevelInstruction(element.sourceSpan, this.constantPool), true); stylingBuilder.buildElementStylingInstruction(element.sourceSpan, this.constantPool),
true);
// Generate Listeners (outputs) // Generate Listeners (outputs)
element.outputs.forEach((outputAst: t.BoundEvent) => { 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 => { stylingBuilder.buildUpdateLevelInstructions(this._valueConverter).forEach(instruction => {
this.processStylingInstruction(implicit, instruction, false); 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 attrExprs: o.Expression[] = [];
const nonSyntheticInputs: t.BoundAttribute[] = []; 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) { if (nonSyntheticInputs.length || outputs.length) {
attrExprs.push(o.literal(core.AttributeMarker.SelectOnly)); attrExprs.push(o.literal(core.AttributeMarker.SelectOnly));
nonSyntheticInputs.forEach((i: t.BoundAttribute) => attrExprs.push(asLiteral(i.name))); 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', () => { describe('style parsing', () => {
it('should parse empty or blank strings', () => { it('should parse empty or blank strings', () => {
const result1 = parseStyle(''); const result1 = parseStyle('');
expect(result1).toEqual({}); expect(result1).toEqual([]);
const result2 = parseStyle(' '); const result2 = parseStyle(' ');
expect(result2).toEqual({}); expect(result2).toEqual([]);
}); });
it('should parse a string into a key/value map', () => { it('should parse a string into a key/value map', () => {
const result = parseStyle('width:100px;height:200px;opacity:0'); 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', () => { it('should trim values and properties', () => {
const result = parseStyle('width :333px ; height:666px ; opacity: 0.5;'); 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', () => { it('should chomp out start/end quotes', () => {
const result = parseStyle( const result = parseStyle(
'content: "foo"; opacity: \'0.5\'; font-family: "Verdana", Helvetica, "sans-serif"'); 'content: "foo"; opacity: \'0.5\'; font-family: "Verdana", Helvetica, "sans-serif"');
expect(result).toEqual( 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', () => { it('should not mess up with quoted strings that contain [:;] values', () => {
const result = parseStyle('content: "foo; man: guy"; width: 100px'); 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', () => { it('should not mess up with quoted strings that contain inner quote values', () => {
const quoteStr = '"one \'two\' three \"four\" five"'; const quoteStr = '"one \'two\' three \"four\" five"';
const result = parseStyle(`content: ${quoteStr}; width: 123px`); 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', () => { it('should respect parenthesis that are placed within a style', () => {
const result = parseStyle('background-image: url("foo.jpg")'); 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', () => { it('should respect multi-level parenthesis that contain special [:;] characters', () => {
const result = parseStyle('color: rgba(calc(50 * 4), var(--cool), :5;); height: 100px;'); 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', () => { it('should hyphenate style properties from camel case', () => {
const result = parseStyle('borderWidth: 200px'); const result = parseStyle('borderWidth: 200px');
expect(result).toEqual({ expect(result).toEqual(['border-width', '200px']);
'border-width': '200px',
});
}); });
describe('quote chomping', () => { describe('quote chomping', () => {

View File

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

View File

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

View File

@ -7,12 +7,13 @@
*/ */
import './ng_dev_mode'; import './ng_dev_mode';
import {assertDomNode} from './assert'; import {assertDomNode} from './assert';
import {EMPTY_ARRAY} from './definition'; import {EMPTY_ARRAY} from './empty';
import {LContext, MONKEY_PATCH_KEY_NAME} from './interfaces/context'; import {LContext, MONKEY_PATCH_KEY_NAME} from './interfaces/context';
import {TNode, TNodeFlags} from './interfaces/node'; import {TNode, TNodeFlags} from './interfaces/node';
import {RElement} from './interfaces/renderer'; import {RElement} from './interfaces/renderer';
import {CONTEXT, HEADER_OFFSET, HOST, LView, TVIEW} from './interfaces/view'; 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. /** 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 './ng_dev_mode';
import {ChangeDetectionStrategy} from '../change_detection/constants'; import {ChangeDetectionStrategy} from '../change_detection/constants';
import {Provider} from '../di/provider';
import {NgModuleDef} from '../metadata/ng_module'; import {NgModuleDef} from '../metadata/ng_module';
import {ViewEncapsulation} from '../metadata/view'; import {ViewEncapsulation} from '../metadata/view';
import {Mutable, Type} from '../type'; import {Mutable, Type} from '../type';
import {noSideEffects, stringify} from '../util'; 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 {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 {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; 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 { function invertObject(obj: any, secondary?: any): any {
if (obj == null) return EMPTY; if (obj == null) return EMPTY_OBJ;
const newLookup: any = {}; const newLookup: any = {};
for (const minifiedKey in obj) { for (const minifiedKey in obj) {
if (obj.hasOwnProperty(minifiedKey)) { if (obj.hasOwnProperty(minifiedKey)) {

View File

@ -7,6 +7,7 @@
*/ */
import {Injector} from '../di/injector'; import {Injector} from '../di/injector';
import {assertDefined} from './assert'; import {assertDefined} from './assert';
import {discoverLocalRefs, getComponentAtNodeIndex, getDirectivesAtNodeIndex, getLContext} from './context_discovery'; import {discoverLocalRefs, getComponentAtNodeIndex, getDirectivesAtNodeIndex, getLContext} from './context_discovery';
import {NodeInjector} from './di'; import {NodeInjector} from './di';
@ -14,7 +15,8 @@ import {LContext} from './interfaces/context';
import {DirectiveDef} from './interfaces/definition'; import {DirectiveDef} from './interfaces/definition';
import {TElementNode, TNode, TNodeProviderIndexes} from './interfaces/node'; import {TElementNode, TNode, TNodeProviderIndexes} from './interfaces/node';
import {CLEANUP, CONTEXT, FLAGS, HOST, LView, LViewFlags, PARENT, RootContext, TVIEW} from './interfaces/view'; 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++]; const secondParam = tCleanup[i++];
if (typeof firstParam === 'string') { if (typeof firstParam === 'string') {
const name: string = firstParam; 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 callback: (value: any) => any = lCleanup[tCleanup[i++]];
const useCaptureOrIndx = tCleanup[i++]; const useCaptureOrIndx = tCleanup[i++];
// if useCaptureOrIndx is boolean then report it as is. // 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 {Type} from '../../type';
import {fillProperties} from '../../util/property'; import {fillProperties} from '../../util/property';
import {EMPTY, EMPTY_ARRAY} from '../definition'; import {EMPTY_ARRAY, EMPTY_OBJ} from '../empty';
import {ComponentDef, ComponentTemplate, DirectiveDef, DirectiveDefFeature, RenderFlags} from '../interfaces/definition'; 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<T>(value: T): T; function maybeUnwrapEmpty<T>(value: T): T;
function maybeUnwrapEmpty(value: any): any { function maybeUnwrapEmpty(value: any): any {
if (value === EMPTY) { if (value === EMPTY_OBJ) {
return {}; return {};
} else if (value === EMPTY_ARRAY) { } else if (value === EMPTY_ARRAY) {
return []; return [];

View File

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

View File

@ -15,6 +15,7 @@ import {Sanitizer} from '../sanitization/security';
import {StyleSanitizeFn} from '../sanitization/style_sanitizer'; import {StyleSanitizeFn} from '../sanitization/style_sanitizer';
import {Type} from '../type'; import {Type} from '../type';
import {normalizeDebugBindingName, normalizeDebugBindingValue} from '../util/ng_reflect'; import {normalizeDebugBindingName, normalizeDebugBindingValue} from '../util/ng_reflect';
import {assertDataInRange, assertDefined, assertEqual, assertHasParent, assertLessThan, assertNotEqual, assertPreviousIsParent} from './assert'; import {assertDataInRange, assertDefined, assertEqual, assertHasParent, assertLessThan, assertNotEqual, assertPreviousIsParent} from './assert';
import {bindingUpdated, bindingUpdated2, bindingUpdated3, bindingUpdated4} from './bindings'; import {bindingUpdated, bindingUpdated2, bindingUpdated3, bindingUpdated4} from './bindings';
import {attachPatchData, getComponentViewByInstance} from './context_discovery'; import {attachPatchData, getComponentViewByInstance} from './context_discovery';
@ -22,7 +23,7 @@ import {diPublicInInjector, getNodeInjectable, getOrCreateInjectable, getOrCreat
import {throwMultipleComponentError} from './errors'; import {throwMultipleComponentError} from './errors';
import {executeHooks, executeInitHooks, queueInitHooks, queueLifecycleHooks} from './hooks'; import {executeHooks, executeInitHooks, queueInitHooks, queueLifecycleHooks} from './hooks';
import {ACTIVE_INDEX, LContainer, VIEWS} from './interfaces/container'; 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 {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 {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'; 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 {LQueries} from './interfaces/query';
import {ProceduralRenderer3, RComment, RElement, RText, Renderer3, RendererFactory3, isProceduralRenderer} from './interfaces/renderer'; import {ProceduralRenderer3, RComment, RElement, RText, Renderer3, RendererFactory3, isProceduralRenderer} from './interfaces/renderer';
import {SanitizerFn} from './interfaces/sanitization'; 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 {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 {assertNodeOfPossibleTypes, assertNodeType} from './node_assert';
import {appendChild, appendProjectedNode, createTextNode, getLViewChild, getRenderParent, insertView, removeView} from './node_manipulation'; import {appendChild, appendProjectedNode, createTextNode, getLViewChild, getRenderParent, insertView, removeView} from './node_manipulation';
import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher'; 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 {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 {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 {NO_CHANGE} from './tokens';
import {findComponentView, getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getRootContext, getRootView, getTNode, isComponent, isComponentDef, loadInternal, readElementValue, readPatchedLView, stringify} from './util'; 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 * A permanent marker promise which signifies that the current CD tree is
* clean. * clean.
@ -456,7 +457,8 @@ export function namespaceHTML() {
* *
* @param index Index of the element in the data array * @param index Index of the element in the data array
* @param name Name of the DOM Node * @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. * @param localRefs A set of local reference bindings on the element.
*/ */
export function element( export function element(
@ -526,7 +528,8 @@ export function elementContainerEnd(): void {
* *
* @param index Index of the element in the LView array * @param index Index of the element in the LView array
* @param name Name of the DOM Node * @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. * @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 * 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); const tNode = createNodeAtIndex(index, TNodeType.Element, native !, name, attrs || null);
if (attrs) { 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); setUpAttributes(native, attrs);
} }
@ -563,6 +574,23 @@ export function elementStart(
attachPatchData(native, lView); attachPatchData(native, lView);
} }
increaseElementDepthCount(); 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; let i = 0;
while (i < attrs.length) { while (i < attrs.length) {
const attrName = attrs[i]; const attrName = attrs[i++];
if (attrName === AttributeMarker.SelectOnly) break; if (typeof attrName == 'number') {
if (attrName === NG_PROJECT_AS_ATTR_NAME) {
i += 2;
} else {
ngDevMode && ngDevMode.rendererSetAttribute++;
if (attrName === AttributeMarker.NamespaceURI) { if (attrName === AttributeMarker.NamespaceURI) {
// Namespaced attributes // Namespaced attributes
const namespaceURI = attrs[i + 1] as string; const namespaceURI = attrs[i++] as string;
const attrName = attrs[i + 2] as string; const attrName = attrs[i++] as string;
const attrVal = attrs[i + 3] as string; const attrVal = attrs[i++] as string;
ngDevMode && ngDevMode.rendererSetAttribute++;
isProc ? isProc ?
(renderer as ProceduralRenderer3) (renderer as ProceduralRenderer3)
.setAttribute(native, attrName, attrVal, namespaceURI) : .setAttribute(native, attrName, attrVal, namespaceURI) :
native.setAttributeNS(namespaceURI, attrName, attrVal); native.setAttributeNS(namespaceURI, attrName, attrVal);
i += 4;
} else { } 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 // Standard attributes
const attrVal = attrs[i + 1]; ngDevMode && ngDevMode.rendererSetAttribute++;
if (isAnimationProp(attrName)) { if (isAnimationProp(attrName)) {
if (isProc) { if (isProc) {
(renderer as ProceduralRenderer3).setProperty(native, attrName, attrVal); (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) : .setAttribute(native, attrName as string, attrVal as string) :
native.setAttribute(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); queueLifecycleHooks(getLView()[TVIEW], previousOrParentTNode);
decreaseElementDepthCount(); 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; 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. * 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 * This instruction is meant to be called during creation mode to register all
* (e.g. `style="..."`) values to the element. This is also where the provided index * dynamic style and class bindings on the element. Note for static values (no binding)
* value is allocated for the styling details for its corresponding element (the element * see `elementStart` and `elementHostAttrs`.
* index is the previous index value from this one).
* *
* (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.
* *
* * @publicApi
* @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.
*/ */
export function elementStyling( export function elementStyling(
classDeclarations?: (string | boolean | InitialStylingFlags)[] | null, classBindingNames?: string[] | null, styleBindingNames?: string[] | null,
styleDeclarations?: (string | boolean | InitialStylingFlags)[] | null,
styleSanitizer?: StyleSanitizeFn | null, directive?: {}): void { 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 tNode = getPreviousOrParentTNode();
const inputData = initializeTNodeInputs(tNode);
if (!tNode.stylingTemplate) { if (!tNode.stylingTemplate) {
const hasClassInput = inputData && inputData.hasOwnProperty('class') ? true : false; tNode.stylingTemplate = createEmptyStylingContext();
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);
} }
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` * This instruction is meant to be run after `elementStyle` and/or `elementStyleProp`.
* have been issued against the element. This function will also determine if any styles have * if any styling bindings have changed then the changes are flushed to the element.
* changed and will then skip the operation if there is nothing new to render.
* *
* Once called then all queued styles will be flushed.
* *
* @param index Index of the element's styling storage that will be rendered. * @param index Index of the element's with which styling is associated.
* (Note that this is not the element index, but rather an index value allocated * @param directive Directive instance that is attempting to change styling. (Defaults to the
* specifically for element styling--the index must be the next index after the element * component of the current view).
* index.) components
* @param directive the ref to the directive that is attempting to change styling. *
* @publicApi
*/ */
export function elementStylingApply(index: number, directive?: {}): void { export function elementStylingApply(index: number, directive?: any): void {
if (directive != undefined) {
return hackImplementationOfElementStylingApply(index, directive); // supported in next PR
}
const lView = getLView(); const lView = getLView();
const isFirstRender = (lView[FLAGS] & LViewFlags.FirstLViewPass) !== 0; const isFirstRender = (lView[FLAGS] & LViewFlags.FirstLViewPass) !== 0;
const totalPlayersQueued = renderStyleAndClassBindings( const totalPlayersQueued = renderStyling(
getStylingContext(index + HEADER_OFFSET, lView), lView[RENDERER], lView, isFirstRender); getStylingContext(index + HEADER_OFFSET, lView), lView[RENDERER], lView, isFirstRender, null,
null, directive);
if (totalPlayersQueued > 0) { if (totalPlayersQueued > 0) {
const rootContext = getRootContext(lView); const rootContext = getRootContext(lView);
scheduleTick(rootContext, RootContextFlags.FlushPlayers); 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 * 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 * (or assigned a different value depending if there are any styles placed
* on the element with `elementStyle` or any styles that are present * on the element with `elementStyle` or any styles that are present
* from when the element was created (with `elementStyling`). * 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. * @param index Index of the element's with which styling is associated.
* (Note that this is not the element index, but rather an index value allocated * @param styleIndex Index of style to update. This index value refers to the
* specifically for element styling--the index must be the next index after the element * index of the style in the style bindings array that was passed into
* index.) * `elementStlyingBindings`.
* @param styleIndex Index of the style property on this element. (Monotonically increasing.) * @param value New value to write (null to remove). Note that if a directive also
* @param value New value to write (null to remove). * 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`. * @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 * Note that when a suffix is provided then the underlying sanitizer will
* be ignored. * 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( export function elementStyleProp(
index: number, styleIndex: number, value: string | number | String | PlayerFactory | null, index: number, styleIndex: number, value: string | number | String | PlayerFactory | null,
suffix?: string, directive?: {}): void { suffix?: string | null, directive?: {}): void {
let valueToAdd: string|null = null; let valueToAdd: string|null = null;
if (value !== null) { if (value !== null) {
if (suffix) { if (suffix) {
@ -1253,35 +1262,59 @@ export function elementStyleProp(
valueToAdd = value as any as string; valueToAdd = value as any as string;
} }
} }
if (directive != undefined) { updateElementStyleProp(
hackImplementationOfElementStyleProp(index, styleIndex, valueToAdd, suffix, directive); getStylingContext(index + HEADER_OFFSET, getLView()), styleIndex, valueToAdd, directive);
} else {
updateElementStyleProp(
getStylingContext(index + HEADER_OFFSET, getLView()), styleIndex, valueToAdd);
}
} }
/** /**
* 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 * This instruction is meant to handle the [class.foo]="exp" case and, therefore,
* the Element they will then be placed with respect to any styles set with `elementStyleProp`. * the class itself must already be applied using `elementStyling` within
* If any styles are set to `null` then they will be removed from the element (unless the same * the creation block.
* style properties have been assigned to the element during creation using `elementStyling`). *
* @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.) * (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. * @param index Index of the element's with which styling is associated.
* (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 classes A key/value style map of CSS classes that will be added to the given element. * @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 * Any missing classes (that have already been applied to the element beforehand) will be
* removed (unset) from the element's list of CSS classes. * 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. * @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 * Any missing styles (that have already been applied to the element beforehand) will be
* removed (unset) from the element's styling. * 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>( export function elementStylingMap<T>(
index: number, classes: {[key: string]: any} | string | NO_CHANGE | null, index: number, classes: {[key: string]: any} | string | NO_CHANGE | null,
@ -1292,113 +1325,24 @@ export function elementStylingMap<T>(
const lView = getLView(); const lView = getLView();
const tNode = getTNode(index, lView); const tNode = getTNode(index, lView);
const stylingContext = getStylingContext(index + HEADER_OFFSET, lView); const stylingContext = getStylingContext(index + HEADER_OFFSET, lView);
if (delegateToClassInput(tNode) && classes !== NO_CHANGE) { if (hasClassInput(tNode) && classes !== NO_CHANGE) {
const initialClasses = stylingContext[StylingIndex.PreviousOrCachedMultiClassValue] as string; const initialClasses = getInitialClassNameValue(stylingContext);
const classInputVal = const classInputVal =
(initialClasses.length ? (initialClasses + ' ') : '') + (classes as string); (initialClasses.length ? (initialClasses + ' ') : '') + (classes as string);
setInputsForProperty(lView, tNode.inputs !['class'] !, classInputVal); setInputsForProperty(lView, tNode.inputs !['class'] !, classInputVal);
} else {
updateStylingMap(stylingContext, classes, styles);
} }
updateStylingMap(stylingContext, classes, styles);
} }
/* START OF HACK BLOCK */ /* 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>( function hackImplementationOfElementStylingMap<T>(
index: number, classes: {[key: string]: any} | string | NO_CHANGE | null, index: number, classes: {[key: string]: any} | string | NO_CHANGE | null,
styles?: {[styleName: string]: any} | NO_CHANGE | null, directive?: {}): void { styles?: {[styleName: string]: any} | NO_CHANGE | null, directive?: {}): void {
throw new Error('unimplemented. Should not be needed by ViewEngine compatibility'); throw new Error('unimplemented. Should not be needed by ViewEngine compatibility');
} }
/* END OF HACK BLOCK */ /* END OF HACK BLOCK */
////////////////////////// //////////////////////////
//// Text //// Text
////////////////////////// //////////////////////////
@ -2885,10 +2829,6 @@ function initializeTNodeInputs(tNode: TNode | null) {
return null; return null;
} }
export function delegateToClassInput(tNode: TNode) {
return tNode.flags & TNodeFlags.hasClassInput;
}
/** /**
* Returns the current OpaqueViewState instance. * 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. // As we already have these constants in LView, we don't need to re-create them.
export const NATIVE = 6; export const NATIVE = 6;
export const RENDER_PARENT = 7; 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. * 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 // Note: This hack is necessary so we don't erroneously get a circular dependency
// failure based on types. // failure based on types.
export const unusedValueExportToPlacateAjd = 1; export const unusedValueExportToPlacateAjd = 1;
export const enum InitialStylingFlags {
VALUES_MODE = 0b1,
}

View File

@ -54,7 +54,7 @@ export const enum TNodeProviderIndexes {
CptViewProvidersCountShifter = 0b00000000000000010000000000000000, 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. * items are not regular attributes and the processing should be adapted accordingly.
*/ */
export const enum AttributeMarker { export const enum AttributeMarker {
@ -65,13 +65,50 @@ export const enum AttributeMarker {
*/ */
NamespaceURI = 0, 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.: * This marker indicates that the following attribute names were extracted from bindings (ex.:
* [foo]="exp") and / or event handlers (ex. (bar)="doSth()"). * [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: * Taking the above bindings and outputs as an example an attributes array could look as follows:
* ['class', 'fade in', AttributeMarker.SelectOnly, 'foo', 'bar'] * ['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 {RElement} from '../interfaces/renderer';
import {PlayerContext} from './player'; import {PlayerContext} from './player';
/** /**
* The styling context acts as a styling manifest (shaped as an array) for determining which * The styling context acts as a styling manifest (shaped as an array) for determining which
* styling properties have been assigned via the provided `updateStylingMap`, `updateStyleProp` * styling properties have been assigned via the provided `updateStylingMap`, `updateStyleProp`
* and `updateClassProp` functions. There are also two initialization functions * and `updateClassProp` functions. It also stores the static style/class values that were
* `allocStylingContext` and `createStylingContextTemplate` which are used to initialize * extracted from the template by the compiler.
* and/or clone the context.
* *
* The context is an array where the first two cells are used for static data (initial styling) * A context is created by Angular when:
* and dirty flags / index offsets). The remaining set of cells is used for multi (map) and single * 1. An element contains static styling values (like style="..." or class="...")
* (prop) style values. * 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: * Note that even if an element contains static styling then this context will be created and
* [i] = mutation/type flag for the style/class value * attached to it. The reason why this happens (instead of treating styles/classes as regular
* [i + 1] = prop string (or null incase it has been removed) * HTML attributes) is because the style/class bindings must be able to default themselves back
* [i + 2] = value string (or null incase it has been removed) * to their respective static values when they are set to null.
*
* 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:
* *
* 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 = [ * context = [
* element, * // 0-8: header values (about 8 entries of configuration data)
* playerContext | null, * // 9+: this is where each entry is stored:
* 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,
* ] * ]
* *
* function pointers(staticIndex: number, dynamicIndex: number) { * Let's say we have the following template code:
* // combine the two indices into a single word. *
* return (staticIndex << StylingFlags.BitCountSize) | * ```
* (dynamicIndex << (StylingIndex.BitCountSize + StylingFlags.BitCountSize)); * <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]) * Since there is a style binding for width present on the element (`[style.width]`) then
* and single ([style.prop] or [class.named]) values. The respective config values * it will always win over the width binding that is present as a host binding within
* (configValA, configValB, etc...) are a combination of the StylingFlags with two index * the `MyWidthDirective`. However, if `[style.width]` renders as `null` (so `myWidth=null`)
* values: the `initialIndex` (which points to the index location of the style value in * then the `MyWidthDirective` will be able to write to the `width` style within the context.
* the initial styles array in slot 0) and the `dynamicIndex` (which points to the * Simply put, whichever directive writes to a value ends up having ownership of it.
* matching single/multi index position in the context array for the same prop).
* *
* This means that every time `updateStyleProp` or `updateClassProp` are called then they * The way in which the ownership is facilitated is through index value. The earliest directives
* must be called using an index value (not a property string) which references the index * get the smallest index values (with 0 being reserved for the template element bindings). Each
* value of the initial style prop/class when the context was created. This also means that * time a value is written from a directive or the template bindings, the value itself gets
* `updateStyleProp` or `updateClassProp` cannot be called with a new property (only * assigned the directive index value in its data. If another directive writes a value again then
* `updateStylingMap` can include new CSS properties that will be added to the context). * 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| export interface StylingContext extends
boolean|RElement|StyleSanitizeFn|PlayerContext|null> { Array<{[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;
/** /**
* A numeric value representing the configuration status (whether the context is dirty or not) * A numeric value representing the configuration status (whether the context is dirty or not)
* mixed together (using bit shifting) with a index value which tells the starting index value * mixed together (using bit shifting) with a index value which tells the starting index value
@ -140,12 +209,27 @@ export interface StylingContext extends Array<InitialStyles|{[key: string]: any}
*/ */
[StylingIndex.MasterFlagPosition]: number; [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 * 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 * 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. * 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. * 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 * 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. * 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 * 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. * 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 * Used as a styling array to house static class and style values that were extracted
* the context during allocation. The 0th value must be null so that index values of `0` within * by the compiler and placed in the animation context via `elementStart` and
* the context flags can always point to a null value safely when nothing is set. * `elementHostAttrs`.
* *
* All other entries in this array are of `string` value and correspond to the values that * See [InitialStylingValuesIndex] for a breakdown of how all this works.
* were extracted from the `style=""` attribute in the HTML code for the provided template.
*/ */
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) * 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 { export const enum StylingFlags {
// Implies no configurations // Implies no configurations
None = 0b00000, None = 0b000000,
// Whether or not the entry or context itself is dirty // Whether or not the entry or context itself is dirty
Dirty = 0b00001, Dirty = 0b000001,
// Whether or not this is a class-based assignment // Whether or not this is a class-based assignment
Class = 0b00010, Class = 0b000010,
// Whether or not a sanitizer was applied to this property // 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 // 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 // If NgClass is present (or some other class handler) then it will handle the map expressions and
// initial classes // initial classes
OnlyProcessSingleClasses = 0b10000, OnlyProcessSingleClasses = 0b010000,
// The max amount of bits used to represent these configuration values // The max amount of bits used to represent these configuration values
BitCountSize = 5, BindingAllocationLocked = 0b100000,
// There are only five bits here BitCountSize = 6,
BitMask = 0b11111 // There are only six bits here
BitMask = 0b111111
} }
/** Used as numeric pointer values to determine what cells to update in the `StylingContext` */ /** Used as numeric pointer values to determine what cells to update in the `StylingContext` */
export const enum StylingIndex { export const enum StylingIndex {
// Position of where the initial styles are stored in the styling context
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`) // 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 // 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 // Position of where the initial styles are stored in the styling context
// This index must align with HOST, see interfaces/view.ts // This index must align with HOST, see interfaces/view.ts
ElementPosition = 5, ElementPosition = 5,
// Position of where the last string-based CSS class value was stored (or a cached version of the // 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) // initial styles when a [class] directive is present)
PreviousOrCachedMultiClassValue = 6, CachedClassValueOrInitialClassString = 6,
// Position of where the last string-based CSS class value was stored // Position of where the last string-based CSS class value was stored
PreviousMultiStyleValue = 7, CachedStyleValue = 7,
// Location of single (prop) value entries are stored within the context
SingleStylesStartPosition = 8,
// Multi and single entries are stored in `StylingContext` as: Flag; PropertyName; PropertyValue // 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, FlagsOffset = 0,
PropertyOffset = 1, PropertyOffset = 1,
ValueOffset = 2, ValueOffset = 2,
@ -233,3 +501,16 @@ export const enum StylingIndex {
// The binary digit value as a mask // The binary digit value as a mask
BitMask = 0b11111111111111, // 14 bits 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 {componentNeedsResolution, maybeQueueResolutionOfComponentResources} from '../../metadata/resource_loading';
import {ViewEncapsulation} from '../../metadata/view'; import {ViewEncapsulation} from '../../metadata/view';
import {Type} from '../../type'; import {Type} from '../../type';
import {stringify} from '../../util'; import {EMPTY_ARRAY, EMPTY_OBJ} from '../empty';
import {EMPTY_ARRAY} from '../definition';
import {NG_COMPONENT_DEF, NG_DIRECTIVE_DEF} from '../fields'; import {NG_COMPONENT_DEF, NG_DIRECTIVE_DEF} from '../fields';
import {stringify} from '../util';
import {R3DirectiveMetadataFacade, getCompilerFacade} from './compiler_facade'; import {R3DirectiveMetadataFacade, getCompilerFacade} from './compiler_facade';
import {R3ComponentMetadataFacade, R3QueryMetadataFacade} from './compiler_facade_interface'; 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[] { function convertToR3QueryPredicate(selector: any): any|string[] {
return typeof selector === 'string' ? splitByComma(selector) : selector; return typeof selector === 'string' ? splitByComma(selector) : selector;
} }

View File

@ -90,6 +90,7 @@ export const angularCoreEnv: {[name: string]: Function} = {
'ɵregisterContentQuery': r3.registerContentQuery, 'ɵregisterContentQuery': r3.registerContentQuery,
'ɵreference': r3.reference, 'ɵreference': r3.reference,
'ɵelementStyling': r3.elementStyling, 'ɵelementStyling': r3.elementStyling,
'ɵelementHostAttrs': r3.elementHostAttrs,
'ɵelementStylingMap': r3.elementStylingMap, 'ɵelementStylingMap': r3.elementStylingMap,
'ɵelementStyleProp': r3.elementStyleProp, 'ɵelementStyleProp': r3.elementStyleProp,
'ɵelementStylingApply': r3.elementStylingApply, 'ɵ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 {StyleSanitizeFn} from '../../sanitization/style_sanitizer';
import {getLContext} from '../context_discovery'; import {getLContext} from '../context_discovery';
import {ACTIVE_INDEX, LContainer} from '../interfaces/container'; import {LContainer} from '../interfaces/container';
import {LContext} from '../interfaces/context'; import {LContext} from '../interfaces/context';
import {AttributeMarker, TAttributes, TNode, TNodeFlags} from '../interfaces/node';
import {PlayState, Player, PlayerContext, PlayerIndex} from '../interfaces/player'; import {PlayState, Player, PlayerContext, PlayerIndex} from '../interfaces/player';
import {RElement} from '../interfaces/renderer'; import {RElement} from '../interfaces/renderer';
import {InitialStyles, StylingContext, StylingIndex} from '../interfaces/styling'; import {InitialStylingValues, StylingContext, StylingFlags, StylingIndex} from '../interfaces/styling';
import {FLAGS, HEADER_OFFSET, HOST, LView, RootContext} from '../interfaces/view'; import {HEADER_OFFSET, HOST, LView, RootContext} from '../interfaces/view';
import {getTNode} from '../util'; import {getTNode} from '../util';
import {CorePlayerHandler} from './core_player_handler'; import {CorePlayerHandler} from './core_player_handler';
@ -23,16 +24,18 @@ const ANIMATION_PROP_PREFIX = '@';
export function createEmptyStylingContext( export function createEmptyStylingContext(
element?: RElement | null, sanitizer?: StyleSanitizeFn | null, element?: RElement | null, sanitizer?: StyleSanitizeFn | null,
initialStylingValues?: InitialStyles): StylingContext { initialStyles?: InitialStylingValues | null,
initialClasses?: InitialStylingValues | null): StylingContext {
return [ return [
null, // PlayerContext 0, // MasterFlags
sanitizer || null, // StyleSanitizer [null, -1, false, sanitizer || null], // DirectiveRefs
initialStylingValues || [null], // InitialStyles initialStyles || [null], // InitialStyles
0, // MasterFlags initialClasses || [null], // InitialClasses
0, // ClassOffset [0, 0], // SinglePropOffsets
element || null, // Element element || null, // Element
null, // PreviousMultiClassValue null, // PreviousMultiClassValue
null // PreviousMultiStyleValue null, // PreviousMultiStyleValue
null, // PlayerContext
]; ];
} }
@ -47,6 +50,9 @@ export function allocStylingContext(
// each instance gets a copy // each instance gets a copy
const context = templateStyleContext.slice() as any as StylingContext; const context = templateStyleContext.slice() as any as StylingContext;
context[StylingIndex.ElementPosition] = element; context[StylingIndex.ElementPosition] = element;
// this will prevent any other directives from extending the context
context[StylingIndex.MasterFlagPosition] |= StylingFlags.BindingAllocationLocked;
return context; return context;
} }
@ -89,8 +95,8 @@ export function getStylingContext(index: number, viewData: LView): StylingContex
export function isStylingContext(value: any): value is StylingContext { export function isStylingContext(value: any): value is StylingContext {
// Not an LView or an LContainer // Not an LView or an LContainer
return Array.isArray(value) && typeof value[FLAGS] !== 'number' && return Array.isArray(value) && typeof value[StylingIndex.MasterFlagPosition] === 'number' &&
typeof value[ACTIVE_INDEX] !== 'number'; Array.isArray(value[StylingIndex.InitialStyleValuesPosition]);
} }
export function isAnimationProp(name: string): boolean { export function isAnimationProp(name: string): boolean {
@ -182,3 +188,15 @@ export function allocPlayerContext(data: StylingContext): PlayerContext {
export function throwInvalidRefError() { export function throwInvalidRefError() {
throw new Error('Only elements that exist in an Angular application can be used for animations'); 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 {global} from '../util';
import {assertDataInRange, assertDefined, assertGreaterThan, assertLessThan} from './assert'; 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 {LContext, MONKEY_PATCH_KEY_NAME} from './interfaces/context';
import {ComponentDef, DirectiveDef} from './interfaces/definition'; import {ComponentDef, DirectiveDef} from './interfaces/definition';
import {NO_PARENT_INJECTOR, RelativeInjectorLocation, RelativeInjectorLocationFlags} from './interfaces/injector'; 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 {StylingContext} from './interfaces/styling';
import {CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, LView, LViewFlags, PARENT, RootContext, TData, TVIEW, TView} from './interfaces/view'; 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. * 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 { export function isLContainer(value: RElement | RComment | LContainer | StylingContext): boolean {
// Styling contexts are also arrays, but their first index contains an element node // 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 { export function isRootView(target: LView): boolean {

View File

@ -9,7 +9,29 @@
import '@angular/core/test/bundling/util/src/reflect_metadata'; import '@angular/core/test/bundling/util/src/reflect_metadata';
import {CommonModule} from '@angular/common'; 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({ @Component({
selector: 'animation-world', selector: 'animation-world',
@ -20,21 +42,40 @@ import {Component, ElementRef, NgModule, ɵPlayState as PlayState, ɵPlayer as P
</nav> </nav>
<div class="list"> <div class="list">
<div <div
*ngFor="let item of items" class="record" [class]="makeClass(item)" style="border-radius: 10px" #makeColorGrey="makeColorGrey"
[style]="styles"> make-color-grey
{{ item }} *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>
</div> </div>
`, `,
}) })
class AnimationWorldComponent { 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; private _hostElement: HTMLElement;
public styles: {[key: string]: any}|null = null; public styles: {[key: string]: any}|null = null;
constructor(element: ElementRef) { this._hostElement = element.nativeElement; } 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() { animateWithStyles() {
this.styles = animateStyleFactory([{opacity: 0}, {opacity: 1}], 300, 'ease-out'); 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 { class AnimationWorldModule {
} }

View File

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

View File

@ -45,10 +45,10 @@
"name": "DefaultIterableDifferFactory" "name": "DefaultIterableDifferFactory"
}, },
{ {
"name": "EMPTY" "name": "EMPTY_ARRAY"
}, },
{ {
"name": "EMPTY_ARRAY" "name": "EMPTY_OBJ"
}, },
{ {
"name": "ElementRef" "name": "ElementRef"
@ -86,6 +86,9 @@
{ {
"name": "IterableDiffers" "name": "IterableDiffers"
}, },
{
"name": "LCONTAINER_LENGTH"
},
{ {
"name": "MONKEY_PATCH_KEY_NAME" "name": "MONKEY_PATCH_KEY_NAME"
}, },
@ -320,21 +323,9 @@
{ {
"name": "_c18" "name": "_c18"
}, },
{
"name": "_c19"
},
{ {
"name": "_c2" "name": "_c2"
}, },
{
"name": "_c20"
},
{
"name": "_c21"
},
{
"name": "_c22"
},
{ {
"name": "_c3" "name": "_c3"
}, },
@ -371,6 +362,9 @@
{ {
"name": "_symbolIterator" "name": "_symbolIterator"
}, },
{
"name": "_updateSingleStylingValue"
},
{ {
"name": "addComponentLogic" "name": "addComponentLogic"
}, },
@ -389,6 +383,9 @@
{ {
"name": "allocStylingContext" "name": "allocStylingContext"
}, },
{
"name": "allowValueChange"
},
{ {
"name": "appendChild" "name": "appendChild"
}, },
@ -491,9 +488,6 @@
{ {
"name": "createRootContext" "name": "createRootContext"
}, },
{
"name": "createStylingContextTemplate"
},
{ {
"name": "createTNode" "name": "createTNode"
}, },
@ -530,9 +524,6 @@
{ {
"name": "defineInjectable" "name": "defineInjectable"
}, },
{
"name": "delegateToClassInput"
},
{ {
"name": "destroyLView" "name": "destroyLView"
}, },
@ -554,6 +545,9 @@
{ {
"name": "directiveInject" "name": "directiveInject"
}, },
{
"name": "directiveOwnerPointers"
},
{ {
"name": "domRendererFactory3" "name": "domRendererFactory3"
}, },
@ -614,6 +608,9 @@
{ {
"name": "findDirectiveMatches" "name": "findDirectiveMatches"
}, },
{
"name": "findOrPatchDirectiveIntoRegistry"
},
{ {
"name": "findViaComponent" "name": "findViaComponent"
}, },
@ -668,6 +665,15 @@
{ {
"name": "getDirectiveDef" "name": "getDirectiveDef"
}, },
{
"name": "getDirectiveIndexFromEntry"
},
{
"name": "getDirectiveIndexFromRegistry"
},
{
"name": "getDirectiveRegistryValuesIndexOf"
},
{ {
"name": "getElementDepthCount" "name": "getElementDepthCount"
}, },
@ -686,9 +692,15 @@
{ {
"name": "getHostTElementNode" "name": "getHostTElementNode"
}, },
{
"name": "getInitialClassNameValue"
},
{ {
"name": "getInitialIndex" "name": "getInitialIndex"
}, },
{
"name": "getInitialStylingValuesIndexOf"
},
{ {
"name": "getInitialValue" "name": "getInitialValue"
}, },
@ -710,6 +722,9 @@
{ {
"name": "getLViewChild" "name": "getLViewChild"
}, },
{
"name": "getMatchingBindingIndex"
},
{ {
"name": "getMultiOrSingleIndex" "name": "getMultiOrSingleIndex"
}, },
@ -791,6 +806,9 @@
{ {
"name": "getRootView" "name": "getRootView"
}, },
{
"name": "getSinglePropIndexValue"
},
{ {
"name": "getStyleSanitizer" "name": "getStyleSanitizer"
}, },
@ -816,19 +834,7 @@
"name": "getValue" "name": "getValue"
}, },
{ {
"name": "hackImplementationOfElementClassProp" "name": "hasClassInput"
},
{
"name": "hackImplementationOfElementStyling"
},
{
"name": "hackImplementationOfElementStylingApply"
},
{
"name": "hackSetStaticClasses"
},
{
"name": "hackSquashDeclaration"
}, },
{ {
"name": "hasParentInjector" "name": "hasParentInjector"
@ -836,6 +842,9 @@
{ {
"name": "hasPlayerBuilderChanged" "name": "hasPlayerBuilderChanged"
}, },
{
"name": "hasStyling"
},
{ {
"name": "hasTagAndTypeMatch" "name": "hasTagAndTypeMatch"
}, },
@ -851,6 +860,9 @@
{ {
"name": "initNodeFlags" "name": "initNodeFlags"
}, },
{
"name": "initializeStaticContext"
},
{ {
"name": "initializeTNodeInputs" "name": "initializeTNodeInputs"
}, },
@ -911,6 +923,9 @@
{ {
"name": "isDifferent" "name": "isDifferent"
}, },
{
"name": "isDirectiveDirty"
},
{ {
"name": "isDirty" "name": "isDirty"
}, },
@ -1083,7 +1098,13 @@
"name": "renderEmbeddedTemplate" "name": "renderEmbeddedTemplate"
}, },
{ {
"name": "renderStyleAndClassBindings" "name": "renderInitialStylesAndClasses"
},
{
"name": "renderInitialStylingValues"
},
{
"name": "renderStyling"
}, },
{ {
"name": "resetComponentState" "name": "resetComponentState"
@ -1127,6 +1148,9 @@
{ {
"name": "setCurrentDirectiveDef" "name": "setCurrentDirectiveDef"
}, },
{
"name": "setDirectiveDirty"
},
{ {
"name": "setDirty" "name": "setDirty"
}, },
@ -1166,6 +1190,9 @@
{ {
"name": "setProp" "name": "setProp"
}, },
{
"name": "setSanitizeFlag"
},
{ {
"name": "setStyle" "name": "setStyle"
}, },
@ -1215,7 +1242,7 @@
"name": "updateClassProp" "name": "updateClassProp"
}, },
{ {
"name": "updateStyleProp" "name": "updateContextWithBindings"
}, },
{ {
"name": "updateViewQuery" "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', () => { it('should return a map of local refs for an element with styling context', () => {
class Comp { class Comp {
static ngComponentDef = defineComponent({ static ngComponentDef = defineComponent({
type: Comp, type: Comp,
@ -554,7 +553,6 @@ describe('discovery utils deprecated', () => {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {
// <div #elRef class="fooClass"> // <div #elRef class="fooClass">
elementStart(0, 'div', null, ['elRef', '']); elementStart(0, 'div', null, ['elRef', '']);
elementStyling(['fooClass']);
elementEnd(); elementEnd();
} }
if (rf & RenderFlags.Update) { if (rf & RenderFlags.Update) {

View File

@ -8,7 +8,7 @@
import {AttributeMarker, defineComponent, defineDirective} from '../../src/render3/index'; 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 {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 {NgIf} from './common_with_def';
import {ComponentFixture, createComponent, renderToHtml} from './render_util'; import {ComponentFixture, createComponent, renderToHtml} from './render_util';
@ -206,8 +206,8 @@ describe('exports', () => {
/** <div [class.red]="myInput.checked"</div> <input type="checkbox" checked #myInput> */ /** <div [class.red]="myInput.checked"</div> <input type="checkbox" checked #myInput> */
const App = createComponent('app', function(rf: RenderFlags, ctx: any) { const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {
elementStart(0, 'div'); elementStart(0, 'div', [AttributeMarker.Classes, 'red']);
elementStyling([InitialStylingFlags.VALUES_MODE, 'red', true]); elementStyling(['red']);
elementEnd(); elementEnd();
element(1, 'input', ['type', 'checkbox', 'checked', 'true'], ['myInput', '']); element(1, 'input', ['type', 'checkbox', 'checked', 'true'], ['myInput', '']);
} }

View File

@ -6,12 +6,12 @@
* found in the LICENSE file at https://angular.io/license * 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 {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 {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 {pureFunction1, pureFunction2} from '../../src/render3/pure_function';
import {ComponentFixture, TemplateFixture, createComponent, createDirective} from './render_util'; import {ComponentFixture, TemplateFixture, createComponent, createDirective} from './render_util';
@ -1141,9 +1141,8 @@ describe('host bindings', () => {
vars: 0, vars: 0,
hostBindings: (rf: RenderFlags, ctx: StaticHostClass, elIndex: number) => { hostBindings: (rf: RenderFlags, ctx: StaticHostClass, elIndex: number) => {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {
elementStyling( elementHostAttrs(ctx, [AttributeMarker.Classes, 'mat-toolbar']);
['mat-toolbar', InitialStylingFlags.VALUES_MODE, 'mat-toolbar', true], null, null, elementStyling(['mat-toolbar'], null, null, ctx);
ctx);
} }
if (rf & RenderFlags.Update) { if (rf & RenderFlags.Update) {
elementStylingApply(0, ctx); elementStylingApply(0, ctx);
@ -1164,6 +1163,5 @@ describe('host bindings', () => {
const hostBindingEl = fixture.hostElement.querySelector('static-host-class') as HTMLElement; const hostBindingEl = fixture.hostElement.querySelector('static-host-class') as HTMLElement;
expect(hostBindingEl.className).toEqual('mat-toolbar'); expect(hostBindingEl.className).toEqual('mat-toolbar');
}); });
}); });
}); });

View File

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

View File

@ -7,13 +7,13 @@
*/ */
import {ElementRef, TemplateRef, ViewContainerRef} from '@angular/core'; import {ElementRef, TemplateRef, ViewContainerRef} from '@angular/core';
import {RendererType2} from '../../src/render/api'; import {RendererType2} from '../../src/render/api';
import {AttributeMarker, defineComponent, defineDirective, templateRefExtractor} from '../../src/render3/index'; 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 {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 {InitialStylingFlags, RenderFlags} from '../../src/render3/interfaces/definition'; import {RenderFlags} from '../../src/render3/interfaces/definition';
import {RElement, Renderer3, RendererFactory3, domRendererFactory3} from '../../src/render3/interfaces/renderer'; 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 {HEADER_OFFSET, CONTEXT} from '../../src/render3/interfaces/view';
import {enableBindings, disableBindings} from '../../src/render3/state'; import {enableBindings, disableBindings} from '../../src/render3/state';
import {sanitizeUrl} from '../../src/sanitization/sanitization'; import {sanitizeUrl} from '../../src/sanitization/sanitization';
@ -1412,7 +1412,6 @@ describe('render3 integration test', () => {
}); });
describe('elementStyle', () => { describe('elementStyle', () => {
it('should support binding to styles', () => { it('should support binding to styles', () => {
const App = createComponent('app', function(rf: RenderFlags, ctx: any) { const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {
@ -1472,8 +1471,7 @@ describe('render3 integration test', () => {
}); });
}); });
describe('elementClass', () => { describe('class-based styling', () => {
it('should support CSS class toggle', () => { it('should support CSS class toggle', () => {
/** <span [class.active]="class"></span> */ /** <span [class.active]="class"></span> */
const App = createComponent('app', function(rf: RenderFlags, ctx: any) { 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', () => { it('should work correctly with existing static classes', () => {
const App = createComponent('app', function(rf: RenderFlags, ctx: any) { const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {
elementStart(0, 'span'); elementStart(0, 'span', [AttributeMarker.Classes, 'existing']);
elementStyling( elementStyling(['existing', 'active']);
['existing', 'active', InitialStylingFlags.VALUES_MODE, 'existing', true]);
elementEnd(); elementEnd();
} }
if (rf & RenderFlags.Update) { if (rf & RenderFlags.Update) {
@ -1553,7 +1550,7 @@ describe('render3 integration test', () => {
const App = createComponent('app', function(rf: RenderFlags, ctx: any) { const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {
elementStart(0, 'my-comp'); elementStart(0, 'my-comp');
{ elementStyling(['active']); } elementStyling(['active']);
elementEnd(); elementEnd();
} }
if (rf & RenderFlags.Update) { if (rf & RenderFlags.Update) {
@ -1572,7 +1569,6 @@ describe('render3 integration test', () => {
expect(fixture.html).toEqual('<my-comp class="">Comp Content</my-comp>'); expect(fixture.html).toEqual('<my-comp class="">Comp Content</my-comp>');
}); });
it('should apply classes properly when nodes have LContainers', () => { it('should apply classes properly when nodes have LContainers', () => {
let structuralComp !: StructuralComp; let structuralComp !: StructuralComp;
@ -1656,17 +1652,17 @@ describe('render3 integration test', () => {
set klass(value: string) { this.classesVal = value; } 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) { const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {
elementStart(0, 'div', ['DirWithClass']); elementStart(
elementStyling([ 0, 'div',
InitialStylingFlags.VALUES_MODE, 'apple', true, 'orange', true, 'banana', true ['DirWithClass', AttributeMarker.Classes, 'apple', 'orange', 'banana']);
]); elementStyling();
elementEnd(); elementEnd();
} }
if (rf & RenderFlags.Update) { 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', 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) { const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {
@ -1698,6 +1694,223 @@ describe('render3 integration test', () => {
const fixture = new ComponentFixture(App); const fixture = new ComponentFixture(App);
expect(mockClassDirective !.classesVal).toEqual('cucumber grape'); 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; this.lastCapturedType = rendererType;
return domRendererFactory3.createRenderer(hostElement, 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 { export class MockPlayer implements Player {
parent: Player|null = null; parent: Player|null = null;
data: any;
log: string[] = []; log: string[] = [];
state: PlayState = PlayState.Pending; state: PlayState = PlayState.Pending;
private _listeners: {[state: string]: (() => any)[]} = {}; private _listeners: {[state: string]: (() => any)[]} = {};