diff --git a/frontend/app/framework/angular/routers/router-2-state.spec.ts b/frontend/app/framework/angular/routers/router-2-state.spec.ts index 1e38600dd..bd4f80602 100644 --- a/frontend/app/framework/angular/routers/router-2-state.spec.ts +++ b/frontend/app/framework/angular/routers/router-2-state.spec.ts @@ -307,13 +307,28 @@ describe('Router2State', () => { routerQueryParams.next({ key1: 'hello', - key2: 'squidex', + key2: 'cms', key3: '!' }); expect(invoked).toEqual(2); }); + it('Should not sync again when no state as changed', () => { + routerQueryParams.next({ + key1: 'hello', + key2: 'squidex' + }); + + routerQueryParams.next({ + key1: 'hello', + key2: 'squidex', + key3: '!' + }); + + expect(invoked).toEqual(1); + }); + it('Should reset other values when synced from route', () => { state.next({ other: 123 }); diff --git a/frontend/app/framework/angular/routers/router-2-state.ts b/frontend/app/framework/angular/routers/router-2-state.ts index eeee03ec6..0e934f73f 100644 --- a/frontend/app/framework/angular/routers/router-2-state.ts +++ b/frontend/app/framework/angular/routers/router-2-state.ts @@ -299,10 +299,10 @@ export class Router2StateMap implements StateSynchronizerMap { + let state: State; + + beforeEach(() => { + state = new State({}); + }); + + it('should update state with new value', () => { + let updateCount = 0; + + state.changes.subscribe(() => { + updateCount++; + }); + + const updated = state.next({ value: 1 }); + + expect(updateCount).toEqual(2); + expect(updated).toBeTruthy(); + }); + + it('should reset state with new value', () => { + let updateCount = 0; + + state.changes.subscribe(() => { + updateCount++; + }); + + const updated = state.resetState({ value: 1 }); + + expect(updateCount).toEqual(2); + expect(updated).toBeTruthy(); + }); + + it('should not update state when nothing changed', () => { + let updateCount = 0; + + state.changes.subscribe(() => { + updateCount++; + }); + + state.next({ value: 1 }); + + const updated = state.next({ value: 1 }); + + expect(updateCount).toEqual(2); + expect(updated).toBeFalsy(); + }); + + it('should not reset state when nothing changed', () => { + let updateCount = 0; + + state.changes.subscribe(() => { + updateCount++; + }); + + state.resetState({ value: 1 }); + + const updated = state.resetState({ value: 1 }); + + expect(updateCount).toEqual(2); + expect(updated).toBeFalsy(); + }); +}); \ No newline at end of file diff --git a/frontend/app/framework/state.ts b/frontend/app/framework/state.ts index 5280f9e75..d62f22e1f 100644 --- a/frontend/app/framework/state.ts +++ b/frontend/app/framework/state.ts @@ -164,7 +164,6 @@ export class ResultSet { export class State { private readonly state: BehaviorSubject>; - private readonly initialState: Readonly; public get changes(): Observable> { return this.state; @@ -185,39 +184,54 @@ export class State { } public projectFrom2(lhs: Observable, rhs: Observable, project: (l: M, r: N) => O, compare?: (x: O, y: O) => boolean) { - return combineLatest(lhs, rhs, (x, y) => project(x, y)).pipe( - distinctUntilChanged(compare), shareReplay(1)); + return combineLatest([lhs, rhs]).pipe( + map(([x, y]) => project(x, y)), distinctUntilChanged(compare), shareReplay(1)); } - constructor(state: Readonly) { - this.initialState = state; - - this.state = new BehaviorSubject(state); + constructor( + private readonly initialState: Readonly + ) { + this.state = new BehaviorSubject(initialState); } public resetState(update?: ((v: T) => Readonly) | Partial) { - let newState = this.initialState; + return this.updateState(this.initialState, update); + } + + public next(update: ((v: T) => Readonly) | Partial) { + return this.updateState(this.state.value, update); + } + + private updateState(state: T, update?: ((v: T) => Readonly) | Partial) { + let newState = state; if (update) { if (Types.isFunction(update)) { - newState = update(this.initialState); + newState = update(state); } else { - newState = { ...this.initialState, ...update }; + newState = { ...state, ...update }; } } - this.state.next(newState); - } + let isChanged = false; - public next(update: ((v: T) => Readonly) | Partial) { - let newState: T; + const newKeys = Object.keys(newState); - if (Types.isFunction(update)) { - newState = update(this.state.value); + if (newKeys.length !== Object.keys(this.snapshot).length) { + isChanged = true; } else { - newState = { ...this.state.value, ...update }; + for (const key of newKeys) { + if (newState[key] !== this.snapshot[key]) { + isChanged = true; + break; + } + } + } + + if (isChanged) { + this.state.next(newState); } - this.state.next(newState); + return isChanged; } } \ No newline at end of file diff --git a/frontend/app/framework/utils/types.ts b/frontend/app/framework/utils/types.ts index 86dd9a830..65d1cccf7 100644 --- a/frontend/app/framework/utils/types.ts +++ b/frontend/app/framework/utils/types.ts @@ -162,15 +162,15 @@ export module Types { return true; } else if (Types.isObject(lhs) && Types.isObject(rhs)) { - if (Object.keys(lhs).length !== Object.keys(rhs).length) { + const lhsKeys = Object.keys(lhs); + + if (lhsKeys.length !== Object.keys(rhs).length) { return false; } - for (const key in lhs) { - if (lhs.hasOwnProperty(key)) { - if (!equals(lhs[key], rhs[key], lazyString)) { - return false; - } + for (const key of lhsKeys) { + if (!equals(lhs[key], rhs[key], lazyString)) { + return false; } }