Port proof-of-concept

TestBranch
KernelDeimos 3 years ago
parent 991ad5aefe
commit a29bbf349f

@ -0,0 +1,176 @@
import { TAttributes } from "../html/htmldefs";
import { ScalarVAO } from "../vao/ScalarVAO.js";
import { AbstractVAO, Detachable, VAO } 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.
export abstract class Component {
public attributes: TAttributes
public children: Renderable[]
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_ = [];
for ( let k in attributes ) {
if ( attributes[k] instanceof AbstractVAO ) {
this.vaos_[k] = attributes[k];
} else {
// TODO: [E]
const vao = new ScalarVAO(attributes[k]);
this.vaos_[k] = vao;
}
}
}
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) {
this.vaos_[vaoName] = new ScalarVAO(null);
}
notifyMounted(event: INotifyMounted) {
this.parent_ = event.parent;
// TODO: [A]
// TODO: [D]
console.log('notifyMounted called on a component', this)
debugger;
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_;
}
}

@ -0,0 +1,75 @@
import { TAttributes } from "../html/htmldefs.js";
import { tagsupports } from "../tagsupports.js";
import { INotifyMounted, Renderable } from "./componentdefs.js";
export interface TagAxiom {
attach (element: Element): void
detach (): void
}
export class Tag {
public nodeName: string
public attributes: TAttributes
public children: Renderable[]
protected element_: Element | null = null
protected axioms_: TagAxiom[]
constructor ({
nodeName, attributes, children
}: {
nodeName: string,
attributes: TAttributes | undefined,
children: Renderable[] | undefined
}) {
this.axioms_ = [];
this.nodeName = nodeName;
this.attributes = attributes ?? {};
this.children = children ?? [];
// Adapt children: expand nested lists
this.children = this.children.flat(Infinity);
for ( const tagsupport of tagsupports ) {
tagsupport.maybeApply(this);
}
}
install (axiom: TagAxiom) {
this.axioms_.push(axiom);
}
createElements () {
const el = document.createElement(this.nodeName);
for ( const k in this.attributes ) {
el.setAttribute(k, this.attributes[k])
}
this.element_ = el;
for ( const child of this.children ) {
const childElements = child.createElements();
for ( const childElement of childElements.flat(Infinity) ) {
el.appendChild(childElement);
}
}
for ( const axiom of this.axioms_ ) {
axiom.attach(el);
}
return [el];
}
notifyMounted (event: INotifyMounted) {
console.log('notifyMounted called on a tag', this);
if ( this.element_ == null ) {
throw new Error('this should never happen');
}
for ( const child of this.children ) {
child.notifyMounted({
parent: this.element_
});
}
}
}

@ -0,0 +1,13 @@
import { TAttributes } from '../html/htmldefs.js';
export interface INotifyMounted {
parent: Element
}
export interface Renderable {
createElements: () => Element[]
notifyMounted: (event: INotifyMounted) => void
}
export interface RenderableConstructor {
new (_: {attributes: TAttributes, children: Renderable[]}): Renderable
}

@ -0,0 +1 @@
export type TAttributes = { [key: string]: any };

