We can then change the values in the cells by calling
javascriptdocument.getElementsByTagName('potions-spreadsheet')[0].A.next(20)
html<!DOCTYPE html> <link rel="shortcut icon" href="#" /> <html lang="fr"> <body> <potions-spreadsheet></potions-spreadsheet> <script src="main.js"></script> </body> </html>
javascriptimport { PotionsSpreadsheet } from "./PotionsSpreadsheet.js"; import { PotionsCell } from "./PotionsCell.js"; customElements.define("potions-spreadsheet", PotionsSpreadsheet); customElements.define("potions-cell", PotionsCell); function getCell(ref) { return document.getElementsByTagName("potions-spreadsheet")[0][ref]; } getCell("A").next(15); getCell("A") .pipe(map(v => v + 1)) .subscribe(getCell("B")); combineLatest(getCell("A"), getCell("B")) .pipe(map(([a, b]) => a + b)) .subscribe(getCell("C"));
javascriptimport { Component } from "./Component.js"; import { BehaviorSubject, map, combineLatest, share } from "rxjs"; export class PotionsSpreadsheet extends Component { isRootState = true; A = new BehaviorSubject(null); B = new BehaviorSubject(null); C = new BehaviorSubject(null); connectedCallback() { super.connectedCallback(); this.state$ = combineLatest(this.A, this.B, this.C).pipe( map(([A, B, C]) => ({ A: A, B: B, C: C })) ); } templateStyle(state) { return `<style> :host { display:flex; } </style>`; } template(state) { return ` <potions-cell reference='A'></potions-cell> <potions-cell reference='B'></potions-cell> <potions-cell reference='C'></potions-cell> `; } }
javascriptimport { Component } from "./Component.js"; import { map, combineLatest, tap } from "rxjs"; export class PotionsCell extends Component { static get observedAttributes() { return ["reference"]; } connectedCallback() { super.connectedCallback(); this.state$ = combineLatest( super.rootState$(), super.observedAttributes$ ).pipe( map(([rootState, observedAttributes]) => { return rootState?.[observedAttributes["reference"]]; }) ); } templateStyle(state) { return ` <style> :host { display:flex; border: 1px solid black; padding:20px; margin:10px; text-align:center; } </style>`; } }
javascriptimport { BehaviorSubject, tap } from "rxjs"; export class Component extends HTMLElement { shadow = this.attachShadow({ mode: "open" }); isRootState = false; _state$ = new BehaviorSubject(null); _observedAttributes$ = new BehaviorSubject(null); connectedCallback() { this._observedAttributes$.next( this.constructor.observedAttributes ?.map(attr => ({ [attr]: this.getAttribute(attr) })) .reduce((prev, curr) => ({ ...prev, ...curr }), {}) ); this._renderSubscription = this._state$ .pipe(tap(s => this.render(s))) .subscribe(); } disconnectedCallback() { this._stateSubscription?.unsubscribe(); this._renderSubscription?.unsubscribe(); } attributeChangedCallback(name, oldValue, newValue) { const newObservedAttributes = { ...this._observedAttributes$.value, ...{ [name]: newValue } }; this._observedAttributes$.next(newObservedAttributes); } templateStyle(state) { return ""; } template(state) { return state; } render(state) { this.shadow.innerHTML = ` ${this.templateStyle(state)} ${this.template(state)} `; } rootState$() { const parentOrHost = el => el.parentNode ?? el.host; let parent = parentOrHost(this); while (parent) { if (parent.isRootState) { return parent.state$; } parent = parentOrHost(parent); } } set state$(state$) { this._stateSubscription?.unsubscribe(); this._stateSubscription = state$.subscribe(this._state$); } get state$() { return this._state$; } get observedAttributes$() { return this._observedAttributes$; } }