Skip to content

Instantly share code, notes, and snippets.

@Floofies
Last active April 19, 2023 18:42
Show Gist options
  • Save Floofies/d789fb4a30d0b5d2a74a98c5b77779bb to your computer and use it in GitHub Desktop.
Save Floofies/d789fb4a30d0b5d2a74a98c5b77779bb to your computer and use it in GitHub Desktop.
Tiny MVVM in TypeScript with shallow diffing/binding
// Object storage with publisher/subscriber model for updates.
type State = null|object;
export class DataStore {
state:State;
oldState:State;
subscribers:Map<number|string, Set<Function>>;
constructor(state: State = null) {
this.state = {};
this.oldState = {};
this.subscribers = new Map();
if (state !== null)
this.state = state;
}
subscribe(callback:Function, accessor:number|string = "*"):void {
let subscriber = this.subscribers.get(accessor);
if(typeof(subscriber) === "undefined")
subscriber = new Set();
subscriber.add(callback);
this.subscribers.set(accessor, subscriber);
}
unsubscribe(callback:Function, accessor:number|string = "*"):void {
let subscriber = this.subscribers.get(accessor);
if(typeof(subscriber) === "undefined")
return
subscriber.delete(callback);
if(subscriber.size === 0)
this.subscribers.delete(accessor);
}
updateSubscribers(accessors:null|Array<number|string> = null) {
if(accessors === null)
accessors = Array.from(this.subscribers.keys());
for(const accessor of accessors) {
let subscriber = this.subscribers.get(accessor);
if(typeof(subscriber) === "undefined")
return
for (const callback of subscriber)
callback(this.state);
}
}
stateChanged(updateAll:boolean = false):void {
if(updateAll) {
this.updateSubscribers();
return;
}
if(this.state === this.oldState)
return;
let changedProps:Array<number|string>;
if((this.state === null) && (this.oldState !== null)) {
changedProps = Object.keys(this.oldState);
} else if ((this.state !== null) && (this.oldState === null)) {
changedProps = Object.keys(this.state);
} else if ((this.state !== null) && (this.oldState !== null)) {
changedProps = [];
for(const accessor in this.state)
if(!(accessor in this.oldState) || (this.state[accessor] !== this.oldState[accessor]))
changedProps.push(accessor);
for(const accessor in this.oldState)
if(!(accessor in this.state))
changedProps.push(accessor);
} else {
return;
}
if(changedProps.length === 0)
return;
this.updateSubscribers(changedProps);
}
patchState(newState:State, updateAll:boolean = false): void {
this.oldState = this.state;
this.state = Object.assign({}, this.oldState, newState);
this.stateChanged(updateAll);
}
setState(newState:State, updateAll:boolean = false):void {
this.oldState = this.state;
this.state = newState;
this.stateChanged(updateAll);
}
getState():State {
return this.state;
}
addAction(name, callback):Function {
const action = ((...args:Array<any>) => {
const newState:State = callback(this.state, ...args);
if ((typeof newState) === "undefined") return;
this.setState(newState);
}).bind(this);
this[name] = action;
return action;
}
}
// Combined View/ViewModel
interface WatcherMap {
[watchedProperty: string|number]: Function
}
export class View {
watchers:Map<DataStore, WatcherMap>;
watcherCallback:null|Function;
renderer:null|Function = null;
state:State = null;
oldState:State = null;
children:Map<any, View>;
childCount:number;
constructor(renderer:null|Function = null) {
this.watchers = new Map();
this.renderer = renderer;
this.state = {};
this.oldState = {};
this.children = new Map();
}
setState(state:State, render = true) {
this.oldState = this.state;
this.state = state;
if (render)
this.render();
}
watch(dataStore, accessor:string|number = "*"):void {
let watcherMap = this.watchers.get(dataStore);
if(typeof(watcherMap) === "undefined")
watcherMap = {};
else if(accessor in watcherMap)
return;
if(this.watcherCallback === null)
this.watcherCallback = (newState:State) => this.setState(newState);
watcherMap[accessor] = this.watcherCallback;
dataStore.subscribe(this.watcherCallback, accessor);
this.watchers.set(dataStore, watcherMap);
}
unwatch(dataStore, accessor):void {
let watcher = this.watchers.get(dataStore);
if(typeof(watcher) === "undefined")
return;
dataStore.unsubscribe(watcher);
this.watchers.delete(dataStore);
}
render(state:State = null, renderChildren:boolean = true) {
if (state === null)
state = this.state;
if (this.renderer !== null)
this.renderer(state, this.oldState);
if (!renderChildren || (this.children.size === 0))
return;
for (const child of this.children.values())
child.setState(state);
}
addChild(renderer:null|View|Function = null, label:any = null):void|View {
const child = (renderer instanceof View) ? renderer : new View(renderer);
if(label === null)
label = child;
this.children.set(label, child);
return child;
}
removeChild(childLabel: any):void {
this.children.delete(childLabel);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment