Try to build from scratch a reactive spreadhseet with an Object Oriented code in javascript and you’ll end up facing two challenges :
- managing dependencies through setters : if B is function of A then A should hold the dependency of B, so when its value changes, it tells B to update.
- creating functions that take cell references instead of usual arguments : “B = A+C+1” will have to involve the definition of functions : operation(a, c) ⇒ (a+c+1) AND the references references(”A”, “B”) ⇒ returns
Here is how simple it is to create the core of a reactive spreadsheet with observables
javascriptimport { BehaviorSubject, map, tap, combineLatest } from "rxjs"; class Cell { constructor(reference) { this.result = new BehaviorSubject(null); this._renderSubscription = this.result .pipe(tap(v => console.log(`${reference} is equal to ${v}`))) .subscribe(); } set formula(formula) { this._formula = formula; this._formulaSubscription?.unsubscribe(); this._formulaSubscription = this._formula.subscribe(this.result); } } const references = new Set(["A", "B", "C"]); const spreadsheet = [...references] .map(ref => ({ [ref]: new Cell(ref) })) .reduce((prev, curr) => ({ ...prev, ...curr }), {}); const setCells = spreadsheet => { let { A, B, C } = spreadsheet; A.formula = new BehaviorSubject(10); // logs A is equal to 10 B.formula = A.result.pipe(map(v => v + 1)); //logs B is equal to 11 A.formula = new BehaviorSubject(20); // logs A is equal to 20 then B is equal to 21 C.formula = combineLatest(A.result, B.result).pipe(map(([a, b]) => a + b)); // logs C is equal to 41 }; setCells(spreadsheet);
Now let’s shift from
- a cell to a component
- a result to a state
- a log to a render method
- a reference to a css-selector
We can also decide to remove the notion of formula entirely
javascriptimport { BehaviorSubject, map, tap, combineLatest } from "rxjs"; class Component { constructor(css_selector) { this._state = new BehaviorSubject(null); this._renderSubscription = this._state .pipe(tap(Component.render(css_selector))) .subscribe(); } set state(state) { this._stateSubscription?.unsubscribe(); this._stateSubscription = state.subscribe(this._state); } get state() { return this._state; } static render(css_selector) { return state => { console.log(`${css_selector} is equal to ${state}`); }; } } const selectors = new Set(["component1", "component2", "component3"]); const page = [...selectors] .map(sel => ({ [sel]: new Component(sel) })) .reduce((prev, curr) => ({ ...prev, ...curr }), {}); const setComponents = page => { let { component1, component2, component3 } = page; component1.state = new BehaviorSubject(10); // logs A is equal to 10 component2.state = component1.state.pipe(map(v => v + 1)); //logs B is equal to 11 component1.state = new BehaviorSubject(20); // logs A is equal to 20 then B is equal to 21 component3.state = combineLatest(component1.state, component2.state).pipe( map(([a, b]) => a + b) ); // logs C is equal to 41 }; setComponents(page);
Next article will show how components naturally extend HTMLElement