import { TAttributes } from "../html/htmldefs"; import { ScalarVAO } from "../vao/ScalarVAO.js"; import { AbstractVAO, Detachable, VAO, VAOConstructor } from "../vao/VAO.js"; import { Renderable, INotifyMounted } from "./componentdefs"; // === TODO === // [A] Separate VAO-rerender from component // [B] it shouldn't be allowed to render a zero-element component, right? // [C] ensure VAO updates can't be missed during a re-render // [D] should render bindings be checked when elements are created // or instead when notifyMounted is called? // [E] infer VAO from type // [F] investigate possibility of detecting/avoiding infinite recursion here // [G] definition of "mounted" doesn't match React. Maybe call it something // else to avoid confusion. // [H] investigate possibility of using an AST test to ensure methods like // setOrCreateVAO are called instead of re-implementing their behaviour // [I] VAOs should install themselves into components so they can add behaviour // to the instance proxy and implement optimizations for methods like // Array.prototype.push, Object.assign, etc. interface PropertyDefinition { $?: VAOConstructor, factory?: () => any } type TPropertyDefinitions = { [key: string]: PropertyDefinition } export abstract class Component { public attributes: TAttributes public children: Renderable[] static properties: TPropertyDefinitions | undefined; protected renderChildren_: Renderable[] | null = null; protected instanceProxy_: Component | null = null; protected vaos_: { [name: string]: VAO } protected elements_: Element[]; protected parent_: Element | null = null; // TODO: [A] protected rerenderVAOs_: { [name: string]: true }; protected rerenderSubscriptions_: Detachable[]; abstract render (): Renderable | Renderable[] constructor ({ attributes, children }: { attributes: TAttributes | undefined, children: Renderable[] | undefined, }) { this.vaos_ = {} this.elements_ = []; this.attributes = attributes ?? {}; this.children = children ?? []; // Adapt children: expand nested lists this.children = this.children.flat(Infinity); // TODO: [A] this.rerenderVAOs_ = {}; this.rerenderSubscriptions_ = []; const constructorAsWhatItIs: typeof Component = (this.constructor as unknown) as typeof Component; const allProperties = constructorAsWhatItIs.allProperties_; for ( let k in allProperties ) { const property = allProperties[k]; const value = this.getPropertyInitialValue_(k); if ( property.$ !== undefined ) { const vao = this.createVAOFromProperty_(property, value); if ( ! ( vao instanceof AbstractVAO ) ) { throw new Error('expected a VAO'); } // TODO: [I] this.vaos_[k] = vao; } else { this.createDynamicVAO_(k, value) } } for ( let k in attributes ) { if ( attributes[k] instanceof AbstractVAO ) { this.vaos_[k] = attributes[k]; } else { this.setOrCreateVAO_(k, attributes[k]); } } } createElements (): Element[] { // TODO: [C] for ( const detachable of this.rerenderSubscriptions_ ) { detachable.detach(); } let renderables = this.render.call(this.instanceProxy); let renderablesTS: Renderable[] = Array.isArray(renderables) ? renderables : [renderables]; this.renderChildren_ = renderablesTS; const elements = []; for ( const renderable of renderablesTS ) { const theseElements = renderable.createElements(); elements.push(...theseElements); } this.elements_ = elements; return elements; } rerenderSelf () { console.log('rerenderSelf was called', this); if ( ! this.parent_ ) { throw new Error('cannot re-render; no existing element'); } if ( this.elements_.length === 0 ) { // TODO: [B] throw new Error('cannot re-render a component with zero elements'); } // The reference element lets us know where to put new elements const referenceElement = this.elements_[0]; // Remove following elements - we're going to replace the first one for ( const element of this.elements_.slice(1) ) { element.remove(); } // NEXT: update VAO rerender listeners in createElements const newElements = this.createElements(); referenceElement.replaceWith(...newElements); this.notifyMounted({ parent: this.parent_, }) } createDynamicVAO_ (vaoName: string, value?: any) { // TODO: [E], [I] this.vaos_[vaoName] = new ScalarVAO(value ?? null); } // TODO: [H] setOrCreateVAO_ (vaoName: string, value: any) { if ( ! this.vaos_[vaoName] ) { this.createDynamicVAO_(vaoName, value); return; } this.vaos_[vaoName].set(value); } notifyMounted(event: INotifyMounted) { this.parent_ = event.parent; // TODO: [A] // TODO: [D] console.log('notifyMounted called on a component', this) for ( const vaoName in this.rerenderVAOs_ ) { this.rerenderSubscriptions_.push( this.vaos_[vaoName].sub('change', () => { this.rerenderSelf(); }) ); } if ( ! this.renderChildren_ ) { // TODO: [G] throw new Error('component mounted but not rendered'); } // TODO: [F] for ( const child of this.renderChildren_ ) { child.notifyMounted({ parent: this.parent_ }); } } get instanceProxy (): Component { if ( this.instanceProxy_ ) return this.instanceProxy_; const self = this; this.instanceProxy_ = new Proxy(this, { get (target, p, receiver, ...__futureProof__) { if ( typeof p === 'symbol' || self.hasOwnProperty(p) ) { return Reflect.get(target, p, receiver, ...__futureProof__); } const vaoRequested = p.endsWith('$'); const vaoName = vaoRequested ? p.slice(0, -1) : p; if ( ! self.vaos_.hasOwnProperty(vaoName) ) { self.createDynamicVAO_(vaoName); } let returnValue = self.vaos_[vaoName]; if ( ! vaoRequested ) { console.log('requested a value!', self); // TODO: [A] self.rerenderVAOs_[vaoName] = true; returnValue = returnValue.get(); } return returnValue; } }); return this.instanceProxy_; } static allProperties__: TPropertyDefinitions | null; static get allProperties_ () { if ( this.allProperties__ ) return this.allProperties__; const properties: TPropertyDefinitions = {}; let cls = this; for ( ; cls.prototype instanceof Component; cls = Object.getPrototypeOf(cls) ) { if ( ! cls.properties ) continue; for ( const k in cls.properties ) { if ( properties.hasOwnProperty(k) ) { properties[k] = Object.assign( { ...cls.properties[k] }, properties[k]); } else { properties[k] = cls.properties[k]; } } } return this.allProperties__ = properties; } getPropertyInitialValue_ (k: string): any { const constructorAsWhatItIs: typeof Component = (this.constructor as unknown) as typeof Component; const allProperties = constructorAsWhatItIs.allProperties_; const property = allProperties[k]; if ( property.factory !== undefined ) { return property.factory(); } return null; } createVAOFromProperty_ (property: PropertyDefinition, initialValue: any): VAO { if ( property.$ === undefined ) { throw new Error('called createVAOFromProperty_ with invalid input'); } return new property.$(initialValue); } }