Skip to content

Instantly share code, notes, and snippets.

@renoirb
Last active June 5, 2021 01:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save renoirb/850de479d101af6928643775c12524b1 to your computer and use it in GitHub Desktop.
Save renoirb/850de479d101af6928643775c12524b1 to your computer and use it in GitHub Desktop.
Poor man's "reactivity" state mutation management

Poor man's "reactivity" state manager

Instead of using RxJS we could have a state manager that knows how to handle all state transitions.

/**
* 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
}
}
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)
}
}
}
/**
* 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
}
@renoirb
Copy link
Author

renoirb commented Jun 5, 2021

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

export interface EventOptions {
  /** should event bubble through the DOM */
  bubbles?: boolean;
  /** event is cancelable */
  cancelable?: boolean;
  /** can event bubble between the shadow DOM and the light DOM boundary */
  composed?: boolean;
}

export class EventEmitter<T> {
  constructor(private target: HTMLElement, private eventName: string) {}

  emit(value: T, options?: EventOptions) {
    this.target.dispatchEvent(
      new CustomEvent<T>(this.eventName, { detail: value, ...options })
    );
  }
}

export function event() {
  return (protoOrDescriptor: any, name: string): any => {
    const descriptor = {
      get(this: HTMLElement) {
        return new EventEmitter(this, name !== undefined ? name : protoOrDescriptor.key);
      },
      enumerable: true,
      configurable: true,
    };

    if (name !== undefined) {
      // legacy TS decorator
      return Object.defineProperty(protoOrDescriptor, name, descriptor);
    } else {
      // TC39 Decorators proposal
      return {
        kind: 'method',
        placement: 'prototype',
        key: protoOrDescriptor.key,
        descriptor,
      };
    }
  };
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment