Skip to content

Commit

Permalink
feat: support dispatch multiple actions (#133)
Browse files Browse the repository at this point in the history
  • Loading branch information
MissLixf authored Dec 4, 2023
1 parent a58edd7 commit 784a87c
Show file tree
Hide file tree
Showing 13 changed files with 478 additions and 2 deletions.
19 changes: 17 additions & 2 deletions packages/store/action.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { findAndCreateStoreMetadata } from './utils';
import { InternalDispatcher } from './internals/dispatcher';
import { ActionCreator } from './action/action-definition';
import { isFunction, isObject, isUndefinedOrNull } from '@tethys/cdk';
// import { Observable, of, throwError } from 'rxjs';
// import { catchError, exhaustMap, shareReplay} from 'rxjs/operators';
// import { ActionContext, ActionStatus } from './actions-stream';
Expand All @@ -18,10 +20,17 @@ export interface DecoratorActionOptions {
payload?: any;
}

function isActionCreator(action: DecoratorActionOptions | string | ActionCreator): action is ActionCreator {
return isFunction(action) && isObject(action()) && !isUndefinedOrNull(action().type);
}

/**
* Decorates a method with a action information.
*/
export function Action(action?: DecoratorActionOptions | string) {

export function Action(
action?: DecoratorActionOptions | string | ActionCreator
): (target: Object, name: string, descriptor: TypedPropertyDescriptor<any>) => void {
return function (target: Object, name: string, descriptor: TypedPropertyDescriptor<any>) {
const metadata = findAndCreateStoreMetadata(target);

Expand All @@ -34,7 +43,13 @@ export function Action(action?: DecoratorActionOptions | string) {
// support string for type
if (typeof action === 'string') {
action = {
type: action
type: action as string
};
}

if (isActionCreator(action)) {
action = {
type: action.prototype.id
};
}
if (!action.type) {
Expand Down
38 changes: 38 additions & 0 deletions packages/store/action/action-definition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { ACTION_ID_PREFIX } from '../types';
type ExtractTypeToPayload<T> = T extends [...infer A, never] ? ExtractTypeToPayload<A> : T extends Array<any> ? T : never;

export interface ActionRef<T = any> {
type: string;
payload: ExtractTypeToPayload<T>;
}

export type ActionCreator<T = any> = (...payload: ExtractTypeToPayload<T>) => ActionRef<T>;

let internalMaxActionId = 0;

function internalCreateActionId(actionType: string): string {
const name = `${ACTION_ID_PREFIX}${actionType}_${internalMaxActionId}`;
internalMaxActionId++;
return name;
}

export function defineAction<T1 = never, T2 = never, T3 = never, T4 = never>(type: string): ActionCreator<[T1, T2, T3, T4]> {
const actionId = internalCreateActionId(type);
function creator(...payload: ExtractTypeToPayload<[T1, T2, T3, T4]>) {
return Object.defineProperty(
{
type: type,
payload: payload
},
'id',
{
value: actionId,
writable: false,
enumerable: false,
configurable: false
}
);
}
creator.prototype.id = actionId;
return creator;
}
39 changes: 39 additions & 0 deletions packages/store/dispatcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Injectable } from '@angular/core';
import { ActionRef } from './action/action-definition';
import { isObject, isUndefinedOrNull } from '@tethys/cdk';
import { InternalStoreFactory } from './internals/internal-store-factory';
import { InternalDispatcher } from './internals/dispatcher';
import { StoreMetaInfo } from './inner-types';
import { META_KEY } from './types';
@Injectable({
providedIn: 'root'
})
export class Dispatcher {
constructor() {}

public dispatch(action: ActionRef) {
dispatch(action);
}
}

export function dispatch(action: ActionRef) {
if (!isObject(action)) {
throw new TypeError(`Action must be object`);
} else if (isUndefinedOrNull(action.type)) {
throw new TypeError(`Action must have a type property`);
}
invokeActions(action);
}

function invokeActions(action: ActionRef) {
InternalStoreFactory.instance.getAllStores().forEach((store) => {
const meta = store[META_KEY] as StoreMetaInfo;
if (!meta || !meta.actions || !meta.actions[action['id']]) {
return;
}
const actionMeta = meta.actions[action['id']];
InternalDispatcher.instance.dispatch(store.defaultStoreInstanceId, actionMeta, () => {
return actionMeta.originalFn.call(store, ...action.payload);
});
});
}
2 changes: 2 additions & 0 deletions packages/store/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@ export * from './plugins/redux-devtools';
export * from './references';
export * from './store';
export * from './store-factory';
export * from './action/action-definition';
export { Dispatcher, dispatch } from './dispatcher';
export { getObjectValue, setObjectValue } from './utils';
export { Id, PaginationInfo, StoreOptions } from './types';
5 changes: 5 additions & 0 deletions packages/store/store/examples/pages/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { defineAction } from '@tethys/store';

export const updateTitle = defineAction<string, { title: string }>('updateTitle');

export const updateContent = defineAction<string, { content: string }>('updateContent');
4 changes: 4 additions & 0 deletions packages/store/store/examples/pages/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
title: Dispatch Actions
order: 11
---
41 changes: 41 additions & 0 deletions packages/store/store/examples/pages/page-list.store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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';

export interface Page {
_id: string;
title: string;
}

interface PagesState extends EntityState<Page> {}

@Injectable({ providedIn: 'root' })
export class PagesStore extends EntityStore<PagesState, Page> {
constructor() {
super({ entities: [] }, {});
}

@Action()
fetchPages() {
const data = {
pages: [
{ _id: '1', title: 'First Page Title' },
{ _id: '2', title: 'Second Page Title' },
{ _id: '3', title: 'Third Page Title' },
{ _id: '4', title: 'Fourth Page Title' }
]
};
return of(data).pipe(
tap((data) => {
this.initialize(data.pages);
})
);
}

@Action(updateTitle)
pureUpdateTitle(_id: string, payload: { title: string }) {
this.update(_id, { title: payload.title });
}
}
88 changes: 88 additions & 0 deletions packages/store/store/examples/pages/page.store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
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';

interface PageDetailState {
detail: { _id: string; title: string; content: string };
}

@Injectable()
export class PageDetailStore extends Store<PageDetailState> {
static detailSelector(state: PageDetailState) {
return state.detail;
}

constructor() {
super({});
}

@Action(updateTitle)
pureUpdateTitle(_id: string, payload: { title: string }) {
if (_id === this.snapshot.detail._id) {
this.update({
detail: {
...this.snapshot.detail,
...payload
}
});
}
}

@Action(updateContent)
pureUpdateContent(_id: string, payload: { content: string }) {
if (_id === this.snapshot.detail._id) {
this.update({
detail: {
...this.snapshot.detail,
...payload
}
});
}
}

@Action()
fetchPageDetail() {
const data = {
pageDetail: { _id: '1', title: 'First Page Title', content: 'First Page Detail Content' }
};
return of(data).pipe(
tap((data) => {
this.update({
detail: {
...this.snapshot.detail,
...data.pageDetail
}
});
})
);
}

@Action()
updateTitle() {
return of(true).pipe(
tap(() => {
dispatch(updateTitle('1', { title: 'New First Page Title' }));
})
);
}

@Action()
updateContent() {
return of(true).pipe(
tap(() => {
dispatch(updateContent('1', { content: 'New First Page Content' }));
})
);
}

@Action()
resetTitle() {
return of(true).pipe(
tap(() => {
dispatch(updateTitle('1', { title: 'First Page Title' }));
})
);
}
}
18 changes: 18 additions & 0 deletions packages/store/store/examples/pages/pages.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<div class="page-container">
<div class="sidebar">
<ng-container *ngIf="pagesStore.entities$ | async as pages">
<p *ngFor="let page of pages">
{{ page.title }}
</p>
</ng-container>
</div>

<div class="detail">
<ng-container *ngIf="pageDetail$ | async as pageDetail">
<h3>{{ pageDetail.title }}</h3>
<p>{{ pageDetail.content }}</p>
</ng-container>
<button class="dg-btn dg-btn-primary dg-btn-xsm" (click)="updateTitle()">更新First标题</button>
<button class="dg-btn dg-btn-primary dg-btn-xsm" (click)="updateContent()">更新First内容</button>
</div>
</div>
23 changes: 23 additions & 0 deletions packages/store/store/examples/pages/pages.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.page-container {
width: 700px;
display: flex;
flex-direction: row;
background: #fafafa;
.sidebar {
max-width: 200px;
border-right: 1px solid;
padding-right: 20px;
p {
padding: 4px;
border-bottom: 1px solid #ccc;
}
}
.detail {
flex: 1;
padding-left: 20px;
.dg-btn {
margin-top: 10px;
margin-right: 8px;
}
}
}
37 changes: 37 additions & 0 deletions packages/store/store/examples/pages/pages.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Component, OnInit } from '@angular/core';
import { PageDetailStore } from './page.store';
import { PagesStore } from './page-list.store';
import { Observable } from 'rxjs';

@Component({
selector: 'thy-store-pages-example',
templateUrl: './pages.component.html',
styleUrls: ['./pages.component.scss'],
providers: [PageDetailStore, PagesStore]
})
export class ThyStorePagesExampleComponent implements OnInit {
constructor(public pagesStore: PagesStore, public pageDetailStore: PageDetailStore) {}

pageDetail$: Observable<any> = this.pageDetailStore.select(PageDetailStore.detailSelector);

ngOnInit() {
this.fetchPages();
this.fetchPageDetail();
}

fetchPages() {
this.pagesStore.fetchPages();
}

fetchPageDetail() {
this.pageDetailStore.fetchPageDetail();
}

updateTitle() {
this.pageDetailStore.updateTitle();
}

updateContent() {
this.pageDetailStore.updateContent();
}
}
Loading

0 comments on commit 784a87c

Please sign in to comment.