diff --git a/src/app/app.ts b/src/app/app.ts index a5c638e6..f323c857 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -2,6 +2,8 @@ import { RootNode } from '../nodes/RootNode' import { EnumNode } from '../nodes/EnumNode' import { ObjectNode } from '../nodes/ObjectNode' import { StringNode } from '../nodes/StringNode' +import { DataModel } from '../model/DataModel' + const EntityCollection = ['sheep', 'pig'] @@ -13,14 +15,12 @@ const predicateTree = new RootNode('predicate', { type: new EnumNode(EntityCollection, {}), nbt: new StringNode() }) +}, { + default: () => ({ condition: 'foo', predicate: { nbt: 'hi' } }) }); -let dummyData = { - condition: 'foo', - predicate: { - nbt: 'hi' - } -} +const model = new DataModel(predicateTree) -document!.getElementById('view')!.innerHTML = predicateTree.render('', dummyData) -document!.getElementById('source')!.textContent = predicateTree.transform(dummyData) +model.render(document!.getElementById('view')!) + +// document!.getElementById('source')!.textContent = predicateTree.transform(dummyData) diff --git a/src/model/DataModel.ts b/src/model/DataModel.ts new file mode 100644 index 00000000..0f0ed496 --- /dev/null +++ b/src/model/DataModel.ts @@ -0,0 +1,38 @@ +import { RootNode } from "../nodes/RootNode" +import { Path } from "./Path" + +type Registry = { + [id: string]: (el: Element) => void +} + +const registryIdLength = 12 +const dec2hex = (dec: number) => ('0' + dec.toString(16)).substr(-2) + +export function getId() { + var arr = new Uint8Array((registryIdLength || 40) / 2) + window.crypto.getRandomValues(arr) + return Array.from(arr, dec2hex).join('') +} + +export class DataModel { + private data: any + private schema: RootNode + private registry: Registry = {} + + constructor(schema: RootNode) { + this.schema = schema + this.data = schema.default + } + + register(id: string, callback: (el: Element) => void) { + this.registry[id] = callback + } + + render(target: HTMLElement) { + target.innerHTML = this.schema.render(new Path(), this.data, this) + for (const id in this.registry) { + const element = target.querySelector(`[data-id="${id}"]`) + if (element !== null) this.registry[id](element) + } + } +} diff --git a/src/model/Path.ts b/src/model/Path.ts new file mode 100644 index 00000000..14ece07a --- /dev/null +++ b/src/model/Path.ts @@ -0,0 +1,26 @@ + +type PathElement = (string | number) + +export class Path { + private arr: PathElement[] + + constructor(arr?: PathElement[]) { + this.arr = arr || [] + } + + last(): PathElement { + return this.arr[this.arr.length - 1] + } + + pop(): Path { + return new Path(this.arr.slice(0, -1)) + } + + push(element: PathElement): Path { + return new Path([...this.arr, element]) + } + + copy(): Path { + return new Path([...this.arr]) + } +} diff --git a/src/nodes/AbstractNode.ts b/src/nodes/AbstractNode.ts index e297410c..c2b20b88 100644 --- a/src/nodes/AbstractNode.ts +++ b/src/nodes/AbstractNode.ts @@ -1,23 +1,28 @@ +import { DataModel, getId } from "../model/DataModel" +import { Path } from "../model/Path" export interface INode { setParent: (parent: INode) => void transform: (value: T) => any - render: (field: string, value: T) => string + render: (path: Path, value: T, model: DataModel) => string } -export interface NodeChildren { +export type NodeChildren = { [name: string]: INode } -export interface NodeMods { +export type NodeMods = { + default?: () => T transform?: (value: T) => any } export abstract class AbstractNode implements INode { - private transformMod = (v: T) => v - protected parent?: INode + parent?: INode + default?: T + transformMod = (v: T) => v constructor(mods?: NodeMods) { + if (mods?.default) this.default = mods.default() if (mods?.transform) this.transformMod = mods.transform } @@ -25,9 +30,24 @@ export abstract class AbstractNode implements INode { this.parent = parent } + wrap(path: Path, model: DataModel, renderResult: string): string { + const id = getId() + model.register(id, el => { + console.log(`Callback ${id} -> ${path.last()}`) + this.mounted(el, path, model) + }) + return `
${renderResult}
` + } + + mounted(el: Element, path: Path, model: DataModel) { + el.addEventListener('change', evt => this.updateModel(el, path, model)) + } + + updateModel(el: Element, path: Path, model: DataModel) {} + transform(value: T) { return this.transformMod(value) } - abstract render(field: string, value: T): string + abstract render(path: Path, value: T, model: DataModel): string } diff --git a/src/nodes/EnumNode.ts b/src/nodes/EnumNode.ts index d2a56152..452f971c 100644 --- a/src/nodes/EnumNode.ts +++ b/src/nodes/EnumNode.ts @@ -1,4 +1,6 @@ import { AbstractNode, NodeMods } from './AbstractNode' +import { DataModel } from '../model/DataModel' +import { Path } from '../model/Path' export class EnumNode extends AbstractNode { protected options: string[] @@ -8,10 +10,13 @@ export class EnumNode extends AbstractNode { this.options = options } - render (field: string, value: string) { - return `${field} + updateModel(el: Element, path: Path, model: DataModel) { + } + + render(path: Path, value: string, model: DataModel) { + return this.wrap(path, model, `${path.last()} ` + `) } } diff --git a/src/nodes/ObjectNode.ts b/src/nodes/ObjectNode.ts index d34ebd5d..c47c2446 100644 --- a/src/nodes/ObjectNode.ts +++ b/src/nodes/ObjectNode.ts @@ -1,4 +1,6 @@ import { AbstractNode, NodeChildren, NodeMods } from './AbstractNode' +import { DataModel } from '../model/DataModel' +import { Path } from '../model/Path' export interface IObject { [name: string]: any @@ -15,10 +17,6 @@ export class ObjectNode extends AbstractNode { }) } - getFields() { - return this.fields - } - transform(value: IObject) { if (value === undefined) return undefined value = value || {} @@ -29,12 +27,12 @@ export class ObjectNode extends AbstractNode { return res; } - render(field: string, value: IObject) { + render(path: Path, value: IObject, model: DataModel) { if (value === undefined) return `` - return `${field}:
+ return this.wrap(path, model, `${path.last()}:
${Object.keys(this.fields).map(f => { - return this.fields[f].render(f, value[f]) - }).join('
')} -
` + return this.fields[f].render(path.push(f), value[f], model) + }).join('')} +
`) } } diff --git a/src/nodes/RootNode.ts b/src/nodes/RootNode.ts index 75758641..c84df6ca 100644 --- a/src/nodes/RootNode.ts +++ b/src/nodes/RootNode.ts @@ -1,10 +1,12 @@ import { NodeChildren, NodeMods } from './AbstractNode' import { ObjectNode, IObject } from './ObjectNode' +import { Path } from '../model/Path' +import { DataModel } from '../model/DataModel' export class RootNode extends ObjectNode { - private id: string + id: string - constructor(id: string, fields: NodeChildren, mods?: NodeMods) { + constructor(id: string, fields: NodeChildren, mods?: NodeMods) { super(fields, mods) this.id = id } @@ -13,12 +15,12 @@ export class RootNode extends ObjectNode { return JSON.stringify(super.transform(value)) } - render(field: string, value: IObject) { + render(path: Path, value: IObject, model: DataModel) { value = value || {} return `
${Object.keys(this.fields).map(f => { - return this.fields[f].render(f, value[f]) - }).join('
')} + return this.fields[f].render(path.push(f), value[f], model) + }).join('')}
` } } diff --git a/src/nodes/StringNode.ts b/src/nodes/StringNode.ts index 9c033166..7ebb8f21 100644 --- a/src/nodes/StringNode.ts +++ b/src/nodes/StringNode.ts @@ -1,11 +1,17 @@ import { AbstractNode, NodeMods } from './AbstractNode' +import { Path } from '../model/Path' +import { DataModel } from '../model/DataModel' export class StringNode extends AbstractNode { constructor(mods?: NodeMods) { super(mods) } - render(field: string, value: string) { - return `${field} ${value || ''}` + updateModel(el: Element, path: Path, model: DataModel) { + } + + render(path: Path, value: string, model: DataModel) { + return this.wrap(path, model, + `${path.last()} `) } }