Created
November 15, 2019 18:34
-
-
Save timdeschryver/dcdeefdba43bef7134c8249e8257dae7 to your computer and use it in GitHub Desktop.
xstate-poc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
EventObject, | |
StateMachine, | |
InterpreterOptions, | |
MachineOptions, | |
StateConfig, | |
interpret, | |
Actor, | |
} from 'xstate' | |
import { from, BehaviorSubject, Observable, merge } from 'rxjs' | |
import { filter, startWith, shareReplay, finalize } from 'rxjs/operators' | |
interface CreateStateMachineOptions<TContext, TEvent extends EventObject> { | |
/** | |
* If provided, will be merged with machine's `context`. | |
*/ | |
context?: Partial<TContext> | |
/** | |
* The state to rehydrate the machine to. The machine will | |
* start at this state instead of its `initialState`. | |
*/ | |
state?: StateConfig<TContext, TEvent> | |
/** | |
* Events to send to the service | |
* E.g. user events, or can also be NgRx actions/selectors | |
*/ | |
events?: Observable<TEvent>[] | |
} | |
const createStateMachineDefaultOptions = { | |
events: [], | |
} | |
export function createStateMachine<TContext, TEvent extends EventObject>( | |
machine: StateMachine<TContext, any, TEvent>, | |
options: Partial<InterpreterOptions> & | |
Partial<CreateStateMachineOptions<TContext, TEvent>> & | |
Partial< | |
MachineOptions<TContext, TEvent> | |
> = createStateMachineDefaultOptions, | |
) { | |
const { | |
context, | |
guards, | |
actions, | |
activities, | |
services, | |
delays, | |
immediate, | |
state: rehydratedState, | |
events, | |
...interpreterOptions | |
} = options | |
const machineConfig = { | |
context, | |
guards, | |
actions, | |
activities, | |
services, | |
delays, | |
} | |
const machineConfigured = machine.withConfig(machineConfig, { | |
...machine.context, | |
...context, | |
}) | |
const service = interpret(machineConfigured, interpreterOptions).start() | |
const input = merge(...events).subscribe(service.send) | |
const state$ = from(service).pipe( | |
filter(p => p.changed), | |
startWith(service.state), | |
shareReplay(1), | |
finalize(() => { | |
service.stop() | |
input.unsubscribe() | |
}), | |
) | |
return state$ | |
} | |
interface CreateActorOptions<TEvent extends EventObject> { | |
events?: Observable<TEvent>[] | |
} | |
const createActorDefaultOptions = { | |
events: [], | |
} | |
export function createActor<TC, TE extends EventObject>( | |
actor: Actor<TC, TE>, | |
options: CreateActorOptions<TE> = createActorDefaultOptions, | |
) { | |
const { events } = options | |
const actor$ = new BehaviorSubject<any>((actor as any).state) | |
const sub = actor.subscribe(s => actor$.next(s)) | |
const input = merge(...events).subscribe(actor.send) | |
return actor$.pipe( | |
shareReplay(1), | |
finalize(() => { | |
sub.unsubscribe() | |
input.unsubscribe() | |
}), | |
) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Component, OnInit, Input } from '@angular/core' | |
import { EventObject, Actor } from 'xstate' | |
import { Observable, Subject } from 'rxjs' | |
import { createActor } from './state' | |
@Component({ | |
selector: 'app-row', | |
template: ` | |
<li | |
*ngIf="actor$ | async as state" | |
(click)="this.userEvents$.next({ type: 'TOGGLE' })" | |
> | |
{{ state.matches('on') ? '✅' : '⬜' }} {{ state.context.firstName }} | |
{{ state.context.lastName }} | |
</li> | |
`, | |
}) | |
export class RowComponent implements OnInit { | |
actor$: Observable<Actor> | |
userEvents$ = new Subject<EventObject>() | |
@Input() actor: Actor | |
ngOnInit() { | |
this.actor$ = createActor(this.actor, { | |
events: [this.userEvents$], | |
}) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Component } from '@angular/core' | |
import { Store, select } from '@ngrx/store' | |
import { customersRefresh } from './global-state-stuff/customers.actions' | |
import { EventObject } from 'xstate' | |
import { tableMachine } from './local-state-stuff/table.machine' | |
import { timer, Subject } from 'rxjs' | |
import { map, mapTo } from 'rxjs/operators' | |
import { selectCustomers } from './global-state-stuff/customers.selects' | |
import { Actions } from '@ngrx/effects' | |
import { createStateMachine } from './state' | |
@Component({ | |
selector: 'app-xstate-poc', | |
template: ` | |
<ng-container *ngIf="state$ | async as state"> | |
State: {{ state.toStrings() }} | |
<hr /> | |
<div *ngIf="state.matches('empty')"> | |
No data, | |
<button (click)="refresh()">Refresh</button> | |
</div> | |
<div *ngIf="state.matches('loaded')"> | |
<div *ngIf="state.matches('loaded.calculating')"> | |
Doing some heavy calculations 💫💫💫 | |
</div> | |
<ul> | |
<app-row | |
*ngFor="let item of state.context.pageData" | |
[actor]="item.ref" | |
> | |
</app-row> | |
</ul> | |
<button (click)="userEvents$.next({ type: 'PREV_PAGE' })">◀</button> | |
<button (click)="userEvents$.next({ type: 'NEXT_PAGE' })">▶</button> | |
<button (click)="userEvents$.next({ type: 'CALCULATE' })">🧮</button> | |
</div> | |
</ng-container> | |
`, | |
}) | |
export class XStatePocComponent { | |
userEvents$ = new Subject<EventObject>() | |
// table with pagination | |
state$ = createStateMachine(tableMachine, { | |
id: 'Local State', | |
services: { | |
// think of a HTTP call | |
someHeavyCalculations: () => timer(2164).pipe(mapTo({ type: 'NOOP' })), | |
}, | |
context: { page: 2 }, | |
devTools: true, | |
events: [ | |
// how else can we call XState's service.send(action) ? | |
this.userEvents$, | |
// forward NgRx's actions to XState | |
this.actions$, | |
// forward a selector result to XState, this must be an action | |
this.store.pipe( | |
select(selectCustomers), | |
map(c => ({ type: 'HYDRATE', data: c })), | |
), | |
], | |
}) | |
constructor(private store: Store<any>, private actions$: Actions) {} | |
refresh() { | |
this.store.dispatch(customersRefresh()) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment