diff --git a/integration/app/app.component.scss b/integration/app/app.component.scss index 771b56a77..a56de874e 100644 --- a/integration/app/app.component.scss +++ b/integration/app/app.component.scss @@ -57,3 +57,7 @@ button { margin: 0 auto; border-top: solid 1px #ddd; } + +.menu a { + margin-right: 10px; +} diff --git a/integration/app/app.component.ts b/integration/app/app.component.ts index 179d633dc..1091d0add 100644 --- a/integration/app/app.component.ts +++ b/integration/app/app.component.ts @@ -3,7 +3,8 @@ import { Store, Select } from '@ngxs/store'; import { Observable } from 'rxjs'; import { FormBuilder, FormArray } from '@angular/forms'; -import { AddTodo, RemoveTodo, TodoState, SetPrefix, TodosState, LoadData } from './todo.state'; +import { AddTodo, RemoveTodo, TodoState } from './store/todo.state'; +import { LoadData, SetPrefix, TodosState } from './store/todos.state'; @Component({ selector: 'app-root', @@ -47,9 +48,12 @@ import { AddTodo, RemoveTodo, TodoState, SetPrefix, TodosState, LoadData } from encapsulation: ViewEncapsulation.None }) export class AppComponent { - @Select(TodoState) todos$: Observable; - @Select(TodoState.pandas) pandas$: Observable; - @Select(TodosState.pizza) pizza$: Observable; + @Select(TodoState) + todos$: Observable; + @Select(TodoState.pandas) + pandas$: Observable; + @Select(TodosState.pizza) + pizza$: Observable; allExtras = [ { name: 'cheese', selected: false }, diff --git a/integration/app/app.module.ts b/integration/app/app.module.ts index 041abde55..75e02b1b8 100644 --- a/integration/app/app.module.ts +++ b/integration/app/app.module.ts @@ -7,20 +7,19 @@ import { NgxsStoragePluginModule } from '@ngxs/storage-plugin'; import { NgxsReduxDevtoolsPluginModule } from '@ngxs/devtools-plugin'; import { NgxsFormPluginModule } from '@ngxs/form-plugin'; import { NgxsLoggerPluginModule } from '@ngxs/logger-plugin'; - import { environment } from '../environments/environment'; import { AppComponent } from './app.component'; +import { MenuComponent } from './menu.component'; import { routes } from './app.routes'; -import { states } from './app.state'; @NgModule({ - declarations: [AppComponent], + declarations: [AppComponent, MenuComponent], imports: [ BrowserModule, FormsModule, ReactiveFormsModule, - RouterModule.forRoot(routes), - NgxsModule.forRoot(states), + RouterModule.forRoot(routes, { useHash: true }), + NgxsModule.forRoot(), NgxsStoragePluginModule.forRoot({ key: ['todos.todo'] }), diff --git a/integration/app/app.routes.ts b/integration/app/app.routes.ts index e877cc991..f1bc8b4a9 100644 --- a/integration/app/app.routes.ts +++ b/integration/app/app.routes.ts @@ -1,7 +1,8 @@ import { Routes } from '@angular/router'; +import { MenuComponent } from './menu.component'; export const routes: Routes = [ - { path: '', pathMatch: 'full', redirectTo: '/list' }, + { path: '', component: MenuComponent }, { path: 'list', loadChildren: './list/list.module#ListModule' }, { path: 'detail', loadChildren: './detail/detail.module#DetailModule' } ]; diff --git a/integration/app/app.state.ts b/integration/app/app.state.ts deleted file mode 100644 index a0e927ad9..000000000 --- a/integration/app/app.state.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { TodoStateModel, TodosState, TodoState } from './todo.state'; - -export interface AppState { - todos: TodoStateModel; -} - -export const states = [TodosState, TodoState]; diff --git a/integration/app/detail/detail.actions.ts b/integration/app/detail/detail.actions.ts new file mode 100644 index 000000000..77629c12b --- /dev/null +++ b/integration/app/detail/detail.actions.ts @@ -0,0 +1,4 @@ +export class DetailFooActions { + public static readonly type: string = '[DetailActions] description'; + constructor(public foo: boolean) {} +} diff --git a/integration/app/detail/detail.component.ts b/integration/app/detail/detail.component.ts index 34c75ef44..c3dd1dfe7 100644 --- a/integration/app/detail/detail.component.ts +++ b/integration/app/detail/detail.component.ts @@ -1,9 +1,23 @@ import { Component } from '@angular/core'; +import { Select, Store } from '@ngxs/store'; +import { DetailFooActions } from './detail.actions'; +import { DetailState, DetailStateModel } from './detail.state'; +import { Observable } from 'rxjs'; @Component({ selector: 'app-detail', template: ` - List + List {{ detail$ | async | json }} +

` }) -export class DetailComponent {} +export class DetailComponent { + @Select(DetailState) + detail$: Observable; + + constructor(private store: Store) {} + + public updateFoo() { + this.store.dispatch(new DetailFooActions(false)); + } +} diff --git a/integration/app/detail/detail.module.ts b/integration/app/detail/detail.module.ts index cf21426de..1c2aa6f28 100644 --- a/integration/app/detail/detail.module.ts +++ b/integration/app/detail/detail.module.ts @@ -3,11 +3,10 @@ import { DetailComponent } from './detail.component'; import { CommonModule } from '@angular/common'; import { RouterModule } from '@angular/router'; import { NgxsModule } from '@ngxs/store'; -import { DetailState } from './detail.state'; import { routes } from './detail.routes'; @NgModule({ - imports: [CommonModule, RouterModule.forChild(routes), NgxsModule.forFeature([DetailState])], + imports: [CommonModule, RouterModule.forChild(routes), NgxsModule.forFeature()], declarations: [DetailComponent] }) export class DetailModule {} diff --git a/integration/app/detail/detail.state.ts b/integration/app/detail/detail.state.ts index e3d51a354..c4484e21b 100644 --- a/integration/app/detail/detail.state.ts +++ b/integration/app/detail/detail.state.ts @@ -1,7 +1,19 @@ -import { State } from '@ngxs/store'; +import { Action, State, StateContext } from '@ngxs/store'; +import { DetailModule } from './detail.module'; +import { DetailFooActions } from './detail.actions'; -@State({ +export interface DetailStateModel { + foo: boolean; +} + +@State({ name: 'detail', - defaults: { foo: true } + defaults: { foo: true }, + providedIn: DetailModule }) -export class DetailState {} +export class DetailState { + @Action(DetailFooActions) + changeFoo(ctx: StateContext, { foo }: DetailFooActions) { + ctx.setState({ foo }); + } +} diff --git a/integration/app/list/list.module.ts b/integration/app/list/list.module.ts index 6e002495f..b23b2cd08 100644 --- a/integration/app/list/list.module.ts +++ b/integration/app/list/list.module.ts @@ -3,11 +3,10 @@ import { ListComponent } from './list.component'; import { CommonModule } from '@angular/common'; import { RouterModule } from '@angular/router'; import { NgxsModule } from '@ngxs/store'; -import { ListState } from './list.state'; import { routes } from './list.routes'; @NgModule({ - imports: [CommonModule, RouterModule.forChild(routes), NgxsModule.forFeature([ListState])], + imports: [CommonModule, RouterModule.forChild(routes), NgxsModule.forFeature()], declarations: [ListComponent] }) export class ListModule {} diff --git a/integration/app/list/list.state.ts b/integration/app/list/list.state.ts index d70e83688..2c21afc93 100644 --- a/integration/app/list/list.state.ts +++ b/integration/app/list/list.state.ts @@ -1,8 +1,10 @@ import { State, Selector } from '@ngxs/store'; +import { ListModule } from './list.module'; @State({ name: 'list', - defaults: ['foo'] + defaults: ['foo'], + providedIn: ListModule }) export class ListState { @Selector() diff --git a/integration/app/menu.component.ts b/integration/app/menu.component.ts new file mode 100644 index 000000000..56b3dcd65 --- /dev/null +++ b/integration/app/menu.component.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-menu', + template: ` + + ` +}) +export class MenuComponent {} diff --git a/integration/app/store/todo.state.ts b/integration/app/store/todo.state.ts new file mode 100644 index 000000000..7ac3b1d6d --- /dev/null +++ b/integration/app/store/todo.state.ts @@ -0,0 +1,34 @@ +import { Action, Selector, State, StateContext } from '@ngxs/store'; + +export class AddTodo { + static type = 'AddTodo'; + + constructor(public readonly payload: string) {} +} + +export class RemoveTodo { + static type = 'RemoveTodo'; + + constructor(public readonly payload: number) {} +} + +@State({ + name: 'todo', + defaults: [] +}) +export class TodoState { + @Selector() + static pandas(state: string[]) { + return state.filter(s => s.indexOf('panda') > -1); + } + + @Action(AddTodo) + addTodo({ getState, setState }: StateContext, { payload }: AddTodo) { + setState([...getState(), payload]); + } + + @Action(RemoveTodo) + removeTodo({ getState, setState }: StateContext, { payload }: RemoveTodo) { + setState(getState().filter((_, i) => i !== payload)); + } +} diff --git a/integration/app/todo.state.ts b/integration/app/store/todos.state.ts similarity index 57% rename from integration/app/todo.state.ts rename to integration/app/store/todos.state.ts index cfeecd309..11340fe20 100644 --- a/integration/app/todo.state.ts +++ b/integration/app/store/todos.state.ts @@ -1,45 +1,13 @@ -import { State, Action, StateContext, Selector } from '@ngxs/store'; +import { Action, Selector, State } from '@ngxs/store'; import { of } from 'rxjs'; import { tap } from 'rxjs/operators'; - -export class AddTodo { - static type = 'AddTodo'; - - constructor(public readonly payload: string) {} -} - -export class RemoveTodo { - static type = 'RemoveTodo'; - - constructor(public readonly payload: number) {} -} +import { TodoState } from './todo.state'; export class TodoStateModel { todo: string[]; pizza: { model: any }; } -@State({ - name: 'todo', - defaults: [] -}) -export class TodoState { - @Selector() - static pandas(state: string[]) { - return state.filter(s => s.indexOf('panda') > -1); - } - - @Action(AddTodo) - addTodo({ getState, setState }: StateContext, { payload }: AddTodo) { - setState([...getState(), payload]); - } - - @Action(RemoveTodo) - removeTodo({ getState, setState }: StateContext, { payload }: RemoveTodo) { - setState(getState().filter((_, i) => i !== payload)); - } -} - export class SetPrefix { static type = 'SetPrefix'; } @@ -54,7 +22,8 @@ export class LoadData { todo: [], pizza: { model: undefined } }, - children: [TodoState] + children: [TodoState], + providedIn: 'ngxsRoot' }) export class TodosState { @Selector() diff --git a/packages/store/src/decorators/state.ts b/packages/store/src/decorators/state.ts index e193d0af3..8b742f4bc 100644 --- a/packages/store/src/decorators/state.ts +++ b/packages/store/src/decorators/state.ts @@ -1,5 +1,8 @@ +import { Type } from '@angular/core'; + import { ensureStoreMetadata } from '../internal/internals'; -import { StoreOptions, META_KEY } from '../symbols'; +import { META_KEY, StoreOptions } from '../symbols'; +import { NgxsStateProvidersModule } from '../internal/state-providers/state-providers.module'; const stateNameRegex = new RegExp('^[a-zA-Z0-9]+$'); @@ -31,6 +34,15 @@ export function State(options: StoreOptions) { meta.defaults = options.defaults; meta.name = options.name; + if (options.providedIn) { + const provideIn: string | Type = options.providedIn; + const type: Type = typeof provideIn !== 'string' ? provideIn : null; + const children = options.children || []; + const states = [target, ...children]; + states.forEach((state: Type) => NgxsStateProvidersModule.provideInNgxsModule(state, type)); + NgxsStateProvidersModule.defineStatesByProvideIn(provideIn, states); + } + if (!options.name) { throw new Error(`States must register a 'name' property`); } diff --git a/packages/store/src/internal/state-providers/state-providers.interfaces.ts b/packages/store/src/internal/state-providers/state-providers.interfaces.ts new file mode 100644 index 000000000..98221f168 --- /dev/null +++ b/packages/store/src/internal/state-providers/state-providers.interfaces.ts @@ -0,0 +1,13 @@ +import { Type } from '@angular/core'; + +export type NgxsStateType = Type; + +export enum NgxsProvidedIn { + root = 'ngxsRoot', + feature = 'ngxsFeature' +} + +export interface NgxsProvides { + ngxsRoot: NgxsStateType[]; + ngxsFeature: NgxsStateType[]; +} diff --git a/packages/store/src/internal/state-providers/state-providers.module.ts b/packages/store/src/internal/state-providers/state-providers.module.ts new file mode 100644 index 000000000..c69868d52 --- /dev/null +++ b/packages/store/src/internal/state-providers/state-providers.module.ts @@ -0,0 +1,37 @@ +import { Injectable, NgModule, Type } from '@angular/core'; +import { NgxsProvidedIn, NgxsProvides, NgxsStateType } from './state-providers.interfaces'; + +@NgModule() +export class NgxsStateProvidersModule { + public static states: NgxsProvides = { + ngxsRoot: [], + ngxsFeature: [] + }; + + public static provideInNgxsModule(target: NgxsStateType, type: Type = null): void { + const { ngxsRoot, ngxsFeature } = NgxsStateProvidersModule.states; + const firstInitialize = ![...ngxsRoot, ...ngxsFeature].includes(target); + + if (firstInitialize) { + Injectable({ providedIn: type || NgxsStateProvidersModule })(target); + } + } + + public static defineStatesByProvideIn(provideIn: string | Type, states: NgxsStateType[]): void { + const stateSourceKey = typeof provideIn === 'string' ? provideIn : NgxsProvidedIn.feature; + const sources = NgxsStateProvidersModule.states[stateSourceKey] || []; + sources.push(...states); + } + + public static filterWithoutDuplicate(states: NgxsStateType[], sources: NgxsStateType[] = []): NgxsStateType[] { + return states.filter((value, index) => { + const nonDuplicateSource = states.indexOf(value) === index; + const nonDuplicateTarget = !sources.includes(value); + return value && nonDuplicateSource && nonDuplicateTarget; + }); + } + + public static flattenedUniqueStates(entry: NgxsStateType[], compare: NgxsStateType[]) { + return this.filterWithoutDuplicate([...entry, ...compare]); + } +} diff --git a/packages/store/src/module.ts b/packages/store/src/module.ts index 0a862e365..f5c0344df 100644 --- a/packages/store/src/module.ts +++ b/packages/store/src/module.ts @@ -11,6 +11,7 @@ import { SelectFactory } from './decorators/select'; import { StateStream } from './internal/state-stream'; import { PluginManager } from './plugin-manager'; import { InitState, UpdateState } from './actions/actions'; +import { NgxsStateProvidersModule } from './internal/state-providers/state-providers.module'; /** * Root module @@ -25,8 +26,12 @@ export class NgxsRootModule { select: SelectFactory, @Optional() @Inject(ROOT_STATE_TOKEN) - states: any[] + entryStates: any[] ) { + // Concat entry states with state providers + const ngxsRootStates = NgxsStateProvidersModule.states.ngxsRoot; + const states = NgxsStateProvidersModule.flattenedUniqueStates(entryStates, ngxsRootStates); + // add stores to the state graph and return their defaults const results = factory.addAndReturnDefaults(states); @@ -63,14 +68,18 @@ export class NgxsFeatureModule { factory: StateFactory, @Optional() @Inject(FEATURE_STATE_TOKEN) - states: any[][] + entryStates: any[][] ) { // Since FEATURE_STATE_TOKEN is a multi token, we need to // flatten it [[Feature1State, Feature2State], [Feature3State]] - const flattenedStates = ([] as any[]).concat(...states); + const flattenedStates = ([] as any[]).concat(...entryStates); + + // Concat entry states with state providers + const ngxsFeatureStates = NgxsStateProvidersModule.states.ngxsFeature; + const states = NgxsStateProvidersModule.flattenedUniqueStates(flattenedStates, ngxsFeatureStates); // add stores to the state graph and return their defaults - const results = factory.addAndReturnDefaults(flattenedStates); + const results = factory.addAndReturnDefaults(states); const stateOperations = internalStateOperations.getRootStateOperations(); if (results) { @@ -101,7 +110,9 @@ export const ROOT_OPTIONS = new InjectionToken('ROOT_OPTIONS'); /** * Ngxs Module */ -@NgModule({}) +@NgModule({ + imports: [NgxsStateProvidersModule] +}) export class NgxsModule { /** * Root module factory @@ -142,7 +153,7 @@ export class NgxsModule { /** * Feature module factory */ - static forFeature(states: any[]): ModuleWithProviders { + static forFeature(states: any[] = []): ModuleWithProviders { return { ngModule: NgxsFeatureModule, providers: [ diff --git a/packages/store/src/symbols.ts b/packages/store/src/symbols.ts index 5782f2d1b..14cce8993 100644 --- a/packages/store/src/symbols.ts +++ b/packages/store/src/symbols.ts @@ -1,4 +1,4 @@ -import { InjectionToken } from '@angular/core'; +import { InjectionToken, Type } from '@angular/core'; import { Observable } from 'rxjs'; export const ROOT_STATE_TOKEN = new InjectionToken('ROOT_STATE_TOKEN'); @@ -91,6 +91,11 @@ export interface StoreOptions { * Sub states for the given state. */ children?: any[]; + + /** + * Define states in your root or feature module + * */ + providedIn?: 'ngxsRoot' | Type | null; } /**