From a67e04bdc4f2205a4c3649c0f934b0bb6bd0498b Mon Sep 17 00:00:00 2001 From: KernelDeimos Date: Sat, 25 Feb 2023 00:52:45 -0500 Subject: [PATCH] Add array rendering support --- src/component/Component.ts | 5 +-- src/index.ts | 6 +++ src/vao/ArrayVAO.ts | 74 +++++++++++++++++++++++++++++++++ src/vao/RenderableVAO.ts | 85 ++++++++++++++++++++++++++++++++++++++ src/vao/VAO.ts | 23 +++++++++++ src/vao/VAOUtil.ts | 17 ++++++++ 6 files changed, 207 insertions(+), 3 deletions(-) create mode 100644 src/vao/ArrayVAO.ts create mode 100644 src/vao/RenderableVAO.ts create mode 100644 src/vao/VAOUtil.ts diff --git a/src/component/Component.ts b/src/component/Component.ts index f5e9d9e..efa9d07 100644 --- a/src/component/Component.ts +++ b/src/component/Component.ts @@ -1,5 +1,5 @@ import { TAttributes } from "../html/htmldefs"; -import { ScalarVAO } from "../vao/ScalarVAO.js"; +import { VAOUtil } from "../vao/VAOUtil.js"; import { AbstractVAO, Detachable, VAO, VAOConstructor } from "../vao/VAO.js"; import { Renderable, INotifyMounted } from "./componentdefs"; @@ -132,7 +132,6 @@ export abstract class Component { element.remove(); } - // NEXT: update VAO rerender listeners in createElements const newElements = this.createElements(); referenceElement.replaceWith(...newElements); this.notifyMounted({ @@ -160,7 +159,7 @@ export abstract class Component { createDynamicVAO_ (vaoName: string, value?: any) { // TODO: [E], [I] - this.vaos_[vaoName] = new ScalarVAO(value ?? null); + this.vaos_[vaoName] = VAOUtil.getAppropriateVAO(value); } // TODO: [H] diff --git a/src/index.ts b/src/index.ts index 13ccc7a..7827183 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,9 @@ import { TAttributes } from './html/htmldefs.js'; import { Renderable, RenderableConstructor } from './component/componentdefs.js'; import { Tag } from './component/Tag.js'; import { Component as ExportableComponent } from './component/Component.js'; +import { ScalarVAO as ExportableScalarVAO } from './vao/ScalarVAO.js'; +import { ArrayVAO as ExportableArrayVAO } from './vao/ArrayVAO.js'; +import { AbstractVAO as ExportableAbstractVAO } from './vao/VAO.js'; export function createElement( tag: string | RenderableConstructor, @@ -28,3 +31,6 @@ export class Example { } export const Component = ExportableComponent; +export const AbstractVAO = ExportableAbstractVAO; +export const ScalarVAO = ExportableScalarVAO; +export const ArrayVAO = ExportableArrayVAO; diff --git a/src/vao/ArrayVAO.ts b/src/vao/ArrayVAO.ts new file mode 100644 index 0000000..6733dcb --- /dev/null +++ b/src/vao/ArrayVAO.ts @@ -0,0 +1,74 @@ +import { AbstractVAO, TMappingFn, VAO } from "./VAO.js"; +import { RenderableVAO } from "./RenderableVAO.js"; +import { VAOUtil } from "./VAOUtil.js"; + +// === TODO === +// [A] yielded VAO should follow array VAO +// [B] consider if this way of passing detachable can be improved + +const DIFF_INSERT = {}; +const DIFF_REMOVE = {}; +const DIFF_REPLACE = {}; + +export class ArrayVAO extends AbstractVAO { + private value_: any[] + + protected getTopics_(): string[] { + return [ + 'change', + 'item.insert', + 'item.remove', + 'item.replace' + ] + } + + constructor (initialValue: any[]) { + super(); + if ( ! Array.isArray(initialValue) ) { + throw new Error('ArrayVAO expects an array'); + } + this.value_ = initialValue; + } + + // TODO: move to AbstractVAO if this isn't modified by version 1.2.0 + set (value: any[]) { + let o = this.value_; + this.value_ = value; + if ( o !== this.value_ ) { + this.pub('change'); + } + } + + get (): any { + return this.value_; + } + + each (mappingFn: TMappingFn): VAO[] { + const results = []; + // TODO: [A] + for ( const item of this.value_ ) { + const vao = VAOUtil.getAppropriateVAO(item); + const [renderable, detachable] = + vao.map(mappingFn, RenderableVAO); + // TODO: [B] + ;(renderable as RenderableVAO).detachable?.detach?.(); + results.push(renderable) + } + + return results; + } + + // diffArrays_(oldArray, newArray) { + // let ptrOld = 0; + // let ptrNew = 0; + + // const events = []; + + // while ( ptrOld < oldArray.length ) { + // } + + // while ( otrNew < newArray.length ) { + // events.push(); + // } + // } +} \ No newline at end of file diff --git a/src/vao/RenderableVAO.ts b/src/vao/RenderableVAO.ts new file mode 100644 index 0000000..fa812e9 --- /dev/null +++ b/src/vao/RenderableVAO.ts @@ -0,0 +1,85 @@ +import { ScalarVAO } from "./ScalarVAO.js"; +import { Detachable } from "./VAO.js"; +import { Renderable, INotifyMounted } from "../component/componentdefs.js"; + +export class RenderableVAO extends ScalarVAO { + protected renderChildren_: Renderable[] | null = null; + protected elements_: Element[]; + protected parent_: Element | null = null; + protected rerenderSubscription_: Detachable | null; + + public detachable: Detachable | null = null; + + constructor (value: any) { + super(value); + + this.elements_ = []; + this.rerenderSubscription_ = null; + } + + createElements (): Element[] { + let renderables = this.get(); + let renderablesTS: Renderable[] = Array.isArray(renderables) + ? renderables : [renderables]; + + const elements = []; + for ( const renderable of renderablesTS ) { + const theseElements = renderable.createElements(); + elements.push(...theseElements); + } + + this.elements_ = elements; + return elements; + } + + rerenderSelf () { + if ( ! this.parent_ ) { + throw new Error('cannot re-render; no existing element'); + } + + if ( this.elements_.length === 0 ) { + throw new Error('cannot re-render a component with zero elements'); + } + + this.onWillDetachFromDOM_(); + + // 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(); + } + + const newElements = this.createElements(); + referenceElement.replaceWith(...newElements); + this.notifyMounted({ + parent: this.parent_, + }) + } + + notifyWillDetachFromDOM () { + if ( this.detachable ) this.detachable.detach(); + this.onWillDetachFromDOM_(); + } + onWillDetachFromDOM_ () { + if ( this.rerenderSubscription_ ) { + this.rerenderSubscription_.detach(); + } + // Detach listeners from child renderables generated last time + if ( ! this.renderChildren_ ) { + throw new Error('tried to detach before rendering'); + } + for ( const renderable of this.renderChildren_ ) { + renderable.notifyWillDetachFromDOM(); + } + } + + notifyMounted(event: INotifyMounted) { + this.parent_ = event.parent; + + this.rerenderSubscription_ = this.sub('change', () => { + this.rerenderSelf(); + }) + } +} \ No newline at end of file diff --git a/src/vao/VAO.ts b/src/vao/VAO.ts index 0550e62..9621d24 100644 --- a/src/vao/VAO.ts +++ b/src/vao/VAO.ts @@ -18,11 +18,14 @@ export type TValueEvent = { extra: any, } +export type TMappingFn = (v: any) => any export interface VAO { sub (topic: string, listener: () => void): Detachable pub (topic: string, extra: any): void get (): any set (value: any): void + map (mappingFn: TMappingFn, outputType?: VAOConstructor): + [VAO, Detachable] } export interface VAOConstructor { @@ -86,4 +89,24 @@ export abstract class AbstractVAO implements VAO { node.listener(event); } } + + map (mappingFn: TMappingFn, outputType?: VAOConstructor): + [VAO, Detachable] { + + if ( ! outputType ) { + throw new Error( + "I want it to use ScalarVAO here but then there would " + + "be a circular dependency" + ) + } + + const outputVAO = new outputType(mappingFn(this.get())); + + const l = () => { + outputVAO.set(mappingFn(this.get())) + } + + const sub = this.sub('change', l); + return [outputVAO, sub]; + } } diff --git a/src/vao/VAOUtil.ts b/src/vao/VAOUtil.ts new file mode 100644 index 0000000..45cd42b --- /dev/null +++ b/src/vao/VAOUtil.ts @@ -0,0 +1,17 @@ +import { ScalarVAO } from "./ScalarVAO.js"; +import { ArrayVAO } from "./ArrayVAO.js"; +import { AbstractVAO, VAO } from "./VAO.js"; + +export class VAOUtil { + static getAppropriateVAO (value: any): VAO { + if ( value instanceof AbstractVAO ) { + return value; + } + + if ( Array.isArray(value) ) { + return new ArrayVAO(value); + } + + return new ScalarVAO(value); + } +} \ No newline at end of file