/** * @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, elementStyleProp, elementStyling, elementStylingApply, elementStylingMap} 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, renderStyling as _renderStyling, setContextDirty, updateClassProp, updateStyleProp, updateStylingMap} from '../../src/render3/styling'; import {defaultStyleSanitizer} from '../../src/sanitization/sanitization'; import {StyleSanitizeFn} from '../../src/sanitization/style_sanitizer'; import {renderToHtml} from './render_util'; describe('styling', () => { let element: LElementNode|null = null; beforeEach(() => { element = {} as any; }); function initContext( styles?: (number | string)[] | null, classes?: (string | number | boolean)[] | null, sanitizer?: StyleSanitizeFn | null): StylingContext { return allocStylingContext(element, createStylingContextTemplate(classes, styles, sanitizer)); } function renderStyles(context: StylingContext, renderer?: Renderer3) { const styles: {[key: string]: any} = {}; _renderStyling(context, (renderer || {}) as Renderer3, styles); return styles; } function trackStylesFactory() { const styles: {[key: string]: any} = {}; return function(context: StylingContext, renderer?: Renderer3): {[key: string]: any} { _renderStyling(context, (renderer || {}) as Renderer3, styles); return styles; }; } function trackClassesFactory() { const classes: {[className: string]: boolean} = {}; return function(context: StylingContext, renderer?: Renderer3): {[key: string]: any} { _renderStyling(context, (renderer || {}) as Renderer3, {}, classes); return classes; }; } function trackStylesAndClasses() { const classes: {[className: string]: boolean} = {}; const styles: {[prop: string]: any} = {}; return function(context: StylingContext, renderer?: Renderer3): {[key: string]: any} { _renderStyling(context, (renderer || {}) as Renderer3, styles, classes); return [styles, classes]; }; } function updateClasses(context: StylingContext, classes: string | {[key: string]: any} | null) { updateStylingMap(context, classes, null); } function updateStyles(context: StylingContext, styles: {[key: string]: any} | null) { updateStylingMap(context, null, styles); } function cleanStyle(a: number = 0, b: number = 0): number { return _clean(a, b, false, false); } function cleanStyleWithSanitization(a: number = 0, b: number = 0): number { return _clean(a, b, false, true); } function cleanClass(a: number, b: number) { return _clean(a, b, true); } function _clean( a: number = 0, b: number = 0, isClassBased: boolean, sanitizable?: boolean): number { let num = 0; if (a) { num |= a << StylingFlags.BitCountSize; } if (b) { num |= b << (StylingFlags.BitCountSize + StylingIndex.BitCountSize); } if (isClassBased) { num |= StylingFlags.Class; } if (sanitizable) { num |= StylingFlags.Sanitize; } return num; } function _dirty( a: number = 0, b: number = 0, isClassBased: boolean, sanitizable?: boolean): number { return _clean(a, b, isClassBased, sanitizable) | StylingFlags.Dirty; } function dirtyStyle(a: number = 0, b: number = 0): number { return _dirty(a, b, false) | StylingFlags.Dirty; } function dirtyStyleWithSanitization(a: number = 0, b: number = 0): number { return _dirty(a, b, false, true); } function dirtyClass(a: number, b: number) { return _dirty(a, b, true); } describe('styles', () => { describe('createStylingContextTemplate', () => { it('should initialize empty template', () => { const template = initContext(); expect(template).toEqual([element, null, null, [null], cleanStyle(0, 7), 0, null]); }); it('should initialize static styles', () => { const template = initContext([InitialStylingFlags.VALUES_MODE, 'color', 'red', 'width', '10px']); expect(template).toEqual([ element, null, null, [null, 'red', '10px'], dirtyStyle(0, 13), // 0, null, // #7 cleanStyle(1, 13), 'color', null, // #10 cleanStyle(2, 16), 'width', null, // #13 dirtyStyle(1, 7), 'color', null, // #16 dirtyStyle(2, 10), '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([], [ 'width', 'height', 'opacity', // InitialStylingFlags.VALUES_MODE, 'width', '100px', 'height', '100px', 'opacity', '0.5' ]); elementEnd(); } if (rf & RenderFlags.Update) { elementStylingMap(0, ctx.myStyles); elementStyleProp(0, 0, ctx.myWidth); elementStylingApply(0); } } expect(renderToHtml( Template, {myStyles: {width: '200px', height: '200px'}, myWidth: '300px'}, 1)) .toEqual(''); expect( renderToHtml(Template, {myStyles: {width: '200px', height: null}, myWidth: null}, 1)) .toEqual(''); }); }); describe('helper functions', () => { it('should build a list of multiple styling values', () => { const getStyles = trackStylesFactory(); const stylingContext = initContext(); updateStyles(stylingContext, { width: '100px', height: '100px', }); updateStyles(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', InitialStylingFlags.VALUES_MODE, 'width', '100px']); updateStyles(stylingContext, { height: '200px', }); expect(renderStyles(stylingContext)).toEqual({width: '100px', height: '200px'}); expect(renderStyles(stylingContext)).toEqual({}); updateStyles(stylingContext, { width: '100px', height: '100px', }); expect(renderStyles(stylingContext)).toEqual({height: '100px'}); updateStyleProp(stylingContext, 1, '100px'); expect(renderStyles(stylingContext)).toEqual({}); updateStyles(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']); updateStyles(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(); updateStyles(stylingContext, { width: '100px', height: '100px', }); expect(isContextDirty(stylingContext)).toBeTruthy(); setContextDirty(stylingContext, false); updateStyles(stylingContext, { width: '100px', height: '100px', }); expect(isContextDirty(stylingContext)).toBeFalsy(); updateStyles(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']); updateStyles(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', InitialStylingFlags.VALUES_MODE, 'width', '100px', 'height', '100px', 'opacity', '0' ]); expect(getStyles(stylingContext)).toEqual({ width: '100px', height: '100px', opacity: '0', }); updateStyles(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', }); updateStyles(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(); updateStyles(stylingContext, {width: '100px', height: '100px'}); expect(stylingContext).toEqual([ element, null, null, [null], dirtyStyle(0, 13), // 2, null, // #7 cleanStyle(0, 13), 'width', null, // #10 cleanStyle(0, 16), 'height', null, // #13 dirtyStyle(0, 7), 'width', '100px', // #16 dirtyStyle(0, 10), 'height', '100px', ]); getStyles(stylingContext); updateStyles(stylingContext, {width: '200px', opacity: '0'}); expect(stylingContext).toEqual([ element, null, null, [null], dirtyStyle(0, 13), // 2, null, // #7 cleanStyle(0, 13), 'width', null, // #10 cleanStyle(0, 19), 'height', null, // #13 dirtyStyle(0, 7), 'width', '200px', // #16 dirtyStyle(), 'opacity', '0', // #19 dirtyStyle(0, 10), 'height', null, ]); getStyles(stylingContext); expect(stylingContext).toEqual([ element, null, null, [null], cleanStyle(0, 13), // 2, null, // #7 cleanStyle(0, 13), 'width', null, // #10 cleanStyle(0, 19), 'height', null, // #13 cleanStyle(0, 7), 'width', '200px', // #16 cleanStyle(), 'opacity', '0', // #19 cleanStyle(0, 10), 'height', null, ]); updateStyles(stylingContext, {width: null}); updateStyleProp(stylingContext, 0, '300px'); expect(stylingContext).toEqual([ element, null, null, [null], dirtyStyle(0, 13), // 2, null, // #7 dirtyStyle(0, 13), 'width', '300px', // #10 cleanStyle(0, 19), 'height', null, // #13 cleanStyle(0, 7), 'width', null, // #16 dirtyStyle(), 'opacity', null, // #19 cleanStyle(0, 10), 'height', null, ]); getStyles(stylingContext); updateStyleProp(stylingContext, 0, null); expect(stylingContext).toEqual([ element, null, null, [null], dirtyStyle(0, 13), // 2, null, // #7 dirtyStyle(0, 13), 'width', null, // #10 cleanStyle(0, 19), 'height', null, // #13 cleanStyle(0, 7), 'width', null, // #16 cleanStyle(), 'opacity', null, // #19 cleanStyle(0, 10), '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(); updateStyles(stylingContext, {width: '100px', height: '100px', opacity: '0.5'}); expect(stylingContext).toEqual([ element, null, null, [null], dirtyStyle(0, 10), // 1, null, // #7 cleanStyle(0, 19), 'lineHeight', null, // #10 dirtyStyle(), 'width', '100px', // #13 dirtyStyle(), 'height', '100px', // #16 dirtyStyle(), 'opacity', '0.5', // #19 cleanStyle(0, 7), 'lineHeight', null, ]); getStyles(stylingContext); updateStyles(stylingContext, {}); expect(stylingContext).toEqual([ element, null, null, [null], dirtyStyle(0, 10), // 1, null, // #7 cleanStyle(0, 19), 'lineHeight', null, // #10 dirtyStyle(), 'width', null, // #13 dirtyStyle(), 'height', null, // #16 dirtyStyle(), 'opacity', null, // #19 cleanStyle(0, 7), 'lineHeight', null, ]); getStyles(stylingContext); updateStyles(stylingContext, { borderWidth: '5px', }); expect(stylingContext).toEqual([ element, null, null, [null], dirtyStyle(0, 10), // 1, null, // #7 cleanStyle(0, 22), 'lineHeight', null, // #10 dirtyStyle(), 'borderWidth', '5px', // #13 cleanStyle(), 'width', null, // #16 cleanStyle(), 'height', null, // #19 cleanStyle(), 'opacity', null, // #22 cleanStyle(0, 7), 'lineHeight', null, ]); updateStyleProp(stylingContext, 0, '200px'); expect(stylingContext).toEqual([ element, null, null, [null], dirtyStyle(0, 10), // 1, null, // #7 dirtyStyle(0, 22), 'lineHeight', '200px', // #10 dirtyStyle(), 'borderWidth', '5px', // #13 cleanStyle(), 'width', null, // #16 cleanStyle(), 'height', null, // #19 cleanStyle(), 'opacity', null, // #22 cleanStyle(0, 7), 'lineHeight', null, ]); updateStyles(stylingContext, {borderWidth: '15px', borderColor: 'red'}); expect(stylingContext).toEqual([ element, null, null, [null], dirtyStyle(0, 10), // 1, null, // #7 dirtyStyle(0, 25), 'lineHeight', '200px', // #10 dirtyStyle(), 'borderWidth', '15px', // #13 dirtyStyle(), 'borderColor', 'red', // #16 cleanStyle(), 'width', null, // #19 cleanStyle(), 'height', null, // #22 cleanStyle(), 'opacity', null, // #25 cleanStyle(0, 7), 'lineHeight', null, ]); }); it('should render all data as not being dirty after the styles are built', () => { const getStyles = trackStylesFactory(); const stylingContext = initContext(['height']); updateStyles(stylingContext, { width: '100px', }); updateStyleProp(stylingContext, 0, '200px'); expect(stylingContext).toEqual([ element, null, null, [null], dirtyStyle(0, 10), // 1, null, // #7 dirtyStyle(0, 13), 'height', '200px', // #7 dirtyStyle(), 'width', '100px', // #13 cleanStyle(0, 7), 'height', null, ]); getStyles(stylingContext); expect(stylingContext).toEqual([ element, null, null, [null], cleanStyle(0, 10), // 1, null, // #7 cleanStyle(0, 13), 'height', '200px', // #7 cleanStyle(), 'width', '100px', // #13 cleanStyle(0, 7), 'height', null, ]); }); it('should mark styles that may contain url values as being sanitizable (when a sanitizer is passed in)', () => { const getStyles = trackStylesFactory(); const initialStyles = ['border-image', 'border-width']; const styleSanitizer = defaultStyleSanitizer; const stylingContext = initContext(initialStyles, null, styleSanitizer); updateStyleProp(stylingContext, 0, 'url(foo.jpg)'); updateStyleProp(stylingContext, 1, '100px'); expect(stylingContext).toEqual([ element, null, styleSanitizer, [null], dirtyStyle(0, 13), // 2, null, // #7 dirtyStyleWithSanitization(0, 13), 'border-image', 'url(foo.jpg)', // #10 dirtyStyle(0, 16), 'border-width', '100px', // #13 cleanStyleWithSanitization(0, 7), 'border-image', null, // #16 cleanStyle(0, 10), 'border-width', null, ]); updateStyles(stylingContext, {'background-image': 'unsafe'}); expect(stylingContext).toEqual([ element, null, styleSanitizer, [null], dirtyStyle(0, 13), // 2, null, // #7 dirtyStyleWithSanitization(0, 16), 'border-image', 'url(foo.jpg)', // #10 dirtyStyle(0, 19), 'border-width', '100px', // #13 dirtyStyleWithSanitization(0, 0), 'background-image', 'unsafe', // #16 cleanStyleWithSanitization(0, 7), 'border-image', null, // #19 cleanStyle(0, 10), 'border-width', null, ]); getStyles(stylingContext); expect(stylingContext).toEqual([ element, null, styleSanitizer, [null], cleanStyle(0, 13), // 2, null, // #7 cleanStyleWithSanitization(0, 16), 'border-image', 'url(foo.jpg)', // #10 cleanStyle(0, 19), 'border-width', '100px', // #13 cleanStyleWithSanitization(0, 0), 'background-image', 'unsafe', // #16 cleanStyleWithSanitization(0, 7), 'border-image', null, // #19 cleanStyle(0, 10), 'border-width', null, ]); }); }); }); describe('classes', () => { it('should initialize with the provided classes', () => { const template = initContext(null, [InitialStylingFlags.VALUES_MODE, 'one', true, 'two', true]); expect(template).toEqual([ element, null, null, [null, true, true], dirtyStyle(0, 13), // 0, null, // #7 cleanClass(1, 13), 'one', null, // #10 cleanClass(2, 16), 'two', null, // #13 dirtyClass(1, 7), 'one', null, // #16 dirtyClass(2, 10), 'two', null ]); }); it('should update multi class properties against the static classes', () => { const getClasses = trackClassesFactory(); const stylingContext = initContext(null, ['bar']); expect(getClasses(stylingContext)).toEqual({}); updateClasses(stylingContext, {foo: true, bar: false}); expect(getClasses(stylingContext)).toEqual({'foo': true, 'bar': false}); updateClasses(stylingContext, 'bar'); expect(getClasses(stylingContext)).toEqual({'foo': false, 'bar': true}); }); it('should update single class properties against the static classes', () => { const getClasses = trackClassesFactory(); const stylingContext = initContext(null, ['bar', 'foo', InitialStylingFlags.VALUES_MODE, 'bar', true]); expect(getClasses(stylingContext)).toEqual({'bar': true}); updateClassProp(stylingContext, 0, true); updateClassProp(stylingContext, 1, true); expect(getClasses(stylingContext)).toEqual({'bar': true, 'foo': true}); updateClassProp(stylingContext, 0, false); updateClassProp(stylingContext, 1, false); expect(getClasses(stylingContext)).toEqual({'bar': true, 'foo': false}); }); it('should understand updating multi-classes using a string-based value while respecting single class-based props', () => { const getClasses = trackClassesFactory(); const stylingContext = initContext(null, ['guy']); expect(getClasses(stylingContext)).toEqual({}); updateStylingMap(stylingContext, 'foo bar guy'); expect(getClasses(stylingContext)).toEqual({'foo': true, 'bar': true, 'guy': true}); updateStylingMap(stylingContext, 'foo man'); updateClassProp(stylingContext, 0, true); expect(getClasses(stylingContext)) .toEqual({'foo': true, 'man': true, 'bar': false, 'guy': true}); }); it('should house itself inside the context alongside styling in harmony', () => { const getStylesAndClasses = trackStylesAndClasses(); const initialStyles = ['width', 'height', InitialStylingFlags.VALUES_MODE, 'width', '100px']; const initialClasses = ['wide', 'tall', InitialStylingFlags.VALUES_MODE, 'wide', true]; const stylingContext = initContext(initialStyles, initialClasses); expect(stylingContext).toEqual([ element, null, null, [null, '100px', true], dirtyStyle(0, 19), // 2, null, // #7 cleanStyle(1, 19), 'width', null, // #10 cleanStyle(0, 22), 'height', null, // #13 cleanClass(2, 25), 'wide', null, // #16 cleanClass(0, 28), 'tall', null, // #19 dirtyStyle(1, 7), 'width', null, // #22 cleanStyle(0, 10), 'height', null, // #25 dirtyClass(2, 13), 'wide', null, // #28 cleanClass(0, 16), 'tall', null, ]); expect(getStylesAndClasses(stylingContext)).toEqual([{width: '100px'}, {wide: true}]); updateStylingMap(stylingContext, 'tall round', {width: '200px', opacity: '0.5'}); expect(stylingContext).toEqual([ element, null, null, [null, '100px', true], dirtyStyle(0, 19), // 2, 'tall round', // #7 cleanStyle(1, 19), 'width', null, // #10 cleanStyle(0, 34), 'height', null, // #13 cleanClass(2, 31), 'wide', null, // #16 cleanClass(0, 25), 'tall', null, // #19 dirtyStyle(1, 7), 'width', '200px', // #22 dirtyStyle(0, 0), 'opacity', '0.5', // #25 dirtyClass(0, 16), 'tall', true, // #28 dirtyClass(0, 0), 'round', true, // #31 cleanClass(2, 13), 'wide', null, // #34 cleanStyle(0, 10), 'height', null, ]); expect(getStylesAndClasses(stylingContext)).toEqual([ {width: '200px', opacity: '0.5'}, {tall: true, round: true, wide: true} ]); updateStylingMap(stylingContext, {tall: true, wide: true}, {width: '500px'}); updateStyleProp(stylingContext, 0, '300px'); expect(stylingContext).toEqual([ element, null, null, [null, '100px', true], dirtyStyle(0, 19), // 2, null, // #7 dirtyStyle(1, 19), 'width', '300px', // #10 cleanStyle(0, 34), 'height', null, // #13 cleanClass(2, 25), 'wide', null, // #16 cleanClass(0, 22), 'tall', null, // #19 cleanStyle(1, 7), 'width', '500px', // #22 cleanClass(0, 16), 'tall', true, // #25 cleanClass(2, 13), 'wide', true, // #28 dirtyClass(0, 0), 'round', null, // #31 dirtyStyle(0, 0), 'opacity', null, // #34 cleanStyle(0, 10), 'height', null, ]); expect(getStylesAndClasses(stylingContext)).toEqual([ {width: '300px', opacity: null}, {tall: true, round: false, wide: true} ]); }); }); });