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);
}
}