UNPKG

@ribajs/core

Version:

Core module of Riba.js

285 lines (253 loc) 7.17 kB
import type { Adapters, Root, Key, ObserverSyncCallback, Obj, Options, } from "./types/index.js"; import { isObject } from "@ribajs/utils/src/type.js"; export class Observer { public static adapters: Adapters; public static interfaces: string[] = []; public static rootInterface: Root; public static updateOptions(options: Partial<Options>): void { if (!options.adapters) { throw new Error("adapters are required!"); } if (options.adapters) { Observer.adapters = { ...Observer.adapters, ...options.adapters }; Observer.interfaces = Object.keys(Observer.adapters); } Observer.rootInterface = options.rootInterface || Observer.interfaces[0]; if (!Observer.rootInterface) { throw new Error("rootInterface is required!"); } } /** * Tokenize the provided keypath string into interface + path tokens for the * observer to work with. */ public static tokenize(keypath: string, root: Root): any[] { const tokens: any[] = []; let current: Key = { i: root, path: "" }; let index: number; let chr: string; for (index = 0; index < keypath.length; index++) { chr = keypath.charAt(index); if (~Observer.interfaces.indexOf(chr)) { tokens.push(current); current = { i: chr, path: "" }; } else { current.path += chr; } } tokens.push(current); return tokens; } /** * Gets the provided key on an object. * @param key * @param obj */ public static get(key: Key, obj: Obj) { return Observer.adapters[key.i].get(obj, key.path); } /** * Observes or unobserve a callback on the object using the provided key. * @param active * @param key * @param obj * @param callback */ public static set( active: boolean, key: Key, obj: Obj, callback: ObserverSyncCallback, ) { if (active) { Observer.adapters[key.i].observe(obj, key.path, callback); } else { Observer.adapters[key.i].unobserve(obj, key.path, callback); } } public keypath: string; public callback: ObserverSyncCallback; public objectPath: Obj[]; public obj: Obj; public target: Obj; public key: Key; public tokens: Key[]; /** * Constructs a new keypath observer and kicks things off. * @param model * @param keypath * @param callback */ constructor(model: Obj, keypath: string, callback: ObserverSyncCallback) { this.keypath = keypath; this.callback = callback; this.objectPath = []; const parseResult = this.parse(); this.key = parseResult.key; this.tokens = parseResult.tokens; this.obj = this.getRootObject(model); this.target = this.realize(); if (isObject(this.target)) { Observer.set(true, this.key, this.target, this.callback); } } /** * Parses the keypath using the interfaces defined on the view. Sets variables * for the tokenized keypath as well as the end key. */ public parse() { let path: string; let root: Root; if (!Observer.interfaces || !Observer.interfaces.length) { throw new Error( `[Observer] Must define at least one adapter interface. interfaces: "${JSON.stringify( Observer.interfaces, )}" adapters: "${JSON.stringify(Observer.adapters)}"`, ); } if (~Observer.interfaces.indexOf(this.keypath[0])) { root = this.keypath[0]; path = this.keypath.substr(1); } else { root = Observer.rootInterface; path = this.keypath; } this.tokens = Observer.tokenize(path, root); if (!this.tokens.length) { throw new Error("[Observer] No tokens"); } this.key = this.tokens.pop() as Key; return { key: this.key, tokens: this.tokens, }; } /** * Realizes the full keypath, attaching observers for every key and correcting * old observers to any changed objects in the keypath. */ public realize() { let current: Obj = this.obj; let unreached = -1; let prev; let token; for (let index = 0; index < this.tokens.length; index++) { token = this.tokens[index]; if (isObject(current)) { if (typeof this.objectPath[index] !== "undefined") { prev = this.objectPath[index]; if (current !== prev) { Observer.set(false, token, prev, this); Observer.set(true, token, current, this); this.objectPath[index] = current; } } else { Observer.set(true, token, current, this); this.objectPath[index] = current; } current = Observer.get(token, current); } else { if (unreached === -1) { unreached = index; } prev = this.objectPath[index]; if (prev) { Observer.set(false, token, prev, this); } } } if (unreached !== -1) { this.objectPath.splice(unreached); } return current; } /** * Updates the keypath. This is called when any intermediary key is changed. */ public sync() { let oldValue; let newValue; const next = this.realize(); if (next !== this.target) { if (isObject(this.target)) { Observer.set(false, this.key, this.target, this.callback); } if (isObject(next)) { Observer.set(true, this.key, next, this.callback); } oldValue = this.value(); this.target = next; newValue = this.value(); if (newValue !== oldValue || Array.isArray(newValue)) { this.callback.sync(); } } else if (Array.isArray(next)) { this.callback.sync(); } } /** * Reads the current end value of the observed keypath. Returns undefined if * the full keypath is unreachable. */ public value() { if (isObject(this.target)) { return Observer.get(this.key, this.target); } } /** * Sets the current end value of the observed keypath. Calling setValue when * the full keypath is unreachable is a no-op. * @param value */ public setValue(value: any) { if (isObject(this.target)) { Observer.adapters[this.key.i].set(this.target, this.key.path, value); } } /** * Unobserve the entire keypath. */ public unobserve() { let obj: Obj; let token; for (let index = 0; index < this.tokens.length; index++) { token = this.tokens[index]; obj = this.objectPath[index]; if (obj) { Observer.set(false, token, obj, this); } } if (isObject(this.target)) { Observer.set(false, this.key, this.target, this.callback); } } /** * traverse the scope chain to find the scope which has the root property * if the property is not found in chain, returns the root scope * @param obj */ public getRootObject(obj: Obj) { let rootProp; let current; if (!obj.$parent) { return obj; } if (this.tokens.length) { rootProp = this.tokens[0].path; } else { rootProp = this.key.path; } current = obj; while (current.$parent && current[rootProp] === undefined) { current = current.$parent; } return current; } }