fix(ivy): ensure falsy styling is not applied during creation mode (#26793)

PR Close #26793
This commit is contained in:
Matias Niemelä 2018-10-26 16:32:18 -07:00
parent 332394d87c
commit 68b2211e64
6 changed files with 235 additions and 88 deletions

View File

@ -1161,8 +1161,9 @@ export function elementStyling(
*/ */
export function elementStylingApply(index: number): void { export function elementStylingApply(index: number): void {
const viewData = getViewData(); const viewData = getViewData();
const totalPlayersQueued = const isFirstRender = (viewData[FLAGS] & LViewFlags.CreationMode) !== 0;
renderStyleAndClassBindings(getStylingContext(index, viewData), getRenderer(), viewData); const totalPlayersQueued = renderStyleAndClassBindings(
getStylingContext(index, viewData), getRenderer(), viewData, isFirstRender);
if (totalPlayersQueued > 0) { if (totalPlayersQueued > 0) {
const rootContext = getRootContext(viewData); const rootContext = getRootContext(viewData);
scheduleTick(rootContext, RootContextFlags.FlushPlayers); scheduleTick(rootContext, RootContextFlags.FlushPlayers);

View File

@ -21,8 +21,8 @@ export interface Player {
export const enum BindingType { export const enum BindingType {
Unset = 0, Unset = 0,
Class = 2, Class = 1,
Style = 3, Style = 2,
} }
export interface BindingStore { setValue(prop: string, value: any): void; } export interface BindingStore { setValue(prop: string, value: any): void; }
@ -35,7 +35,7 @@ export interface BindingStore { setValue(prop: string, value: any): void; }
* to be used with `PlayerFactory`. * to be used with `PlayerFactory`.
*/ */
export interface PlayerFactoryBuildFn { export interface PlayerFactoryBuildFn {
(element: HTMLElement, type: BindingType, values: {[key: string]: any}, (element: HTMLElement, type: BindingType, values: {[key: string]: any}, isFirstRender: boolean,
currentPlayer: Player|null): Player|null; currentPlayer: Player|null): Player|null;
} }
@ -53,7 +53,7 @@ export interface PlayerFactoryBuildFn {
export interface PlayerFactory { '__brand__': 'Brand for PlayerFactory that nothing will match'; } export interface PlayerFactory { '__brand__': 'Brand for PlayerFactory that nothing will match'; }
export interface PlayerBuilder extends BindingStore { export interface PlayerBuilder extends BindingStore {
buildPlayer(currentPlayer: Player|null): Player|undefined|null; buildPlayer(currentPlayer: Player|null, isFirstRender: boolean): Player|undefined|null;
} }
/** /**

View File

@ -196,7 +196,7 @@ export const enum StylingFlags {
// The max amount of bits used to represent these configuration values // The max amount of bits used to represent these configuration values
BitCountSize = 5, BitCountSize = 5,
// There are only five bits here // There are only five bits here
BitMask = 0b1111 BitMask = 0b11111
} }
/** 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` */

View File

@ -480,8 +480,10 @@ export function updateClassProp(
*/ */
export function renderStyleAndClassBindings( export function renderStyleAndClassBindings(
context: StylingContext, renderer: Renderer3, rootOrView: RootContext | LViewData, context: StylingContext, renderer: Renderer3, rootOrView: RootContext | LViewData,
classesStore?: BindingStore | null, stylesStore?: BindingStore | null): number { isFirstRender: boolean, classesStore?: BindingStore | null,
stylesStore?: BindingStore | null): number {
let totalPlayersQueued = 0; let totalPlayersQueued = 0;
if (isContextDirty(context)) { if (isContextDirty(context)) {
const flushPlayerBuilders: any = const flushPlayerBuilders: any =
context[StylingIndex.MasterFlagPosition] & StylingFlags.PlayerBuildersDirty; context[StylingIndex.MasterFlagPosition] & StylingFlags.PlayerBuildersDirty;
@ -523,6 +525,12 @@ export function renderStyleAndClassBindings(
valueToApply = getInitialValue(context, flag); valueToApply = getInitialValue(context, flag);
} }
// if the first render is true then we do not want to start applying falsy
// values to the DOM element's styling. Otherwise then we know there has
// been a change and even if it's falsy then it's removing something that
// was truthy before.
const doApplyValue = isFirstRender ? valueToApply : true;
if (doApplyValue) {
if (isClassBased) { if (isClassBased) {
setClass( setClass(
native, prop, valueToApply ? true : false, renderer, classesStore, playerBuilder); native, prop, valueToApply ? true : false, renderer, classesStore, playerBuilder);
@ -532,6 +540,8 @@ export function renderStyleAndClassBindings(
native, prop, valueToApply as string | null, renderer, sanitizer, stylesStore, native, prop, valueToApply as string | null, renderer, sanitizer, stylesStore,
playerBuilder); playerBuilder);
} }
}
setDirty(context, i, false); setDirty(context, i, false);
} }
} }
@ -547,7 +557,7 @@ export function renderStyleAndClassBindings(
const playerInsertionIndex = i + PlayerIndex.PlayerOffsetPosition; const playerInsertionIndex = i + PlayerIndex.PlayerOffsetPosition;
const oldPlayer = playerContext[playerInsertionIndex] as Player | null; const oldPlayer = playerContext[playerInsertionIndex] as Player | null;
if (builder) { if (builder) {
const player = builder.buildPlayer(oldPlayer); const player = builder.buildPlayer(oldPlayer, isFirstRender);
if (player !== undefined) { if (player !== undefined) {
if (player != null) { if (player != null) {
const wasQueued = addPlayerInternal( const wasQueued = addPlayerInternal(
@ -924,13 +934,13 @@ export class ClassAndStylePlayerBuilder<T> implements PlayerBuilder {
} }
} }
buildPlayer(currentPlayer?: Player|null): Player|undefined|null { buildPlayer(currentPlayer: Player|null, isFirstRender: boolean): Player|undefined|null {
// if no values have been set here then this means the binding didn't // if no values have been set here then this means the binding didn't
// change and therefore the binding values were not updated through // change and therefore the binding values were not updated through
// `setValue` which means no new player will be provided. // `setValue` which means no new player will be provided.
if (this._dirty) { if (this._dirty) {
const player = const player = this._factory.fn(
this._factory.fn(this._element, this._type, this._values !, currentPlayer || null); this._element, this._type, this._values !, isFirstRender, currentPlayer || null);
this._values = {}; this._values = {};
this._dirty = false; this._dirty = false;
return player; return player;

View File

@ -147,11 +147,14 @@ renderComponent(AnimationWorldComponent, {playerHandler});
function animateStyleFactory(keyframes: any[], duration: number, easing: string) { function animateStyleFactory(keyframes: any[], duration: number, easing: string) {
const limit = keyframes.length - 1; const limit = keyframes.length - 1;
const finalKeyframe = keyframes[limit]; const finalKeyframe = keyframes[limit];
return bindPlayerFactory((element: HTMLElement, type: number, values: {[key: string]: any}) => { return bindPlayerFactory(
(element: HTMLElement, type: number, values: {[key: string]: any},
isFirstRender: boolean) => {
const kf = keyframes.slice(0, limit); const kf = keyframes.slice(0, limit);
kf.push(values); kf.push(values);
return new WebAnimationsPlayer(element, keyframes, duration, easing); return new WebAnimationsPlayer(element, keyframes, duration, easing);
}, finalKeyframe); },
finalKeyframe);
} }
class WebAnimationsPlayer implements Player { class WebAnimationsPlayer implements Player {

View File

@ -48,34 +48,40 @@ describe('style and class based bindings', () => {
return lViewData[CONTEXT] as RootContext; return lViewData[CONTEXT] as RootContext;
} }
function renderStyles(context: StylingContext, renderer?: Renderer3, lViewData?: LViewData) { function renderStyles(
context: StylingContext, firstRender?: boolean, renderer?: Renderer3, lViewData?: LViewData) {
const store = new MockStylingStore(element as HTMLElement, BindingType.Style); const store = new MockStylingStore(element as HTMLElement, BindingType.Style);
const handler = new CorePlayerHandler(); const handler = new CorePlayerHandler();
_renderStyling( _renderStyling(
context, (renderer || {}) as Renderer3, context, (renderer || {}) as Renderer3,
getRootContextInternal(lViewData || createMockViewData(handler, context)), null, store); getRootContextInternal(lViewData || createMockViewData(handler, context)), !!firstRender,
null, store);
return store.getValues(); return store.getValues();
} }
function trackStylesFactory() { function trackStylesFactory(store?: MockStylingStore) {
const store = new MockStylingStore(element as HTMLElement, BindingType.Style); store = store || new MockStylingStore(element as HTMLElement, BindingType.Style);
const handler = new CorePlayerHandler(); const handler = new CorePlayerHandler();
return function(context: StylingContext, renderer?: Renderer3): {[key: string]: any} { return function(context: StylingContext, firstRender?: boolean, renderer?: Renderer3):
{[key: string]: any} {
const lViewData = createMockViewData(handler, context); const lViewData = createMockViewData(handler, context);
_renderStyling( _renderStyling(
context, (renderer || {}) as Renderer3, getRootContextInternal(lViewData), null, store); context, (renderer || {}) as Renderer3, getRootContextInternal(lViewData),
return store.getValues(); !!firstRender, null, store);
return store !.getValues();
}; };
} }
function trackClassesFactory() { function trackClassesFactory(store?: MockStylingStore) {
const store = new MockStylingStore(element as HTMLElement, BindingType.Class); store = store || new MockStylingStore(element as HTMLElement, BindingType.Class);
const handler = new CorePlayerHandler(); const handler = new CorePlayerHandler();
return function(context: StylingContext, renderer?: Renderer3): {[key: string]: any} { return function(context: StylingContext, firstRender?: boolean, renderer?: Renderer3):
{[key: string]: any} {
const lViewData = createMockViewData(handler, context); const lViewData = createMockViewData(handler, context);
_renderStyling( _renderStyling(
context, (renderer || {}) as Renderer3, getRootContextInternal(lViewData), store); context, (renderer || {}) as Renderer3, getRootContextInternal(lViewData),
return store.getValues(); !!firstRender, store);
return store !.getValues();
}; };
} }
@ -83,11 +89,12 @@ describe('style and class based bindings', () => {
const classStore = new MockStylingStore(element as HTMLElement, BindingType.Class); const classStore = new MockStylingStore(element as HTMLElement, BindingType.Class);
const styleStore = new MockStylingStore(element as HTMLElement, BindingType.Style); const styleStore = new MockStylingStore(element as HTMLElement, BindingType.Style);
const handler = new CorePlayerHandler(); const handler = new CorePlayerHandler();
return function(context: StylingContext, renderer?: Renderer3): {[key: string]: any} { return function(context: StylingContext, firstRender?: boolean, renderer?: Renderer3):
{[key: string]: any} {
const lViewData = createMockViewData(handler, context); const lViewData = createMockViewData(handler, context);
_renderStyling( _renderStyling(
context, (renderer || {}) as Renderer3, getRootContextInternal(lViewData), classStore, context, (renderer || {}) as Renderer3, getRootContextInternal(lViewData),
styleStore); !!firstRender, classStore, styleStore);
return [classStore.getValues(), styleStore.getValues()]; return [classStore.getValues(), styleStore.getValues()];
}; };
} }
@ -234,7 +241,7 @@ describe('style and class based bindings', () => {
height: '100px', height: '100px',
}); });
updateStyles(stylingContext, {height: '200px'}); updateStyles(stylingContext, {height: '200px'});
expect(getStyles(stylingContext)).toEqual({width: null, height: '200px'}); expect(getStyles(stylingContext, true)).toEqual({height: '200px'});
}); });
it('should evaluate the delta between style changes when rendering occurs', () => { it('should evaluate the delta between style changes when rendering occurs', () => {
@ -1029,6 +1036,32 @@ describe('style and class based bindings', () => {
}); });
}); });
it('should skip issuing style updates if there is nothing to update upon first render', () => {
const stylingContext = initContext([InitialStylingFlags.VALUES_MODE, 'color', '']);
const store = new MockStylingStore(element as HTMLElement, BindingType.Class);
const getStyles = trackStylesFactory(store);
let styles: any = {fontSize: ''};
updateStyleProp(stylingContext, 0, '');
updateStylingMap(stylingContext, null, styles);
getStyles(stylingContext, true);
expect(store.getValues()).toEqual({});
styles = {fontSize: '20px'};
updateStyleProp(stylingContext, 0, 'red');
updateStylingMap(stylingContext, null, styles);
getStyles(stylingContext);
expect(store.getValues()).toEqual({fontSize: '20px', color: 'red'});
styles = {};
updateStyleProp(stylingContext, 0, '');
updateStylingMap(stylingContext, null, styles);
getStyles(stylingContext);
expect(store.getValues()).toEqual({fontSize: null, color: ''});
});
}); });
describe('classes', () => { describe('classes', () => {
@ -1447,6 +1480,34 @@ describe('style and class based bindings', () => {
// apply the styles // apply the styles
expect(getClasses(stylingContext)).toEqual({apple: true, orange: true, banana: true}); expect(getClasses(stylingContext)).toEqual({apple: true, orange: true, banana: true});
}); });
it('should skip issuing class updates if there is nothing to update upon first render', () => {
const stylingContext = initContext(null, [InitialStylingFlags.VALUES_MODE, 'blue', false]);
const store = new MockStylingStore(element as HTMLElement, BindingType.Class);
const getClasses = trackClassesFactory(store);
let classes: any = {red: false};
updateClassProp(stylingContext, 0, false);
updateStylingMap(stylingContext, classes);
// apply the styles
getClasses(stylingContext, true);
expect(store.getValues()).toEqual({});
classes = {red: true};
updateClassProp(stylingContext, 0, true);
updateStylingMap(stylingContext, classes);
getClasses(stylingContext);
expect(store.getValues()).toEqual({red: true, blue: true});
classes = {red: false};
updateClassProp(stylingContext, 0, false);
updateStylingMap(stylingContext, classes);
getClasses(stylingContext);
expect(store.getValues()).toEqual({red: false, blue: false});
});
}); });
describe('players', () => { describe('players', () => {
@ -1457,20 +1518,22 @@ describe('style and class based bindings', () => {
const classes = 'foo bar'; const classes = 'foo bar';
let classResult: any; let classResult: any;
const classFactory = const classFactory = bindPlayerFactory(
bindPlayerFactory((element: HTMLElement, type: BindingType, value: any) => { (element: HTMLElement, type: BindingType, value: any, firstRender: boolean) => {
const player = new MockPlayer(); const player = new MockPlayer();
classResult = {player, element, type, value}; classResult = {player, element, type, value};
return player; return player;
}, classes); },
classes);
let styleResult: any; let styleResult: any;
const styleFactory = const styleFactory = bindPlayerFactory(
bindPlayerFactory((element: HTMLElement, type: BindingType, value: any) => { (element: HTMLElement, type: BindingType, value: any, firstRender: boolean) => {
const player = new MockPlayer(); const player = new MockPlayer();
styleResult = {player, element, type, value}; styleResult = {player, element, type, value};
return player; return player;
}, styles); },
styles);
updateStylingMap(context, classFactory, styleFactory); updateStylingMap(context, classFactory, styleFactory);
expect(classResult).toBeFalsy(); expect(classResult).toBeFalsy();
@ -1562,7 +1625,7 @@ describe('style and class based bindings', () => {
5, classPlayerBuilder, null, stylePlayerBuilder, null 5, classPlayerBuilder, null, stylePlayerBuilder, null
]); ]);
renderStyles(context, undefined, lViewData); renderStyles(context, false, undefined, lViewData);
expect(context[StylingIndex.PlayerContext]).toEqual([ expect(context[StylingIndex.PlayerContext]).toEqual([
5, classPlayerBuilder, currentClassPlayer !, stylePlayerBuilder, currentStylePlayer ! 5, classPlayerBuilder, currentClassPlayer !, stylePlayerBuilder, currentStylePlayer !
]); ]);
@ -1638,7 +1701,7 @@ describe('style and class based bindings', () => {
barPlayerBuilder, null barPlayerBuilder, null
]); ]);
renderStyles(context, undefined, lViewData); renderStyles(context, false, undefined, lViewData);
const classMapPlayer = capturedClassPlayers.shift() !; const classMapPlayer = capturedClassPlayers.shift() !;
const barPlayer = capturedClassPlayers.shift() !; const barPlayer = capturedClassPlayers.shift() !;
const styleMapPlayer = capturedStylePlayers.shift() !; const styleMapPlayer = capturedStylePlayers.shift() !;
@ -1671,7 +1734,7 @@ describe('style and class based bindings', () => {
bazPlayerBuilder, null bazPlayerBuilder, null
]); ]);
renderStyles(context, undefined, lViewData); renderStyles(context, false, undefined, lViewData);
const heightPlayer = capturedStylePlayers.shift() !; const heightPlayer = capturedStylePlayers.shift() !;
const bazPlayer = capturedClassPlayers.shift() !; const bazPlayer = capturedClassPlayers.shift() !;
@ -1698,7 +1761,7 @@ describe('style and class based bindings', () => {
const players: MockPlayer[] = []; const players: MockPlayer[] = [];
const buildFn = const buildFn =
(element: HTMLElement, type: BindingType, value: any, (element: HTMLElement, type: BindingType, value: any, firstRender: boolean,
oldPlayer: MockPlayer | null) => { oldPlayer: MockPlayer | null) => {
const player = new MockPlayer(value); const player = new MockPlayer(value);
players.push(player); players.push(player);
@ -1709,7 +1772,7 @@ describe('style and class based bindings', () => {
let mapFactory = bindPlayerFactory(buildFn, {width: '200px'}); let mapFactory = bindPlayerFactory(buildFn, {width: '200px'});
updateStylingMap(context, null, mapFactory); updateStylingMap(context, null, mapFactory);
renderStyles(context, undefined, lViewData); renderStyles(context, false, undefined, lViewData);
expect(players.length).toEqual(1); expect(players.length).toEqual(1);
const p1 = players.pop() !; const p1 = players.pop() !;
@ -1717,7 +1780,7 @@ describe('style and class based bindings', () => {
mapFactory = bindPlayerFactory(buildFn, {width: '100px'}); mapFactory = bindPlayerFactory(buildFn, {width: '100px'});
updateStylingMap(context, null, mapFactory); updateStylingMap(context, null, mapFactory);
renderStyles(context, undefined, lViewData); renderStyles(context, false, undefined, lViewData);
expect(players.length).toEqual(1); expect(players.length).toEqual(1);
const p2 = players.pop() !; const p2 = players.pop() !;
@ -1792,7 +1855,7 @@ describe('style and class based bindings', () => {
const fooPlayerBuilder = makePlayerBuilder(fooWithPlayerFactory, true); const fooPlayerBuilder = makePlayerBuilder(fooWithPlayerFactory, true);
updateStyleProp(context, 0, colorWithPlayerFactory as any); updateStyleProp(context, 0, colorWithPlayerFactory as any);
updateClassProp(context, 0, fooWithPlayerFactory as any); updateClassProp(context, 0, fooWithPlayerFactory as any);
renderStyles(context, undefined, lViewData); renderStyles(context, false, undefined, lViewData);
const p1 = classPlayers.shift(); const p1 = classPlayers.shift();
const p2 = stylePlayers.shift(); const p2 = stylePlayers.shift();
@ -1856,7 +1919,7 @@ describe('style and class based bindings', () => {
const fooWithoutPlayerFactory = false; const fooWithoutPlayerFactory = false;
updateStyleProp(context, 0, colorWithoutPlayerFactory); updateStyleProp(context, 0, colorWithoutPlayerFactory);
updateClassProp(context, 0, fooWithoutPlayerFactory); updateClassProp(context, 0, fooWithoutPlayerFactory);
renderStyles(context, undefined, lViewData); renderStyles(context, false, undefined, lViewData);
expect(context).toEqual([ expect(context).toEqual([
([9, null, null, null, null, null, null, null, null] as any), ([9, null, null, null, null, null, null, null, null] as any),
@ -1930,28 +1993,28 @@ describe('style and class based bindings', () => {
expect(styleCalls).toEqual(0); expect(styleCalls).toEqual(0);
expect(classCalls).toEqual(0); expect(classCalls).toEqual(0);
renderStyles(context, undefined, lViewData); renderStyles(context, false, undefined, lViewData);
expect(styleCalls).toEqual(1); expect(styleCalls).toEqual(1);
expect(classCalls).toEqual(1); expect(classCalls).toEqual(1);
renderStyles(context, undefined, lViewData); renderStyles(context, false, undefined, lViewData);
expect(styleCalls).toEqual(1); expect(styleCalls).toEqual(1);
expect(classCalls).toEqual(1); expect(classCalls).toEqual(1);
styleFactory.value = {opacity: '0.5'}; styleFactory.value = {opacity: '0.5'};
updateStylingMap(context, classFactory, styleFactory); updateStylingMap(context, classFactory, styleFactory);
renderStyles(context, undefined, lViewData); renderStyles(context, false, undefined, lViewData);
expect(styleCalls).toEqual(2); expect(styleCalls).toEqual(2);
expect(classCalls).toEqual(1); expect(classCalls).toEqual(1);
classFactory.value = 'foo'; classFactory.value = 'foo';
updateStylingMap(context, classFactory, styleFactory); updateStylingMap(context, classFactory, styleFactory);
renderStyles(context, undefined, lViewData); renderStyles(context, false, undefined, lViewData);
expect(styleCalls).toEqual(2); expect(styleCalls).toEqual(2);
expect(classCalls).toEqual(2); expect(classCalls).toEqual(2);
updateStylingMap(context, 'foo', {opacity: '0.5'}); updateStylingMap(context, 'foo', {opacity: '0.5'});
renderStyles(context, undefined, lViewData); renderStyles(context, false, undefined, lViewData);
expect(styleCalls).toEqual(2); expect(styleCalls).toEqual(2);
expect(classCalls).toEqual(2); expect(classCalls).toEqual(2);
}); });
@ -1975,14 +2038,14 @@ describe('style and class based bindings', () => {
const mapFactory = bindPlayerFactory(mapBuildFn, {color: 'black'}); const mapFactory = bindPlayerFactory(mapBuildFn, {color: 'black'});
updateStylingMap(context, null, mapFactory); updateStylingMap(context, null, mapFactory);
updateStyleProp(context, 0, 'green'); updateStyleProp(context, 0, 'green');
renderStyles(context, undefined, lViewData); renderStyles(context, false, undefined, lViewData);
expect(propPlayer).toBeFalsy(); expect(propPlayer).toBeFalsy();
expect(styleMapPlayer).toBeFalsy(); expect(styleMapPlayer).toBeFalsy();
const propFactory = bindPlayerFactory(propBuildFn, 'orange'); const propFactory = bindPlayerFactory(propBuildFn, 'orange');
updateStyleProp(context, 0, propFactory as any); updateStyleProp(context, 0, propFactory as any);
renderStyles(context, undefined, lViewData); renderStyles(context, false, undefined, lViewData);
expect(propPlayer).toBeTruthy(); expect(propPlayer).toBeTruthy();
expect(styleMapPlayer).toBeFalsy(); expect(styleMapPlayer).toBeFalsy();
@ -1990,7 +2053,7 @@ describe('style and class based bindings', () => {
propPlayer = styleMapPlayer = null; propPlayer = styleMapPlayer = null;
updateStyleProp(context, 0, null); updateStyleProp(context, 0, null);
renderStyles(context, undefined, lViewData); renderStyles(context, false, undefined, lViewData);
expect(propPlayer).toBeFalsy(); expect(propPlayer).toBeFalsy();
expect(styleMapPlayer).toBeTruthy(); expect(styleMapPlayer).toBeTruthy();
@ -1998,7 +2061,7 @@ describe('style and class based bindings', () => {
propPlayer = styleMapPlayer = null; propPlayer = styleMapPlayer = null;
updateStylingMap(context, null, null); updateStylingMap(context, null, null);
renderStyles(context, undefined, lViewData); renderStyles(context, false, undefined, lViewData);
expect(propPlayer).toBeFalsy(); expect(propPlayer).toBeFalsy();
expect(styleMapPlayer).toBeFalsy(); expect(styleMapPlayer).toBeFalsy();
@ -2012,14 +2075,15 @@ describe('style and class based bindings', () => {
let previousPlayer: MockPlayer|null = null; let previousPlayer: MockPlayer|null = null;
let currentPlayer: MockPlayer|null = null; let currentPlayer: MockPlayer|null = null;
const buildFn = const buildFn =
(element: HTMLElement, type: BindingType, value: any, existingPlayer: MockPlayer) => { (element: HTMLElement, type: BindingType, value: any, firstRender: boolean,
existingPlayer: MockPlayer) => {
previousPlayer = existingPlayer; previousPlayer = existingPlayer;
return currentPlayer = new MockPlayer(value); return currentPlayer = new MockPlayer(value);
}; };
let factory = bindPlayerFactory<{[key: string]: any}>(buildFn, {width: '200px'}); let factory = bindPlayerFactory<{[key: string]: any}>(buildFn, {width: '200px'});
updateStylingMap(context, null, factory); updateStylingMap(context, null, factory);
renderStyles(context, undefined, lViewData); renderStyles(context, false, undefined, lViewData);
expect(previousPlayer).toEqual(null); expect(previousPlayer).toEqual(null);
expect(currentPlayer !.value).toEqual({width: '200px'}); expect(currentPlayer !.value).toEqual({width: '200px'});
@ -2027,7 +2091,7 @@ describe('style and class based bindings', () => {
factory = bindPlayerFactory(buildFn, {height: '200px'}); factory = bindPlayerFactory(buildFn, {height: '200px'});
updateStylingMap(context, null, factory); updateStylingMap(context, null, factory);
renderStyles(context, undefined, lViewData); renderStyles(context, false, undefined, lViewData);
expect(previousPlayer !.value).toEqual({width: '200px'}); expect(previousPlayer !.value).toEqual({width: '200px'});
expect(currentPlayer !.value).toEqual({width: null, height: '200px'}); expect(currentPlayer !.value).toEqual({width: null, height: '200px'});
@ -2041,7 +2105,7 @@ describe('style and class based bindings', () => {
let currentPlayer: MockPlayer|null = null; let currentPlayer: MockPlayer|null = null;
let previousPlayer: MockPlayer|null = null; let previousPlayer: MockPlayer|null = null;
const buildFn = const buildFn =
(element: HTMLElement, type: BindingType, value: any, (element: HTMLElement, type: BindingType, value: any, firstRender: boolean,
existingPlayer: MockPlayer | null) => { existingPlayer: MockPlayer | null) => {
previousPlayer = existingPlayer; previousPlayer = existingPlayer;
return currentPlayer = new MockPlayer(value); return currentPlayer = new MockPlayer(value);
@ -2049,7 +2113,7 @@ describe('style and class based bindings', () => {
let factory = bindPlayerFactory<any>(buildFn, {foo: true}); let factory = bindPlayerFactory<any>(buildFn, {foo: true});
updateStylingMap(context, null, factory); updateStylingMap(context, null, factory);
renderStyles(context, undefined, lViewData); renderStyles(context, false, undefined, lViewData);
expect(currentPlayer).toBeTruthy(); expect(currentPlayer).toBeTruthy();
expect(previousPlayer).toBeFalsy(); expect(previousPlayer).toBeFalsy();
@ -2059,7 +2123,7 @@ describe('style and class based bindings', () => {
factory = bindPlayerFactory(buildFn, {bar: true}); factory = bindPlayerFactory(buildFn, {bar: true});
updateStylingMap(context, null, factory); updateStylingMap(context, null, factory);
renderStyles(context, undefined, lViewData); renderStyles(context, false, undefined, lViewData);
expect(currentPlayer).toBeTruthy(); expect(currentPlayer).toBeTruthy();
expect(previousPlayer).toBeTruthy(); expect(previousPlayer).toBeTruthy();
@ -2081,7 +2145,8 @@ describe('style and class based bindings', () => {
const lViewData = createMockViewData(handler, context); const lViewData = createMockViewData(handler, context);
let values: {[key: string]: any}|null = null; let values: {[key: string]: any}|null = null;
const buildFn = (element: HTMLElement, type: BindingType, value: any) => { const buildFn =
(element: HTMLElement, type: BindingType, value: any, isFirstRender: boolean) => {
values = value; values = value;
return new MockPlayer(); return new MockPlayer();
}; };
@ -2089,13 +2154,13 @@ describe('style and class based bindings', () => {
let factory = bindPlayerFactory<{[key: string]: any}>( let factory = bindPlayerFactory<{[key: string]: any}>(
buildFn, {width: '200px', height: '100px', opacity: '1'}); buildFn, {width: '200px', height: '100px', opacity: '1'});
updateStylingMap(context, null, factory); updateStylingMap(context, null, factory);
renderStyles(context, undefined, lViewData); renderStyles(context, false, undefined, lViewData);
expect(values !).toEqual({width: '200px-safe!', height: '100px-safe!', opacity: '1'}); expect(values !).toEqual({width: '200px-safe!', height: '100px-safe!', opacity: '1'});
factory = bindPlayerFactory(buildFn, {width: 'auto'}); factory = bindPlayerFactory(buildFn, {width: 'auto'});
updateStylingMap(context, null, factory); updateStylingMap(context, null, factory);
renderStyles(context, undefined, lViewData); renderStyles(context, false, undefined, lViewData);
expect(values !).toEqual({width: 'auto-safe!', height: null, opacity: null}); expect(values !).toEqual({width: 'auto-safe!', height: null, opacity: null});
}); });
@ -2127,7 +2192,7 @@ describe('style and class based bindings', () => {
updateStyleProp(context, 0, bindPlayerFactory(styleBuildFn, '100px') as any); updateStyleProp(context, 0, bindPlayerFactory(styleBuildFn, '100px') as any);
updateClassProp(context, 0, bindPlayerFactory(classBuildFn, true) as any); updateClassProp(context, 0, bindPlayerFactory(classBuildFn, true) as any);
updateClassProp(context, 1, bindPlayerFactory(classBuildFn, true) as any); updateClassProp(context, 1, bindPlayerFactory(classBuildFn, true) as any);
renderStyles(context, undefined, lViewData); renderStyles(context, false, undefined, lViewData);
handler.flushPlayers(); handler.flushPlayers();
const [p1, p2, p3, p4, p5] = players; const [p1, p2, p3, p4, p5] = players;
@ -2145,7 +2210,7 @@ describe('style and class based bindings', () => {
expect(p4.state).toEqual(PlayState.Running); expect(p4.state).toEqual(PlayState.Running);
expect(p5.state).toEqual(PlayState.Running); expect(p5.state).toEqual(PlayState.Running);
renderStyles(context, undefined, lViewData); renderStyles(context, false, undefined, lViewData);
expect(p1.state).toEqual(PlayState.Destroyed); expect(p1.state).toEqual(PlayState.Destroyed);
expect(p2.state).toEqual(PlayState.Destroyed); expect(p2.state).toEqual(PlayState.Destroyed);
expect(p3.state).toEqual(PlayState.Destroyed); expect(p3.state).toEqual(PlayState.Destroyed);
@ -2226,6 +2291,74 @@ describe('style and class based bindings', () => {
expect(getPlayers(target)).toEqual([p1, p2, p4, p6]); expect(getPlayers(target)).toEqual([p1, p2, p4, p6]);
}); });
it('should build a player and signal that the first render is active', () => {
const firstRenderCaptures: any[] = [];
const otherRenderCaptures: any[] = [];
const buildFn =
(element: HTMLElement, type: BindingType, value: any, isFirstRender: boolean) => {
if (isFirstRender) {
firstRenderCaptures.push({type, value});
} else {
otherRenderCaptures.push({type, value});
}
return new MockPlayer();
};
const styleMapFactory =
bindPlayerFactory(buildFn, {height: '200px'}) as BoundPlayerFactory<any>;
const classMapFactory = bindPlayerFactory(buildFn, {bar: true}) as BoundPlayerFactory<any>;
const widthFactory = bindPlayerFactory(buildFn, '100px') as BoundPlayerFactory<any>;
const fooFactory = bindPlayerFactory(buildFn, true) as BoundPlayerFactory<any>;
class Comp {
static ngComponentDef = defineComponent({
type: Comp,
selectors: [['comp']],
directives: [Comp],
factory: () => new Comp(),
consts: 1,
vars: 0,
template: (rf: RenderFlags, ctx: Comp) => {
if (rf & RenderFlags.Create) {
elementStart(0, 'div');
elementStyling(['foo'], ['width']);
elementEnd();
}
if (rf & RenderFlags.Update) {
elementStylingMap(0, classMapFactory, styleMapFactory);
elementStyleProp(0, 0, widthFactory);
elementClassProp(0, 0, fooFactory);
elementStylingApply(0);
}
}
});
}
const fixture = new ComponentFixture(Comp);
expect(firstRenderCaptures.length).toEqual(4);
expect(firstRenderCaptures[0]).toEqual({type: BindingType.Class, value: {bar: true}});
expect(firstRenderCaptures[1]).toEqual({type: BindingType.Style, value: {height: '200px'}});
expect(firstRenderCaptures[2]).toEqual({type: BindingType.Style, value: {width: '100px'}});
expect(firstRenderCaptures[3]).toEqual({type: BindingType.Class, value: {foo: true}});
expect(otherRenderCaptures.length).toEqual(0);
firstRenderCaptures.length = 0;
styleMapFactory.value = {height: '100px'};
classMapFactory.value = {bar: false};
widthFactory.value = '50px';
fooFactory.value = false;
fixture.update();
expect(firstRenderCaptures.length).toEqual(0);
expect(otherRenderCaptures.length).toEqual(4);
expect(otherRenderCaptures[0]).toEqual({type: BindingType.Class, value: {bar: false}});
expect(otherRenderCaptures[1]).toEqual({type: BindingType.Style, value: {height: '100px'}});
expect(otherRenderCaptures[2]).toEqual({type: BindingType.Style, value: {width: '50px'}});
expect(otherRenderCaptures[3]).toEqual({type: BindingType.Class, value: {foo: false}});
});
}); });
}); });