Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 47 additions & 5 deletions packages/store/src/decorators/state.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,44 @@
import { inject } from '@angular/core';
import {
傻StateClass,
傻META_KEY,
傻META_OPTIONS_KEY,
傻MetaDataModel,
傻StateClass,
傻StateClassInternal,
傻StoreOptions,
傻ensureStoreMetadata
} from '@ngxs/store/internals';
import { Observable } from 'rxjs';

import { StateOperator } from '../symbols';
import { ensureStateNameIsValid } from '../utils/store-validators';
import { StateContextFactory } from '../internal/state-context-factory';

interface MutateMetaOptions<T> {
meta: 傻MetaDataModel;
inheritedStateClass: 傻StateClassInternal;
optionsWithInheritance: 傻StoreOptions<T>;
}

interface BaseState<T> {
getState(): T;
setState(value: T): void;
patchState(value: Partial<T>): void;
dispatch(actions: any | any[]): Observable<void>;
}

interface StateDecorator<T> {
(target: 傻StateClass): void;
new (): BaseState<T>;
}

/**
* Decorates a class with ngxs state information.
*/
export function State<T>(options: 傻StoreOptions<T>) {
return (target: 傻StateClass): void => {
const stateClass: 傻StateClassInternal = target;
export function State<T>(options: 傻StoreOptions<T>): StateDecorator<T> {
return function (this: BaseState<T>) {
// eslint-disable-next-line prefer-rest-params
const stateClass = (new.target || arguments[0]) as unknown as 傻StateClassInternal;
const meta: 傻MetaDataModel = 傻ensureStoreMetadata(stateClass);
const inheritedStateClass: 傻StateClassInternal = Object.getPrototypeOf(stateClass);
const optionsWithInheritance: 傻StoreOptions<T> = getStateOptions(
Expand All @@ -30,7 +47,32 @@ export function State<T>(options: 傻StoreOptions<T>) {
);
mutateMetaData<T>({ meta, inheritedStateClass, optionsWithInheritance });
stateClass[傻META_OPTIONS_KEY] = optionsWithInheritance;
};

// If this function is being called as a constructor function.
if (new.target) {
const stateContextFactory = inject(StateContextFactory);

this.getState = () => {
const ctx = stateContextFactory.createStateContext<T>(meta.path!);
return ctx.getState();
};

this.setState = (value: T | StateOperator<T>) => {
const ctx = stateContextFactory.createStateContext<T>(meta.path!);
ctx.setState(value);
};

this.patchState = (value: Partial<T>) => {
const ctx = stateContextFactory.createStateContext<T>(meta.path!);
ctx.patchState(value);
};

this.dispatch = actions => {
const ctx = stateContextFactory.createStateContext<T>(meta.path!);
return ctx.dispatch(actions);
};
}
} as unknown as StateDecorator<T>;
}

function getStateOptions<T>(
Expand Down
2 changes: 1 addition & 1 deletion packages/store/src/internal/lifecycle-state-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,6 @@ export class LifecycleStateManager implements OnDestroy {
}

private _getStateContext(mappedStore: MappedStore): StateContext<any> {
return this._stateContextFactory.createStateContext(mappedStore);
return this._stateContextFactory.createStateContext(mappedStore.path);
}
}
12 changes: 6 additions & 6 deletions packages/store/src/internal/state-context-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ExistingState, StateOperator, isStateOperator } from '@ngxs/store/opera
import { Observable } from 'rxjs';

import { StateContext } from '../symbols';
import { MappedStore, StateOperations } from '../internal/internals';
import { StateOperations } from '../internal/internals';
import { InternalStateOperations } from '../internal/state-operations';
import { simplePatch } from './state-operators';

Expand All @@ -19,25 +19,25 @@ export class StateContextFactory {
/**
* Create the state context
*/
createStateContext<T>(mappedStore: MappedStore): StateContext<T> {
createStateContext<T>(path: string): StateContext<T> {
const root = this._internalStateOperations.getRootStateOperations();

return {
getState(): T {
const currentAppState = root.getState();
return getState(currentAppState, mappedStore.path);
return getState(currentAppState, path);
},
patchState(val: Partial<T>): void {
const currentAppState = root.getState();
const patchOperator = simplePatch<T>(val);
setStateFromOperator(root, currentAppState, patchOperator, mappedStore.path);
setStateFromOperator(root, currentAppState, patchOperator, path);
},
setState(val: T | StateOperator<T>): void {
const currentAppState = root.getState();
if (isStateOperator(val)) {
setStateFromOperator(root, currentAppState, val, mappedStore.path);
setStateFromOperator(root, currentAppState, val, path);
} else {
setStateValue(root, currentAppState, val, mappedStore.path);
setStateValue(root, currentAppState, val, path);
}
},
dispatch(actions: any | any[]): Observable<void> {
Expand Down
10 changes: 9 additions & 1 deletion packages/store/src/internal/state-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,14 @@ export class StateFactory implements OnDestroy {
ensureStatesAreDecorated(stateClasses);
}

// Just playing around with this because the metadata is not
// set until the constructor function is called (if `State` is used
// as a factory function rather than as a decorator, which returns
// a base state).
for (const stateClass of stateClasses) {
this._injector.get(stateClass);
}

const { newStates } = this.addToStatesMap(stateClasses);
if (!newStates.length) return [];

Expand Down Expand Up @@ -295,7 +303,7 @@ export class StateFactory implements OnDestroy {

if (actionMetas) {
for (const actionMeta of actionMetas) {
const stateContext = this._stateContextFactory.createStateContext(metadata);
const stateContext = this._stateContextFactory.createStateContext(metadata.path);
try {
let result = metadata.instance[actionMeta.fn](stateContext, action);

Expand Down
40 changes: 40 additions & 0 deletions packages/store/tests/state-extends.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Component, Injectable } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { Action, InitState, NgxsOnInit, provideStore, State, Store } from '@ngxs/store';
import { freshPlatform } from '@ngxs/store/internals/testing';

describe('State decorator returns a constructor function', () => {
@Component({ selector: 'app-root', template: '', standalone: true })
class TestComponent {}

it(
'should call an InitState action handler before the ngxsOnInit method on root module initialisation',
freshPlatform(async () => {
// Arrange
@Injectable()
class FooState
extends State<string[]>({ name: 'foo', defaults: [] })
implements NgxsOnInit
{
ngxsOnInit() {
this.setState([...this.getState(), 'onInit']);
}

@Action(InitState)
initState() {
this.setState([...this.getState(), 'initState']);
}
}

// Act
const { injector } = await bootstrapApplication(TestComponent, {
providers: [provideStore([FooState])]
});

const store = injector.get(Store);

// Assert
expect(store.snapshot().foo).toEqual(['initState', 'onInit']);
})
);
});