/** * @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 {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): StylingContext { return allocStylingContext(element, createStylingContextTemplate(styles, classes)); } 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, null, classes); } function cleanStyle(a: number = 0, b: number = 0): number { return _clean(a, b, false); } function cleanClass(a: number, b: number) { return _clean(a, b, true); } function _clean(a: number = 0, b: number = 0, isClassBased: boolean): number { let num = 0; if (a) { num |= a << StylingFlags.BitCountSize; } if (b) { num |= b << (StylingFlags.BitCountSize + StylingIndex.BitCountSize); } if (isClassBased) { num |= StylingFlags.Class; } return num; } function _dirty(a: number = 0, b: number = 0, isClassBased: boolean): number { return _clean(a, b, isClassBased) | StylingFlags.Dirty; } function dirtyStyle(a: number = 0, b: number = 0): number { return _dirty(a, b, false) | StylingFlags.Dirty; } 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], cleanStyle(0, 5), 0, null]); }); it('should initialize static styles', () => { const template = initContext([InitialStylingFlags.VALUES_MODE, 'color', 'red', 'width', '10px']); expect(template).toEqual([ element, [null, 'red', '10px'], dirtyStyle(0, 11), // 0, null, // #5 cleanStyle(1, 11), 'color', null, // #8 cleanStyle(2, 14), 'width', null, // #11 dirtyStyle(1, 5), 'color', null, // #14 dirtyStyle(2, 8), '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' })).toEqual(''); expect(renderToHtml(Template, {myStyles: {width: '200px', height: null}, myWidth: null})) .toEqual(''); }); }); describe('helper functions', () => { it('should build a list of multiple styling values', () => { const getStyles = trackStylesFactory(); const stylingContext = initContext(); updateStylingMap(stylingContext, { width: '100px', height: '100px', }); updateStylingMap(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']); updateStylingMap(stylingContext, { height: '200px', }); expect(renderStyles(stylingContext)).toEqual({width: '100px', height: '200px'}); expect(renderStyles(stylingContext)).toEqual({}); updateStylingMap(stylingContext, { width: '100px', height: '100px', }); expect(renderStyles(stylingContext)).toEqual({height: '100px'}); updateStyleProp(stylingContext, 1, '100px'); expect(renderStyles(stylingContext)).toEqual({}); updateStylingMap(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']); updateStylingMap(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(); updateStylingMap(stylingContext, { width: '100px', height: '100px', }); expect(isContextDirty(stylingContext)).toBeTruthy(); setContextDirty(stylingContext, false); updateStylingMap(stylingContext, { width: '100px', height: '100px', }); expect(isContextDirty(stylingContext)).toBeFalsy(); updateStylingMap(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']); updateStylingMap(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', }); updateStylingMap(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', }); updateStylingMap(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(); updateStylingMap(stylingContext, {width: '100px', height: '100px'}); expect(stylingContext).toEqual([ element, [null], dirtyStyle(0, 11), // 2, null, // #5 cleanStyle(0, 11), 'width', null, // #8 cleanStyle(0, 14), 'height', null, // #11 dirtyStyle(0, 5), 'width', '100px', // #14 dirtyStyle(0, 8), 'height', '100px', ]); getStyles(stylingContext); updateStylingMap(stylingContext, {width: '200px', opacity: '0'}); expect(stylingContext).toEqual([ element, [null], dirtyStyle(0, 11), // 2, null, // #5 cleanStyle(0, 11), 'width', null, // #8 cleanStyle(0, 17), 'height', null, // #11 dirtyStyle(0, 5), 'width', '200px', // #14 dirtyStyle(), 'opacity', '0', // #17 dirtyStyle(0, 8), 'height', null, ]); getStyles(stylingContext); expect(stylingContext).toEqual([ element, [null], cleanStyle(0, 11), // 2, null, // #5 cleanStyle(0, 11), 'width', null, // #8 cleanStyle(0, 17), 'height', null, // #11 cleanStyle(0, 5), 'width', '200px', // #14 cleanStyle(), 'opacity', '0', // #17 cleanStyle(0, 8), 'height', null, ]); updateStylingMap(stylingContext, {width: null}); updateStyleProp(stylingContext, 0, '300px'); expect(stylingContext).toEqual([ element, [null], dirtyStyle(0, 11), // 2, null, // #5 dirtyStyle(0, 11), 'width', '300px', // #8 cleanStyle(0, 17), 'height', null, // #11 cleanStyle(0, 5), 'width', null, // #14 dirtyStyle(), 'opacity', null, // #17 cleanStyle(0, 8), 'height', null, ]); getStyles(stylingContext); updateStyleProp(stylingContext, 0, null); expect(stylingContext).toEqual([ element, [null], dirtyStyle(0, 11), // 2, null, // #5 dirtyStyle(0, 11), 'width', null, // #8 cleanStyle(0, 17), 'height', null, // #11 cleanStyle(0, 5), 'width', null, // #14 cleanStyle(), 'opacity', null, // #17 cleanStyle(0, 8), '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(); updateStylingMap(stylingContext, {width: '100px', height: '100px', opacity: '0.5'}); expect(stylingContext).toEqual([ element, [null], dirtyStyle(0, 8), // 1, null, // #5 cleanStyle(0, 17), 'lineHeight', null, // #8 dirtyStyle(), 'width', '100px', // #11 dirtyStyle(), 'height', '100px', // #14 dirtyStyle(), 'opacity', '0.5', // #17 cleanStyle(0, 5), 'lineHeight', null, ]); getStyles(stylingContext); updateStylingMap(stylingContext, {}); expect(stylingContext).toEqual([ element, [null], dirtyStyle(0, 8), // 1, null, // #5 cleanStyle(0, 17), 'lineHeight', null, // #8 dirtyStyle(), 'width', null, // #11 dirtyStyle(), 'height', null, // #14 dirtyStyle(), 'opacity', null, // #17 cleanStyle(0, 5), 'lineHeight', null, ]); getStyles(stylingContext); updateStylingMap(stylingContext, { borderWidth: '5px', }); expect(stylingContext).toEqual([ element, [null], dirtyStyle(0, 8), // 1, null, // #5 cleanStyle(0, 20), 'lineHeight', null, // #8 dirtyStyle(), 'borderWidth', '5px', // #11 cleanStyle(), 'width', null, // #14 cleanStyle(), 'height', null, // #17 cleanStyle(), 'opacity', null, // #20 cleanStyle(0, 5), 'lineHeight', null, ]); updateStyleProp(stylingContext, 0, '200px'); expect(stylingContext).toEqual([ element, [null], dirtyStyle(0, 8), // 1, null, // #5 dirtyStyle(0, 20), 'lineHeight', '200px', // #8 dirtyStyle(), 'borderWidth', '5px', // #11 cleanStyle(), 'width', null, // #14 cleanStyle(), 'height', null, // #17 cleanStyle(), 'opacity', null, // #20 cleanStyle(0, 5), 'lineHeight', null, ]); updateStylingMap(stylingContext, {borderWidth: '15px', borderColor: 'red'}); expect(stylingContext).toEqual([ element, [null], dirtyStyle(0, 8), // 1, null, // #5 dirtyStyle(0, 23), 'lineHeight', '200px', // #8 dirtyStyle(), 'borderWidth', '15px', // #11 dirtyStyle(), 'borderColor', 'red', // #14 cleanStyle(), 'width', null, // #17 cleanStyle(), 'height', null, // #20 cleanStyle(), 'opacity', null, // #23 cleanStyle(0, 5), 'lineHeight', null, ]); }); it('should render all data as not being dirty after the styles are built', () => { const getStyles = trackStylesFactory(); const stylingContext = initContext(['height']); updateStylingMap(stylingContext, { width: '100px', }); updateStyleProp(stylingContext, 0, '200px'); expect(stylingContext).toEqual([ element, [null], dirtyStyle(0, 8), // 1, null, // #5 dirtyStyle(0, 11), 'height', '200px', // #5 dirtyStyle(), 'width', '100px', // #11 cleanStyle(0, 5), 'height', null, ]); getStyles(stylingContext); expect(stylingContext).toEqual([ element, [null], cleanStyle(0, 8), // 1, null, // #5 cleanStyle(0, 11), 'height', '200px', // #5 cleanStyle(), 'width', '100px', // #11 cleanStyle(0, 5), 'height', 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, true, true], dirtyStyle(0, 11), // 0, null, // #5 cleanClass(1, 11), 'one', null, // #8 cleanClass(2, 14), 'two', null, // #11 dirtyClass(1, 5), 'one', null, // #14 dirtyClass(2, 8), '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, null, 'foo bar guy'); expect(getClasses(stylingContext)).toEqual({'foo': true, 'bar': true, 'guy': true}); updateStylingMap(stylingContext, null, '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, '100px', true], dirtyStyle(0, 17), // 2, null, // #5 cleanStyle(1, 17), 'width', null, // #8 cleanStyle(0, 20), 'height', null, // #11 cleanClass(2, 23), 'wide', null, // #14 cleanClass(0, 26), 'tall', null, // #17 dirtyStyle(1, 5), 'width', null, // #20 cleanStyle(0, 8), 'height', null, // #23 dirtyClass(2, 11), 'wide', null, // #26 cleanClass(0, 14), 'tall', null, ]); expect(getStylesAndClasses(stylingContext)).toEqual([{width: '100px'}, {wide: true}]); updateStylingMap(stylingContext, {width: '200px', opacity: '0.5'}, 'tall round'); expect(stylingContext).toEqual([ element, [null, '100px', true], dirtyStyle(0, 17), // 2, 'tall round', // #5 cleanStyle(1, 17), 'width', null, // #8 cleanStyle(0, 32), 'height', null, // #11 cleanClass(2, 29), 'wide', null, // #14 cleanClass(0, 23), 'tall', null, // #17 dirtyStyle(1, 5), 'width', '200px', // #20 dirtyStyle(0, 0), 'opacity', '0.5', // #23 dirtyClass(0, 14), 'tall', true, // #26 dirtyClass(0, 0), 'round', true, // #29 cleanClass(2, 11), 'wide', null, // #32 cleanStyle(0, 8), 'height', null, ]); expect(getStylesAndClasses(stylingContext)).toEqual([ {width: '200px', opacity: '0.5'}, {tall: true, round: true, wide: true} ]); updateStylingMap(stylingContext, {width: '500px'}, {tall: true, wide: true}); updateStyleProp(stylingContext, 0, '300px'); expect(stylingContext).toEqual([ element, [null, '100px', true], dirtyStyle(0, 17), // 2, null, // #5 dirtyStyle(1, 17), 'width', '300px', // #8 cleanStyle(0, 32), 'height', null, // #11 cleanClass(2, 23), 'wide', null, // #14 cleanClass(0, 20), 'tall', null, // #17 cleanStyle(1, 5), 'width', '500px', // #20 cleanClass(0, 14), 'tall', true, // #23 cleanClass(2, 11), 'wide', true, // #26 dirtyClass(0, 0), 'round', null, // #29 dirtyStyle(0, 0), 'opacity', null, // #32 cleanStyle(0, 8), 'height', null, ]); expect(getStylesAndClasses(stylingContext)).toEqual([ {width: '300px', opacity: null}, {tall: true, round: false, wide: true} ]); }); }); });