Skip to content

Instantly share code, notes, and snippets.

@timdeschryver
Created November 15, 2019 18:34
Show Gist options
  • Save timdeschryver/dcdeefdba43bef7134c8249e8257dae7 to your computer and use it in GitHub Desktop.
Save timdeschryver/dcdeefdba43bef7134c8249e8257dae7 to your computer and use it in GitHub Desktop.
xstate-poc
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()
}),
)
}
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$],
})
}
}
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