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

PR Close #24602
This commit is contained in:
Matias Niemelä
2018-06-19 12:45:00 -07:00
committed by Miško Hevery
parent 52d43a99ef
commit 3980640d53
22 changed files with 1904 additions and 143 deletions

View File

@ -11,7 +11,7 @@ import {browserDetection} from '@angular/platform-browser/testing/src/browser_ut
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ContentChildren, Directive, HostBinding, HostListener, Injectable, Input, NgModule, OnDestroy, Optional, Pipe, PipeTransform, QueryList, SimpleChanges, TemplateRef, ViewChild, ViewChildren, ViewContainerRef} from '../../../src/core';
import * as $r3$ from '../../../src/core_render3_private_export';
import {AttributeMarker} from '../../../src/render3';
import {ComponentDefInternal} from '../../../src/render3/interfaces/definition';
import {ComponentDefInternal, InitialStylingFlags} from '../../../src/render3/interfaces/definition';
import {ComponentFixture, renderComponent, toHtml} from '../render_util';
@ -304,6 +304,7 @@ describe('elements', () => {
it('should bind to a specific style', () => {
type $MyComponent$ = MyComponent;
const c0 = ['color', 'width'];
@Component({
selector: 'my-component',
template: `<div [style.color]="someColor" [style.width.px]="someWidth"></div>`
@ -318,11 +319,14 @@ describe('elements', () => {
factory: function MyComponent_Factory() { return new MyComponent(); },
template: function MyComponent_Template(rf: $RenderFlags$, ctx: $MyComponent$) {
if (rf & 1) {
$r3$.ɵEe(0, 'div');
$r3$.ɵE(0, 'div');
$r3$.ɵs(1, c0);
$r3$.ɵe();
}
if (rf & 2) {
$r3$.ɵsn(0, 'color', $r3$.ɵb(ctx.someColor));
$r3$.ɵsn(0, 'width', $r3$.ɵb(ctx.someWidth), 'px');
$r3$.ɵsp(1, 0, ctx.someColor);
$r3$.ɵsp(1, 1, ctx.someWidth, 'px');
$r3$.ɵsa(1);
}
}
});
@ -349,10 +353,7 @@ describe('elements', () => {
it('should bind to many and keep order', () => {
type $MyComponent$ = MyComponent;
// NORMATIVE
const $e0_attrs$ = ['style', 'color: red;'];
// /NORMATIVE
const c0 = ['color', InitialStylingFlags.INITIAL_STYLES, 'color', 'red'];
@Component({
selector: 'my-component',
template:
@ -367,7 +368,9 @@ describe('elements', () => {
factory: function MyComponent_Factory() { return new MyComponent(); },
template: function MyComponent_Template(rf: $RenderFlags$, ctx: $MyComponent$) {
if (rf & 1) {
$r3$.ɵEe(0, 'div', $e0_attrs$);
$r3$.ɵE(0, 'div');
$r3$.ɵs(1, c0);
$r3$.ɵe();
}
if (rf & 2) {
$r3$.ɵp(0, 'id', $r3$.ɵb(ctx.someString + 1));
@ -402,11 +405,14 @@ describe('elements', () => {
factory: function StyleComponent_Factory() { return new StyleComponent(); },
template: function StyleComponent_Template(rf: $RenderFlags$, ctx: $StyleComponent$) {
if (rf & 1) {
$r3$.ɵEe(0, 'div');
$r3$.ɵE(0, 'div');
$r3$.ɵs(1);
$r3$.ɵe();
}
if (rf & 2) {
$r3$.ɵk(0, $r3$.ɵb(ctx.classExp));
$r3$.ɵs(0, $r3$.ɵb(ctx.styleExp));
$r3$.ɵsm(1, ctx.styleExp);
$r3$.ɵsa(1);
}
}
});

View File

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

View File

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

View File

@ -9,7 +9,7 @@
import {RenderFlags} from '@angular/core/src/render3';
import {defineComponent, defineDirective} from '../../src/render3/index';
import {NO_CHANGE, bind, container, containerRefreshEnd, containerRefreshStart, elementAttribute, elementClassNamed, elementEnd, elementProperty, elementStart, elementStyleNamed, embeddedViewEnd, embeddedViewStart, interpolation1, interpolation2, interpolation3, interpolation4, interpolation5, interpolation6, interpolation7, interpolation8, interpolationV, load, loadDirective, projection, projectionDef, text, textBinding,} from '../../src/render3/instructions';
import {NO_CHANGE, bind, container, containerRefreshEnd, containerRefreshStart, elementAttribute, elementClassNamed, elementEnd, elementProperty, elementStart, elementStyleProp, elementStyling, elementStylingApply, embeddedViewEnd, embeddedViewStart, interpolation1, interpolation2, interpolation3, interpolation4, interpolation5, interpolation6, interpolation7, interpolation8, interpolationV, load, loadDirective, projection, projectionDef, text, textBinding} from '../../src/render3/instructions';
import {HEADER_OFFSET} from '../../src/render3/interfaces/view';
import {sanitizeUrl} from '../../src/sanitization/sanitization';
import {Sanitizer, SecurityContext} from '../../src/sanitization/security';
@ -747,10 +747,12 @@ describe('render3 integration test', () => {
function Template(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
elementStart(0, 'span');
elementStyling(1, ['border-color']);
elementEnd();
}
if (rf & RenderFlags.Update) {
elementStyleNamed(0, 'border-color', bind(ctx));
elementStyleProp(1, 0, ctx);
elementStylingApply(1);
}
}
@ -764,10 +766,12 @@ describe('render3 integration test', () => {
function Template(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
elementStart(0, 'span');
elementStyling(1, ['font-size']);
elementEnd();
}
if (rf & RenderFlags.Update) {
elementStyleNamed(0, 'font-size', bind(ctx), 'px');
elementStyleProp(1, 0, ctx, 'px');
elementStylingApply(1);
}
}

View File

@ -25,7 +25,8 @@ function testLStaticData(tagName: string, attrs: TAttributes | null): TNode {
child: null,
parent: null,
dynamicContainerNode: null,
detached: null
detached: null,
stylingTemplate: null
};
}

View File

@ -0,0 +1,657 @@
/**
* @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 {elementEnd, elementStart, elementStyle, elementStyleProp, elementStyling, elementStylingApply} from '../../src/render3/instructions';
import {InitialStylingFlags, RenderFlags} from '../../src/render3/interfaces/definition';
import {LElementNode} from '../../src/render3/interfaces/node';
import {Renderer3} from '../../src/render3/interfaces/renderer';
import {StylingContext, StylingFlags, StylingIndex, allocStylingContext, createStylingContextTemplate, isContextDirty, renderStyles as _renderStyles, setContextDirty, updateStyleMap, updateStyleProp} from '../../src/render3/styling';
import {renderToHtml} from './render_util';
describe('styling', () => {
let lElement: LElementNode|null = null;
beforeEach(() => { lElement = { native: {} } as any; });
function initContext(styles?: (number | string)[]): StylingContext {
return allocStylingContext(createStylingContextTemplate(styles));
}
function renderStyles(context: StylingContext, renderer?: Renderer3) {
const styles: {[key: string]: any} = {};
_renderStyles(lElement !, context, (renderer || {}) as Renderer3, styles);
return styles;
}
function trackStylesFactory() {
const styles: {[key: string]: any} = {};
return function(context: StylingContext, renderer?: Renderer3): {[key: string]: any} {
_renderStyles(lElement !, context, (renderer || {}) as Renderer3, styles);
return styles;
};
}
function clean(a: number = 0, b: number = 0): number {
let num = 0;
if (a) {
num |= a << StylingFlags.BitCountSize;
}
if (b) {
num |= b << (StylingFlags.BitCountSize + StylingIndex.BitCountSize);
}
return num;
}
function dirty(a: number = 0, b: number = 0): number { return clean(a, b) | StylingFlags.Dirty; }
describe('createStylingContextTemplate', () => {
it('should initialize empty template', () => {
const template = createStylingContextTemplate();
expect(template).toEqual([
[null],
clean(0, 2),
]);
});
it('should initialize static styles', () => {
debugger;
const template = createStylingContextTemplate(
[InitialStylingFlags.INITIAL_STYLES, 'color', 'red', 'width', '10px']);
expect(template).toEqual([
[null, 'red', '10px'],
dirty(0, 8), //
// #2
clean(1, 8),
'color',
null,
// #5
clean(2, 11),
'width',
null,
// #8
dirty(1, 2),
'color',
null,
// #11
dirty(2, 5),
'width',
null,
]);
});
});
describe('instructions', () => {
it('should handle a combination of initial, multi and singular style values (in that order)',
() => {
function Template(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
elementStart(0, 'span');
elementStyling(1, [
'width', 'height', 'opacity', //
0, 'width', '100px', 'height', '100px', 'opacity', '0.5'
]);
elementEnd();
}
if (rf & RenderFlags.Update) {
elementStyle(1, ctx.myStyles);
elementStyleProp(1, 0, ctx.myWidth);
elementStylingApply(1);
}
}
expect(renderToHtml(Template, {
myStyles: {width: '200px', height: '200px'},
myWidth: '300px'
})).toEqual('<span style="width: 300px; height: 200px; opacity: 0.5;"></span>');
expect(renderToHtml(Template, {myStyles: {width: '200px', height: null}, myWidth: null}))
.toEqual('<span style="width: 200px; height: 100px; opacity: 0.5;"></span>');
});
});
describe('helper functions', () => {
it('should build a list of multiple styling values', () => {
const getStyles = trackStylesFactory();
const stylingContext = initContext();
updateStyleMap(stylingContext, {
width: '100px',
height: '100px',
});
updateStyleMap(stylingContext, {height: '200px'});
expect(getStyles(stylingContext)).toEqual({width: null, height: '200px'});
});
it('should evaluate the delta between style changes when rendering occurs', () => {
const stylingContext = initContext(['width', 'height', 0, 'width', '100px']);
updateStyleMap(stylingContext, {
height: '200px',
});
expect(renderStyles(stylingContext)).toEqual({width: '100px', height: '200px'});
expect(renderStyles(stylingContext)).toEqual({});
updateStyleMap(stylingContext, {
width: '100px',
height: '100px',
});
expect(renderStyles(stylingContext)).toEqual({height: '100px'});
updateStyleProp(stylingContext, 1, '100px');
expect(renderStyles(stylingContext)).toEqual({});
updateStyleMap(stylingContext, {
width: '100px',
height: '100px',
});
expect(renderStyles(stylingContext)).toEqual({});
});
it('should update individual values on a set of styles', () => {
const getStyles = trackStylesFactory();
const stylingContext = initContext(['width', 'height']);
updateStyleMap(stylingContext, {
width: '100px',
height: '100px',
});
updateStyleProp(stylingContext, 1, '200px');
expect(getStyles(stylingContext)).toEqual({width: '100px', height: '200px'});
});
it('should only mark itself as updated when one or more properties have been applied', () => {
const stylingContext = initContext();
expect(isContextDirty(stylingContext)).toBeFalsy();
updateStyleMap(stylingContext, {
width: '100px',
height: '100px',
});
expect(isContextDirty(stylingContext)).toBeTruthy();
setContextDirty(stylingContext, false);
updateStyleMap(stylingContext, {
width: '100px',
height: '100px',
});
expect(isContextDirty(stylingContext)).toBeFalsy();
updateStyleMap(stylingContext, {
width: '200px',
height: '100px',
});
expect(isContextDirty(stylingContext)).toBeTruthy();
});
it('should only mark itself as updated when any single properties have been applied', () => {
const stylingContext = initContext(['height']);
updateStyleMap(stylingContext, {
width: '100px',
height: '100px',
});
setContextDirty(stylingContext, false);
updateStyleProp(stylingContext, 0, '100px');
expect(isContextDirty(stylingContext)).toBeFalsy();
setContextDirty(stylingContext, false);
updateStyleProp(stylingContext, 0, '200px');
expect(isContextDirty(stylingContext)).toBeTruthy();
});
it('should prioritize multi and single styles over initial styles', () => {
const getStyles = trackStylesFactory();
const stylingContext = initContext(
['width', 'height', 'opacity', 0, 'width', '100px', 'height', '100px', 'opacity', '0']);
expect(getStyles(stylingContext)).toEqual({
width: '100px',
height: '100px',
opacity: '0',
});
updateStyleMap(stylingContext, {width: '200px', height: '200px'});
expect(getStyles(stylingContext)).toEqual({
width: '200px',
height: '200px',
opacity: '0',
});
updateStyleProp(stylingContext, 0, '300px');
expect(getStyles(stylingContext)).toEqual({
width: '300px',
height: '200px',
opacity: '0',
});
updateStyleProp(stylingContext, 0, null);
expect(getStyles(stylingContext)).toEqual({
width: '200px',
height: '200px',
opacity: '0',
});
updateStyleMap(stylingContext, {});
expect(getStyles(stylingContext)).toEqual({
width: '100px',
height: '100px',
opacity: '0',
});
});
it('should cleanup removed styles from the context once the styles are built', () => {
const stylingContext = initContext(['width', 'height']);
const getStyles = trackStylesFactory();
updateStyleMap(stylingContext, {width: '100px', height: '100px'});
expect(stylingContext).toEqual([
[null],
dirty(0, 8), //
// #2
clean(0, 8),
'width',
null,
// #5
clean(0, 11),
'height',
null,
// #8
dirty(0, 2),
'width',
'100px',
// #11
dirty(0, 5),
'height',
'100px',
]);
getStyles(stylingContext);
updateStyleMap(stylingContext, {width: '200px', opacity: '0'});
expect(stylingContext).toEqual([
[null],
dirty(0, 8), //
// #2
clean(0, 8),
'width',
null,
// #5
clean(0, 14),
'height',
null,
// #8
dirty(0, 2),
'width',
'200px',
// #11
dirty(),
'opacity',
'0',
// #14
dirty(0, 5),
'height',
null,
]);
getStyles(stylingContext);
expect(stylingContext).toEqual([
[null],
clean(0, 8), //
// #2
clean(0, 8),
'width',
null,
// #5
clean(0, 14),
'height',
null,
// #8
clean(0, 2),
'width',
'200px',
// #11
clean(),
'opacity',
'0',
// #14
clean(0, 5),
'height',
null,
]);
updateStyleMap(stylingContext, {width: null});
updateStyleProp(stylingContext, 0, '300px');
expect(stylingContext).toEqual([
[null],
dirty(0, 8), //
// #2
dirty(0, 8),
'width',
'300px',
// #5
clean(0, 14),
'height',
null,
// #8
clean(0, 2),
'width',
null,
// #11
dirty(),
'opacity',
null,
// #14
clean(0, 5),
'height',
null,
]);
getStyles(stylingContext);
updateStyleProp(stylingContext, 0, null);
expect(stylingContext).toEqual([
[null],
dirty(0, 8), //
// #2
dirty(0, 8),
'width',
null,
// #5
clean(0, 14),
'height',
null,
// #8
clean(0, 2),
'width',
null,
// #11
clean(),
'opacity',
null,
// #14
clean(0, 5),
'height',
null,
]);
});
it('should find the next available space in the context when data is added after being removed before',
() => {
const stylingContext = initContext(['lineHeight']);
const getStyles = trackStylesFactory();
updateStyleMap(stylingContext, {width: '100px', height: '100px', opacity: '0.5'});
expect(stylingContext).toEqual([
[null],
dirty(0, 5), //
// #2
clean(0, 14),
'lineHeight',
null,
// #5
dirty(),
'width',
'100px',
// #8
dirty(),
'height',
'100px',
// #11
dirty(),
'opacity',
'0.5',
// #14
dirty(0, 2),
'lineHeight',
null,
]);
getStyles(stylingContext);
updateStyleMap(stylingContext, {});
expect(stylingContext).toEqual([
[null],
dirty(0, 5), //
// #2
clean(0, 14),
'lineHeight',
null,
// #5
dirty(),
'width',
null,
// #8
dirty(),
'height',
null,
// #11
dirty(),
'opacity',
null,
// #14
clean(0, 2),
'lineHeight',
null,
]);
getStyles(stylingContext);
updateStyleMap(stylingContext, {
borderWidth: '5px',
});
expect(stylingContext).toEqual([
[null],
dirty(0, 5), //
// #2
clean(0, 17),
'lineHeight',
null,
// #5
dirty(),
'borderWidth',
'5px',
// #8
clean(),
'width',
null,
// #11
clean(),
'height',
null,
// #14
clean(),
'opacity',
null,
// #17
clean(0, 2),
'lineHeight',
null,
]);
updateStyleProp(stylingContext, 0, '200px');
expect(stylingContext).toEqual([
[null],
dirty(0, 5), //
// #2
dirty(0, 17),
'lineHeight',
'200px',
// #5
dirty(),
'borderWidth',
'5px',
// #8
clean(),
'width',
null,
// #11
clean(),
'height',
null,
// #14
clean(),
'opacity',
null,
// #17
clean(0, 2),
'lineHeight',
null,
]);
updateStyleMap(stylingContext, {borderWidth: '15px', borderColor: 'red'});
expect(stylingContext).toEqual([
[null],
dirty(0, 5), //
// #2
dirty(0, 20),
'lineHeight',
'200px',
// #5
dirty(),
'borderWidth',
'15px',
// #8
dirty(),
'borderColor',
'red',
// #11
clean(),
'width',
null,
// #14
clean(),
'height',
null,
// #17
clean(),
'opacity',
null,
// #20
clean(0, 2),
'lineHeight',
null,
]);
});
it('should render all data as not being dirty after the styles are built', () => {
const getStyles = trackStylesFactory();
const stylingContext = initContext(['height']);
updateStyleMap(stylingContext, {
width: '100px',
});
updateStyleProp(stylingContext, 0, '200px');
expect(stylingContext).toEqual([
[null],
dirty(0, 5), //
// #2
dirty(0, 8),
'height',
'200px',
// #2
dirty(),
'width',
'100px',
// #8
clean(0, 2),
'height',
null,
]);
getStyles(stylingContext);
expect(stylingContext).toEqual([
[null],
clean(0, 5), //
// #2
clean(0, 8),
'height',
'200px',
// #2
clean(),
'width',
'100px',
// #8
clean(0, 2),
'height',
null,
]);
});
});
});