Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(store): implements ngxsOnChanges #1389

Merged
merged 11 commits into from
Oct 25, 2019
Merged
33 changes: 33 additions & 0 deletions docs/advanced/life-cycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,29 @@

States can implement life-cycle events.

## `ngxsOnChanges`

If a state implements the NgxsOnChanges interface, its ngxsOnChanges method respond when (re)sets state. The states' ngxsOnChanges methods are invoked in a topological sorted order going from parent to child. The first parameter is the NgxsSimpleChange object of current and previous state.

```TS
export interface ZooStateModel {
animals: string[];
}

@State<ZooStateModel>({
name: 'zoo',
defaults: {
animals: []
}
})
export class ZooState implements NgxsOnChanges {
ngxsOnChanges(change: NgxsSimpleChange) {
console.log('prev state', change.previousValue);
console.log('next state', change.currentValue);
}
}
```

## `ngxsOnInit`

If a state implements the `NgxsOnInit` interface, its `ngxsOnInit` method will be invoked after
Expand Down Expand Up @@ -51,6 +74,16 @@ export class ZooState implements NgxsAfterBootstrap {
}
```

## Lifecycle sequence

After creating the state by calling its constructor, NGXS calls the lifecycle hook methods in the following sequence at specific moments:

| Hook | Purpose and Timing |
| -------------------- | ------------------------------------------------------------------------- |
| ngxsOnChanges() | Called before ngxsOnInit() and whenever state change. |
| ngxsOnInit() | Called once, after the first ngxsOnChanges(). |
| ngxsAfterBootstrap() | Called once, after the root view and all its children have been rendered. |

## Feature Modules Order of Imports

If you have feature modules they need to be imported after the root module:
Expand Down
21 changes: 19 additions & 2 deletions packages/store/src/internal/internals.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { PlainObjectOf, StateClass } from '@ngxs/store/internals';
import { Observable } from 'rxjs';

import {
META_KEY,
META_OPTIONS_KEY,
NgxsConfig,
NgxsLifeCycle,
NgxsSimpleChange,
SELECTOR_META_KEY,
StoreOptions
} from '../symbols';
import { ActionHandlerMetaData } from '../actions/symbols';

import { PlainObjectOf, StateClass } from '@ngxs/store/internals';
import { getValue } from '../utils/utils';

function asReadonly<T>(value: T): Readonly<T> {
return value;
Expand Down Expand Up @@ -72,6 +74,11 @@ export interface StatesAndDefaults {

export type Callback<T = any, V = any> = (...args: V[]) => T;

export interface RootStateDiff<T> {
currentAppState: T;
newAppState: T;
}

/**
* Ensures metadata is attached to the class and returns it.
*
Expand Down Expand Up @@ -366,3 +373,13 @@ export function topologicalSort(graph: StateKeyGraph): string[] {
export function isObject(obj: any) {
return (typeof obj === 'object' && obj !== null) || typeof obj === 'function';
}

export function getStateDiffChanges<T>(
metadata: MappedStore,
diff: RootStateDiff<T>
): NgxsSimpleChange {
const instance: NgxsLifeCycle = metadata.instance;
const previousValue: T = getValue(diff.currentAppState, metadata.depth);
const currentValue: T = getValue(diff.newAppState, metadata.depth);
return new NgxsSimpleChange(previousValue, currentValue, !instance.isFirstChange);
}
48 changes: 32 additions & 16 deletions packages/store/src/internal/lifecycle-state-manager.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Injectable } from '@angular/core';
import { NgxsBootstrapper } from '@ngxs/store/internals';
import { filter, tap, mergeMap } from 'rxjs/operators';
import { NgxsBootstrapper, PlainObject } from '@ngxs/store/internals';
import { filter, mergeMap, tap } from 'rxjs/operators';

import { StateContextFactory } from './state-context-factory';
import { InternalStateOperations } from './state-operations';
import { MappedStore, StatesAndDefaults } from './internals';
import { LifecycleHooks, NgxsLifeCycle } from '../symbols';
import { getStateDiffChanges, MappedStore, StatesAndDefaults } from './internals';
import { NgxsLifeCycle, NgxsSimpleChange, StateContext } from '../symbols';

@Injectable()
export class LifecycleStateManager {
Expand All @@ -25,33 +25,49 @@ export class LifecycleStateManager {
mergeMap(() => this.bootstrapper.appBootstrapped$),
filter(appBootstrapped => !!appBootstrapped)
)
.subscribe(() => {
this.invokeBootstrap(results!.states);
});
.subscribe(() => this.invokeBootstrap(results!.states));
}

/**
* Invoke the init function on the states.
*/
invokeInit(stateMetadatas: MappedStore[]): void {
this.invokeLifecycleHooks(stateMetadatas, LifecycleHooks.NgxsOnInit);
for (const metadata of stateMetadatas) {
const instance: NgxsLifeCycle = metadata.instance;

if (instance.ngxsOnChanges) {
markwhitfeld marked this conversation as resolved.
Show resolved Hide resolved
const currentAppState: PlainObject = {};
const newAppState: PlainObject = this.internalStateOperations
.getRootStateOperations()
.getState();

const firstDiffChange: NgxsSimpleChange = getStateDiffChanges(metadata, {
currentAppState,
newAppState
});

instance.ngxsOnChanges(firstDiffChange);
}

if (instance.ngxsOnInit) {
instance.ngxsOnInit(this.getStateContext(metadata));
}
}
}

/**
* Invoke the bootstrap function on the states.
*/
invokeBootstrap(stateMetadatas: MappedStore[]) {
this.invokeLifecycleHooks(stateMetadatas, LifecycleHooks.NgxsAfterBootstrap);
}

private invokeLifecycleHooks(stateMetadatas: MappedStore[], hook: LifecycleHooks): void {
for (const metadata of stateMetadatas) {
const instance: NgxsLifeCycle = metadata.instance;

if (instance[hook]) {
const stateContext = this.stateContextFactory.createStateContext(metadata);
instance[hook]!(stateContext);
if (instance.ngxsAfterBootstrap) {
instance.ngxsAfterBootstrap(this.getStateContext(metadata));
}
}
}

private getStateContext(metadata: MappedStore): StateContext<any> {
return this.stateContextFactory.createStateContext(metadata);
}
}
16 changes: 14 additions & 2 deletions packages/store/src/internal/state-context-factory.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';

import { StateContext, StateOperator } from '../symbols';
import { MappedStore } from '../internal/internals';
import { NgxsLifeCycle, NgxsSimpleChange, StateContext, StateOperator } from '../symbols';
import { getStateDiffChanges, MappedStore } from '../internal/internals';
import { setValue, getValue } from '../utils/utils';
import { InternalStateOperations } from '../internal/state-operations';
import { simplePatch } from './state-operators';
Expand All @@ -27,6 +27,18 @@ export class StateContextFactory {

function setStateValue(currentAppState: any, newValue: T): any {
const newAppState = setValue(currentAppState, metadata.depth, newValue);

if (metadata.instance && metadata.instance.ngxsOnChanges) {
const instance: NgxsLifeCycle = metadata.instance;
splincode marked this conversation as resolved.
Show resolved Hide resolved
instance.isFirstChange = true;
splincode marked this conversation as resolved.
Show resolved Hide resolved
const change: NgxsSimpleChange = getStateDiffChanges<T>(metadata, {
currentAppState,
newAppState
});

instance.ngxsOnChanges!(change);
splincode marked this conversation as resolved.
Show resolved Hide resolved
}

root.setState(newAppState);
return newAppState;
// In doing this refactoring I noticed that there is a 'bug' where the
Expand Down
4 changes: 3 additions & 1 deletion packages/store/src/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ export {
StateOperator,
NgxsOnInit,
NgxsAfterBootstrap,
NgxsModuleOptions
NgxsOnChanges,
NgxsModuleOptions,
NgxsSimpleChange
splincode marked this conversation as resolved.
Show resolved Hide resolved
} from './symbols';
export { Selector } from './decorators/selector';
export { getActionTypeFromInstance, actionMatcher } from './utils/utils';
Expand Down
18 changes: 14 additions & 4 deletions packages/store/src/symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ export const META_KEY = 'NGXS_META';
export const META_OPTIONS_KEY = 'NGXS_OPTIONS_META';
export const SELECTOR_META_KEY = 'NGXS_SELECTOR_META';

export type NgxsLifeCycle = Partial<NgxsOnInit> & Partial<NgxsAfterBootstrap>;
export type NgxsLifeCycle = Partial<NgxsOnChanges> &
Partial<NgxsOnInit> &
Partial<NgxsAfterBootstrap>;

export type NgxsPluginFn = (state: any, mutation: any, next: NgxsNextPluginFn) => any;

/**
Expand Down Expand Up @@ -132,9 +135,8 @@ export interface StoreOptions<T> {
children?: any[];
}

export const enum LifecycleHooks {
splincode marked this conversation as resolved.
Show resolved Hide resolved
NgxsOnInit = 'ngxsOnInit',
NgxsAfterBootstrap = 'ngxsAfterBootstrap'
export class NgxsSimpleChange<T = any> {
constructor(public previousValue: T, public currentValue: T, public firstChange: boolean) {}
splincode marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand All @@ -144,6 +146,14 @@ export interface NgxsOnInit {
ngxsOnInit(ctx?: StateContext<any>): void | any;
}

/**
* On change interface
*/
export interface NgxsOnChanges {
isFirstChange?: boolean;
splincode marked this conversation as resolved.
Show resolved Hide resolved
ngxsOnChanges(change?: NgxsSimpleChange): void;
splincode marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* After bootstrap interface
*/
Expand Down
Loading