@ -1,9 +1,24 @@
export function createElement(tag: string, attributes: { [key: string]: any }, ...children: any[]) {
return {
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';
export function createElement(
tag: string | RenderableConstructor,
attributes: TAttributes,
...children: any[]
) {
if ( typeof tag !== 'string' ) {
return new tag({ attributes, children });
}
const renderable: Renderable = new Tag({
nodeName: tag,
attributes,
children
};
children,
});
return renderable;
}
export class Example {
@ -11,3 +26,5 @@ export class Example {
return 'This is a test for the build configuration';
}
}
export const Component = ExportableComponent;

@ -0,0 +1,135 @@
import { Tag, TagAxiom } from "./component/Tag.js";
import { AbstractVAO, Detachable } from "./vao/VAO.js";
type TagSupportPredicate = {
description: string,
code: (tag: Tag) => boolean
}
type TagSupportSpec = {
predicates: TagSupportPredicate[],
execute: (tag: Tag) => void
};
export const tagsupports: TagSupport[] = [];
const p = (model: TagSupportSpec) => {
tagsupports.push(TagSupport.fromSpec(model));
}
class TagSupport {
static fromSpec(spec: TagSupportSpec): TagSupport {
return new TagSupport(spec);
}
protected spec: TagSupportSpec;
constructor (spec: TagSupportSpec) {
this.spec = spec;
}
maybeApply (tag: Tag): void {
for ( const predicate of this.spec.predicates ) {
if ( ! predicate.code(tag) ) return;
}
this.spec.execute(tag);
}
}
const IS_AN_INPUT_TAG: TagSupportPredicate = {
description: 'is an input tag',
code: tag => tag.nodeName == 'input'
};
const ATTR_VALUE_IS: (key: string, val: any) => TagSupportPredicate = (key, val) => {
return {
description: `attribute "${key}" is "${val}"`,
code: tag => tag.attributes[key] == val
};
}
const ATTR_VALUE_IS_VAO: TagSupportPredicate = {
description: 'value attribute is a VAO',
code: tag => tag.attributes.value instanceof AbstractVAO
};
class BindInputValueAxiom implements TagAxiom {
private feedback_ = false;
private subs_: Detachable[] = [];
private tag_: Tag
constructor (tag: Tag) {
this.tag_ = tag;
}
attach (element: Element) {
const inputElement: HTMLInputElement = element as HTMLInputElement;
const vao = this.tag_.attributes.value;
inputElement.value = vao.get();
element.addEventListener('change', () => {
this.feedback_ = true;
vao.set(inputElement.value);
this.feedback_ = false;
})
// NEXT: doing subs next
this.subs_.push(vao.sub('change', () => {
if ( this.feedback_ ) return;
inputElement.value = vao.get();
}))
}
detach () {
for ( const sub of this.subs_ ) sub.detach();
}
}
class BindCheckedAxiom implements TagAxiom {
private feedback_ = false;
private subs_: Detachable[] = [];
private tag_: Tag
constructor (tag: Tag) {
this.tag_ = tag;
}
attach (element: Element) {
const inputElement: HTMLInputElement = element as HTMLInputElement;
const vao = this.tag_.attributes.value;
inputElement.value = vao.get();
element.addEventListener('change', () => {
this.feedback_ = true;
vao.set(inputElement.checked);
this.feedback_ = false;
})
// NEXT: doing subs next
this.subs_.push(vao.sub('change', () => {
if ( this.feedback_ ) return;
inputElement.checked = !! vao.get();
}))
}
detach () {
for ( const sub of this.subs_ ) sub.detach();
}
}
p({
predicates: [
IS_AN_INPUT_TAG,
ATTR_VALUE_IS('type', 'text'),
ATTR_VALUE_IS_VAO
],
execute: (tag) => {
tag.install(new BindInputValueAxiom(tag));
}
});
p({
predicates: [
IS_AN_INPUT_TAG,
ATTR_VALUE_IS('type', 'checkbox'),
ATTR_VALUE_IS_VAO
],
execute: (tag) => {
tag.install(new BindCheckedAxiom(tag));
}
});

@ -0,0 +1,26 @@
import { AbstractVAO } from "./VAO.js";
export class ScalarVAO extends AbstractVAO {
private value_: any
protected getTopics_(): string[] {
return ['change'];
}
constructor (initialValue: any) {
super();
this.value_ = initialValue;
}
set (value: any) {
let o = this.value_;
this.value_ = value;
if ( o !== this.value_ ) {
this.pub('change');
}
}
get (): any {
return this.value_;
}
}

@ -0,0 +1,85 @@
export interface Detachable {
detach: () => void
}
type TListenerNode = {
next?: TListenerNode | null,
prev?: TListenerNode | null,
listener?: (event: TValueEvent) => void,
}
type TListenerList = {
[topic: string]: TListenerNode[]
}
export type TValueEvent = {
vao: VAO,
value: any,
extra: any,
}
export interface VAO {
sub (topic: string, listener: () => void): Detachable
pub (topic: string, extra: any): void
get (): any
set (value: any): void
}
export abstract class AbstractVAO implements VAO {
private listeners_: TListenerList
constructor () {
this.listeners_ = {};
const topics = this.getTopics_();
for ( const topic of topics ) {
this.listeners_[topic] = [
{ next: null }
];
}
}
protected abstract getTopics_(): string[]
abstract get (): any
abstract set (value: any): void
sub (topic: string, listener: () => void): Detachable {
const head = this.listeners_[topic][0];
const node: TListenerNode = {
next: head.next,
prev: head,
listener
};
if ( node.next ) {
node.next.prev = node;
}
head.next = node;
return {
detach: () => {
if ( node.prev ) node.prev.next = node.next;
if ( node.next ) node.next.prev = node.prev;
node.prev = null;
}
}
}
pub (topic: string, extra?: any) {
let node = this.listeners_[topic][0];
const event = {
...(extra || {}),
vao: this,
value: this.get()
};
while ( node.next ) {
node = node.next;
if ( node.listener === undefined ) {
continue;
}
node.listener(event);
}
}
}

@ -10,7 +10,11 @@
"allowJs": true,
"strict": true,
"isolatedModules": true,
"esModuleInterop": true
"esModuleInterop": true,
"lib": [
"es2019",
"dom"
]
},
"include": ["./src/"]
}

@ -4,5 +4,6 @@
"module": "ESNext",
"outDir": "dist/mjs",
"target": "ESNext"
}
},
"include": ["./src/"]
}

Loading…
Cancel
Save