Instead of using RxJS we could have a state manager that knows how to handle all state transitions.
Last active
June 5, 2021 01:01
-
-
Save renoirb/850de479d101af6928643775c12524b1 to your computer and use it in GitHub Desktop.
Poor man's "reactivity" state mutation management
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
/** | |
* When a component has a number of properties with values as scalar and we want to | |
* use as the representation of each state. | |
*/ | |
export type ReactiveShallowHashMap<V = string | number | boolean> = Record<string, V> | |
/** | |
* Are all keys of an object only strings? | |
* | |
* @param obj An object or hash-map where we have keys and values | |
*/ | |
export const isRecord = <V = unknown>(val: unknown): val is Record<string, V> => | |
Object.keys(val || {}).every(k => typeof k === 'string') && val !== null && typeof val === 'object' | |
export type AssertsIsRecord = <V = unknown>(val: unknown) => asserts val is Record<string, V> | |
// eslint-disable-next-line | |
// export type AssertIsRecord = (val: unknown) => asserts val is object // typescript-eslint/ban-types | |
export const assertsIsRecord: AssertsIsRecord = val => { | |
if (isRecord(val)) { | |
return | |
} | |
const message = `Invalid input, a state must be an object where each keys are only strings` | |
throw new TypeError(message) | |
} | |
export const assertsIsScalarOnlyRecord: <V = string | number | boolean>( | |
val: unknown, | |
) => asserts val is ReactiveShallowHashMap<V> = input => { | |
assertsIsRecord(input) | |
// In other words, for every properties, check if they are scalar, nothing else. | |
if (Object.entries(input).every(([, propValue]) => ['string', 'number', 'boolean'].includes(typeof propValue))) { | |
return | |
} | |
const message = `Invalid input, it must be an object where keys are only strings, and value are only scalar values` | |
throw new TypeError(message) | |
} | |
/** | |
* When a component has many properties that may change over time. | |
* What are the ways to handle transitions. | |
* How do we listen and mutate the state of that component. | |
*/ | |
export interface StateTransitioner<T extends ReactiveShallowHashMap, E = unknown> { | |
/** | |
* How should we make state transitions | |
*/ | |
stateMutator?(state?: T): E | |
/** | |
* What is the current state of that component mutable properties | |
*/ | |
readonly state: T | |
} | |
export abstract class AbstractStateTransitionManager<T extends ReactiveShallowHashMap, E = unknown> | |
implements StateTransitioner<T, E> { | |
abstract stateMutator?(state: T): E | |
private stateChanges: T[] = [] | |
get state(): T { | |
return this.stateChanges.slice(-1)[0] as T | |
} | |
set state(value: T) { | |
assertsIsScalarOnlyRecord(value) | |
this.stateChanges.push(Object.freeze({ ...value })) | |
} | |
constructor(initialState: Partial<T>) { | |
this.state = initialState as T | |
} | |
} |
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 { UserDoNotDisturb, Listener, PresenceUpdated } from './typings' | |
import type { ReactiveShallowHashMap } from './reactivity' | |
import { AbstractStateTransitionManager } from './reactivity' | |
/** | |
* What are the properties of the DND Switch that affects its properties. | |
*/ | |
export interface DndSwitchComponentState extends ReactiveShallowHashMap { | |
readonly externalUserKey: ExternalUserKey | |
readonly isDoNotDisturb: boolean | |
} | |
export class DndSwitchStateTransitionManager extends AbstractStateTransitionManager< | |
DndSwitchComponentState, | |
Listener<PresenceUpdated> | |
> { | |
constructor(initialState: Partial<DndSwitchComponentState>) { | |
const state = { | |
isDoNotDisturb: false, | |
externalUserKey: '', | |
...initialState, | |
} as DndSwitchComponentState | |
super(state) | |
} | |
set isDoNotDisturb(isDoNotDisturb: boolean) { | |
const previous = this.state | |
this.state = { ...previous, isDoNotDisturb } | |
} | |
stateMutator(initialState: Partial<DndSwitchComponentState>): Listener<PresenceUpdated> { | |
const internalState = this.state | |
const state = { | |
...internalState, | |
...initialState, | |
} as DndSwitchComponentState | |
if (!initialState.externalUserKey || initialState.externalUserKey.length < 1) { | |
const message = `Invalid externalUserKey value "${initialState.externalUserKey}" it must be a non empty value` | |
throw new TypeError(message) | |
} | |
this.state = state | |
return (payload: PresenceUpdated): Promise<boolean> => { | |
// Reminder, this state getter is returning a copy of the latest of the stateChanges | |
const { externalUserKey, isDoNotDisturb } = this.state as DndSwitchComponentState | |
const isApplicable = payload.externalUserKey === externalUserKey | |
const previousValue = isDoNotDisturb ? UserDoNotDisturb.DO_NOT_DISTURB : UserDoNotDisturb.NONE | |
const after = { | |
externalUserKey, | |
isDoNotDisturb: previousValue === UserDoNotDisturb.DO_NOT_DISTURB, | |
} | |
console.log('dnd-switch\t\t\t\tDndSwitchStateTransitionManager\n', { | |
isApplicable, | |
payload: payload, | |
before: { | |
externalUserKey, | |
isDoNotDisturb, | |
}, | |
after, | |
}) | |
// Push state change | |
this.state = after | |
return Promise.resolve(isApplicable) | |
} | |
} | |
} |
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
/** | |
* Typings used in example, not part of the idea this Gist is for | |
*/ | |
export declare enum UserDoNotDisturb { | |
NONE = "NONE", | |
DO_NOT_DISTURB = "DO_NOT_DISTURB" | |
} | |
export type Listener<T> = (payload: T) => void | |
export interface PresenceUpdated { | |
readonly externalUserKey: string | |
readonly presence: Presence | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Yeaaaah. No.
It would be best to use microsoft/fast reactivity in source
web-components/fast-element/src/observation/observable.ts
and along with DOM event communication.Reactivity ("Observable" and state)
See microsoft/fast Observables and State (see Observable)
Or vue-next's (Vue 3) reactivity and @vue/runtime-dom tests (see source and here)
Event Propagation
Source from this article