diff --git a/packages/store/action.ts b/packages/store/action.ts index ef0975d..a10476b 100644 --- a/packages/store/action.ts +++ b/packages/store/action.ts @@ -43,7 +43,7 @@ export function Action( // support string for type if (typeof action === 'string') { action = { - type: action as string + type: action }; } diff --git a/packages/store/action/action-definition.ts b/packages/store/action/action-definition.ts index 07f9d94..69f84dd 100644 --- a/packages/store/action/action-definition.ts +++ b/packages/store/action/action-definition.ts @@ -1,5 +1,5 @@ import { ACTION_ID_PREFIX } from '../types'; -type ExtractTypeToPayload = T extends [...infer A, never] ? ExtractTypeToPayload : T extends Array ? T : never; +export type ExtractTypeToPayload = T extends [...infer A, never] ? ExtractTypeToPayload : T extends Array ? T : never; export interface ActionRef { type: string; diff --git a/packages/store/action/action-group-definition.ts b/packages/store/action/action-group-definition.ts new file mode 100644 index 0000000..202b9d0 --- /dev/null +++ b/packages/store/action/action-group-definition.ts @@ -0,0 +1,30 @@ +import { ActionCreator, ExtractTypeToPayload, defineAction } from './action-definition'; + +const payloadSymbol = '__payloadSymbol'; + +type PayloadRef = { [payloadSymbol]: true }; + +type ExtractActionCreators = { [key in keyof T]?: ActionCreator }; + +function isPayloadRef(variable: unknown): variable is PayloadRef { + return typeof variable === 'object' && payloadSymbol in variable && variable[payloadSymbol] === true; +} + +export function payload() { + return { + [payloadSymbol]: true + } as PayloadRef & { payload: ExtractTypeToPayload<[T1, T2, T3, T4]> }; +} + +export function defineActions>(groupName: string, actions: T) { + let result: ExtractActionCreators = {}; + for (const key in actions) { + if (isPayloadRef(actions[key])) { + const type = `${groupName}_${key}`; + result[key] = defineAction(type) as ActionCreator; + } else { + throw new Error(`${key} is not a PayloadRef, please use payload function define it.`); + } + } + return result; +} diff --git a/packages/store/public-api.ts b/packages/store/public-api.ts index 4ff591a..30c21ee 100644 --- a/packages/store/public-api.ts +++ b/packages/store/public-api.ts @@ -7,6 +7,7 @@ export * from './references'; export * from './store'; export * from './store-factory'; export * from './action/action-definition'; +export { defineActions, payload } from './action/action-group-definition'; export { Dispatcher, dispatch } from './dispatcher'; export { getObjectValue, setObjectValue } from './utils'; export { Id, PaginationInfo, StoreOptions } from './types'; diff --git a/packages/store/store/examples/pages/actions.ts b/packages/store/store/examples/pages/actions.ts index 530b12e..d0951ae 100644 --- a/packages/store/store/examples/pages/actions.ts +++ b/packages/store/store/examples/pages/actions.ts @@ -1,5 +1,10 @@ -import { defineAction } from '@tethys/store'; +import { defineAction, defineActions, payload } from '@tethys/store'; export const updateTitle = defineAction('updateTitle'); export const updateContent = defineAction('updateContent'); + +export const groupActions = defineActions('page', { + updateTitle: payload(), + updateContent: payload() +}); diff --git a/packages/store/store/examples/pages/page-list.store.ts b/packages/store/store/examples/pages/page-list.store.ts index e01387e..6369855 100644 --- a/packages/store/store/examples/pages/page-list.store.ts +++ b/packages/store/store/examples/pages/page-list.store.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { Action, EntityState, EntityStore } from '@tethys/store'; import { of } from 'rxjs'; import { tap } from 'rxjs/operators'; -import { updateTitle } from './actions'; +import { groupActions } from './actions'; export interface Page { _id: string; @@ -34,7 +34,7 @@ export class PagesStore extends EntityStore { ); } - @Action(updateTitle) + @Action(groupActions.updateTitle) pureUpdateTitle(_id: string, payload: { title: string }) { this.update(_id, { title: payload.title }); } diff --git a/packages/store/store/examples/pages/page.store.ts b/packages/store/store/examples/pages/page.store.ts index 2b5bb8e..cae5dbb 100644 --- a/packages/store/store/examples/pages/page.store.ts +++ b/packages/store/store/examples/pages/page.store.ts @@ -2,7 +2,7 @@ import { Injectable, inject } from '@angular/core'; import { Action, dispatch, Store } from '@tethys/store'; import { of } from 'rxjs'; import { tap } from 'rxjs/operators'; -import { updateContent, updateTitle } from './actions'; +import { groupActions, updateContent, updateTitle } from './actions'; interface PageDetailState { detail: { _id: string; title: string; content: string }; @@ -18,7 +18,7 @@ export class PageDetailStore extends Store { super({}); } - @Action(updateTitle) + @Action(groupActions.updateTitle) pureUpdateTitle(_id: string, payload: { title: string }) { if (_id === this.snapshot.detail._id) { this.update({ @@ -63,7 +63,7 @@ export class PageDetailStore extends Store { updateTitle() { return of(true).pipe( tap(() => { - dispatch(updateTitle('1', { title: 'New First Page Title' })); + dispatch(groupActions.updateTitle('1', { title: 'New First Page Title' })); }) ); } diff --git a/packages/store/test/dispatch-actions.spec.ts b/packages/store/test/dispatch-actions.spec.ts index e84709d..0ef9a3d 100644 --- a/packages/store/test/dispatch-actions.spec.ts +++ b/packages/store/test/dispatch-actions.spec.ts @@ -1,26 +1,33 @@ import { Injectable } from '@angular/core'; -import { Action, dispatch, Store, defineAction, EntityState, EntityStore } from '@tethys/store'; +import { Action, dispatch, Store, defineAction, EntityState, EntityStore, defineActions, payload } from '@tethys/store'; import { TestBed } from '@angular/core/testing'; +import { produce } from '@tethys/cdk'; -export const updateTitle = defineAction('updateTitle'); +const updateTitle = defineAction('updateTitle'); -export const updateContent = defineAction('updateContent'); +const updateContent = defineAction('updateContent'); -export const upvote = defineAction('like page'); +const upvote = defineAction('like page'); -export const likePage = defineAction('like page'); +const likePage = defineAction('like page'); + +const commentActions = defineActions('PAGE', { + add: payload(), + delete: payload() +}); interface Page { _id: string; title: string; voteCount?: number; + commentCount?: number; } interface PageDetailState { - detail: { _id: string; title: string; content: string; like?: boolean }; + detail: { _id: string; title: string; content: string; like?: boolean; comments?: { _id: string; content: string }[] }; } -const pageDetail = { _id: '1', title: 'First Page Title', content: 'First Page Detail Content' }; +const pageDetail = { _id: '1', title: 'First Page Title', content: 'First Page Detail Content', comments: [] }; const pageList = [ { _id: '1', title: 'First Page Title' }, @@ -39,6 +46,10 @@ const likePageSpy = jasmine.createSpy('like page'); const upvoteSpy = jasmine.createSpy('upvote'); +const detailAddCommentSpy = jasmine.createSpy('detail add comment'); + +const listAddCommentSpy = jasmine.createSpy('list add comment'); + @Injectable({ providedIn: 'root' }) export class PageDetailStore extends Store { static detailSelector(state: PageDetailState) { @@ -75,6 +86,17 @@ export class PageDetailStore extends Store { } } + @Action(commentActions.add) + pureAddComment(_id: string, payload: { _id: string; content: string }) { + detailAddCommentSpy(_id, payload); + this.update({ + detail: { + ...this.snapshot.detail, + comments: produce(this.snapshot.detail.comments).add(payload) + } + }); + } + @Action(likePage) pureGiveALike(_id: string) { likePageSpy(_id); @@ -87,6 +109,10 @@ export class PageDetailStore extends Store { dispatchUpdateContent(_id: string, payload: { content: string }) { dispatch(updateContent(_id, payload)); } + + dispatchAddComment(_id: string, payload: { _id: string; content: string }) { + dispatch(commentActions.add(_id, payload)); + } } interface PagesState extends EntityState {} @@ -112,6 +138,15 @@ export class PagesStore extends EntityStore { })); } + @Action(commentActions.add) + pureAddComment(_id: string) { + listAddCommentSpy(_id); + this.update(_id, (entity) => ({ + ...entity, + commentCount: (entity.commentCount || 0) + 1 + })); + } + dispatchUpvote(id: string) { dispatch(upvote(id)); } @@ -161,4 +196,17 @@ describe('#dispatchActions', () => { expect(entity.voteCount).toEqual(1); }); }); + + describe('#dispatch group action', () => { + it('should action functions invoke when dispatch', () => { + detailStore = TestBed.inject(PageDetailStore); + listStore = TestBed.inject(PagesStore); + detailStore.dispatchAddComment('1', { _id: 'comment_1', content: 'A New Comment' }); + expect(detailAddCommentSpy).toHaveBeenCalledWith('1', { _id: 'comment_1', content: 'A New Comment' }); + expect(listAddCommentSpy).toHaveBeenCalledWith('1'); + expect(detailStore.snapshot.detail.comments[0]).toEqual({ _id: 'comment_1', content: 'A New Comment' }); + const entity = listStore.snapshot.entities.find((item) => item._id === '1'); + expect(entity.commentCount).toEqual(1); + }); + }); });