/** * @class Represents a dynamic attribute or element */ class Dynamic { /** * @constructor * @param {() => any} callback */ constructor(callback) { this.callback = callback } } /** * @class Represents a variable state */ class State { /** * @constructor * @param {any} defaultValue * @protected */ constructor(defaultValue = undefined) { /** * @type {any} * @public * The value of this state */ this.value = defaultValue /** * @type {Renderable[]} * @protected * The renderable linked to this state */ this.renderables = [] } /** * @param {Renderable} renderable * @protected */ addRenderable(renderable) { this.renderables.push(renderable) } /** * Updates the value of the state * @param {any} newValue * @public */ update(newValue) { this.value = newValue for (const renderable of this.renderables) { renderable.render() renderable.update() } } /** * @returns {String} * @public */ toString() { return this.value.toString() } } /** * @class Represents a rendererable component or html element */ class Renderable { /** * @constructor * @param {Renderable[]} children * @protected */ constructor(children) { if (!children) children = [] /** * The parent renderable of this renderable * @type {HTMLElement | DocumentFragment} * @protected */ this.parentRenderable = null /** * The element that is currently in the dom * @type {HTMLElement | DocumentFragment} * @protected */ this.domElement = null /** * The element from the most recent render * @type {HTMLElement | DocumentFragment} * @protected */ this.renderedElement = null /** * The elements children * @type {Renderable[]} * @protected */ this.children = children for (const child of this.children) { if (child instanceof App) { throw new Error('App must be at root of component tree') } } } /** * @protected * @override */ render() { /* Render all children */ for (const child of this.children) { child.render() child.parentRenderable = this child.domElement = child.renderedElement this.renderedElement.appendChild(child.renderedElement) } } /** * @protected * @override */ update() { this.domElement.replaceWith(this.renderedElement) this.domElement = this.renderedElement } } /** * @class Representsa HTML Element */ class Element extends Renderable { /** * @constructor * @protected * @param {String} type * @param {any} attrs * @param {Renderable[]} children */ constructor(type, attrs, children) { // call parent constructor super(children) /** * The element type * @type {String} * @protected */ this.type = type /** * The element attributes * @type {any} * @protected */ this.attrs = attrs if (this.attrs['signals']) { for (const signal of this.attrs['signals']) { signal.addRenderable(this) } } } /** * @protected * @override */ render() { let el; if (this.type == 'path' || this.type == 'g' || this.type == 'svg') { let svgns = "http://www.w3.org/2000/svg"; el = document.createElementNS(svgns, this.type); } else { el = document.createElement(this.type); } for (const key in this.attrs) { let val = this.attrs[key] if (key == 'signals') { continue } if (val instanceof Dynamic) val = val.callback() if (key.startsWith("on")) { el[key] = val } else { el.setAttribute(key, val) } } this.renderedElement = el super.render() } } /** * @class Represents raw content to be added to HTML */ class Content extends Renderable { /** * @constructor * @param {() => any} callback * @param {State[]} states * @protected */ constructor(callback, states) { super([]) /** * @type {() => any} * @protected */ this.callback = callback for (const state of states) { state.addRenderable(this) } } /** * @protected * @override */ render() { this.renderedElement = document .createRange() .createContextualFragment(this.callback()) } /** * @protected * @override */ update() { this.parentRenderable.render() this.parentRenderable.update() } } class SelectableRenderable extends Renderable { /** * @constructor * @protected */ constructor () { super([]) /** * @type {Renderable} * @protected */ this.selectedElement = null } /** * @protected * @override */ render() { this.selectedElement.render() this.renderedElement = this.selectedElement.renderedElement this.selectedElement.parentRenderable = this.parentRenderable super.render() } } var routerCallbacks = [] export var routeSignal = state(document.location.pathname.toString()) /** * @class Represends a page router */ class Router extends SelectableRenderable { /** * @constructor * @param {(string) => Renderable} callback * @protected */ constructor (callback) { super() /** * @type {(string) => Renderable} * @protected */ this.callback = callback /** * @protected */ this.location = document.location.pathname.toString() routerCallbacks.push(() => { this.location = document.location.pathname.toString() this.render() this.update() }) } /** * @protected * @override */ render() { super.selectedElement = this.callback(this.location.toString()) super.render() } } /** * @class Represends a component switch */ class Conditional extends SelectableRenderable { /** * @constructor * @param {() => any} conditional * @param {{[key: any]: Renderable}} cases * @param {State[]} states * @protected */ constructor (conditional, cases, states) { super() /** * @protected */ this.conditional = conditional /** * @protected */ this.cases = cases /** * @protected */ this.states = states } /** * @protected * @override */ render() { let value = this.conditional() this.renderedElement = null for (const [key, element] of Object.entries(this.routes)) { if (key === value) { super.selectedElement = element break; } } super.render() } } class App extends Renderable { /** * @constructor * @param {Renderable} container * @protected */ constructor(container) { super([]) /** * @type {Renderable} * @protected */ this.container = container } /** * Renders the app into the DOM * @param {HTMLElement} parent - the parent element to place the app * @public * @override */ render(parent) { this.container.render() this.container.parentRenderable = parent if (this.container.domElement) this.container.domElement.remove() this.container.domElement = this.container.renderedElement parent.appendChild(this.container.domElement) } /** * @private * @override */ update() { /* empty */ } } /** * @returns {App} * @param {Renderable} container */ export function app(container) { return new App(container) } /** * @returns {Dynamic} * @param {() => any} callback */ export function dynamic(callback) { return new Dynamic(callback) } /** * @returns {State} * @param {any} defaultValue */ export function state(defaultValue) { return new State(defaultValue) } /** * @param {() => string} callback * @param {...State} states * @returns {Content} */ export function content(callback, ...states) { return new Content(callback, states) } /** * @param {(string) => Renderable} callback * @returns {Router} */ export function router(callback) { return new Router(callback) } /** * @param {() => any} conditional * @param {{[key: any]: Renderable}} cases * @param {...State} states * @returns {Conditional} */ export function conditional(conditional, cases, ...states) { return new Conditional(conditional, cases, states) } export function backLocation() { window.history.back() routeSignal.update(document.location.pathname.toString()) for (const callback of routerCallbacks) callback() } export function forwardLocation() { window.history.forward() routeSignal.update(document.location.pathname.toString()) for (const callback of routerCallbacks) callback() } export function pushLocation(newLocation) { window.history.pushState({}, '', newLocation) routeSignal.update(document.location.pathname.toString()) for (const callback of routerCallbacks) callback() } export function replaceLocation(newLocation) { window.history.replaceState({}, '', newLocation) routeSignal.update(document.location.pathname.toString()) for (const callback of routerCallbacks) callback() } /** * Returns a HTML paragraph * @param {{[key: string]: any}} attrs - a map of the HTML attributes * @param {Renderable[]} children - the elements children */ export function p(attrs, ...children) { return new Element("p", attrs, children) } /** * Returns a HTML span * @param {{[key: string]: any}} attrs - a map of the HTML attributes * @param {Renderable[]} children - the elements children */ export function span(attrs, ...children) { return new Element("span", attrs, children) } /** * Returns a HTML div * @param {{[key: string]: any}} attrs - a map of the HTML attributes * @param {Renderable[]} children - the elements children */ export function div(attrs, ...children) { return new Element("div", attrs, children) } /** * Returns a HTML anchor * @param {{[key: string]: any}} attrs - a map of the HTML attributes * @param {Renderable[]} children - the elements children */ export function a(attrs, ...children) { return new Element("a", attrs, children) } /** * Returns a HTML icon * @param {{[key: string]: any}} attrs - a map of the HTML attributes * @param {Renderable[]} children - the elements children */ export function i(attrs, ...children) { return new Element("i", attrs, children) } /** * Returns a HTML form * @param {{[key: string]: any}} attrs - a map of the HTML attributes * @param {Renderable[]} children - the elements children */ export function form(attrs, ...children) { return new Element("form", attrs, children) } /** * Returns a HTML img * @param {String} alt - the alt tag * @param {{[key: string]: any}} attrs - a map of the HTML attributes * @param {Renderable[]} children - the elements children */ export function img(alt, attrs, children) { attrs['onerror'] = (event) => event.target.remove() attrs['alt'] = alt return new Element("img", attrs, children) } /** * Returns a HTML input * @param {{[key: string]: any}} attrs - a map of the HTML attributes * @param {Renderable[]} children - the elements children */ export function input(attrs, ...children) { return new Element("input", attrs, children) } /** * Returns a HTML button * @param {{[key: string]: any}} attrs - a map of the HTML attributes * @param {Renderable[]} children - the elements children */ export function button(attrs, ...children) { return new Element("button", attrs, children) } /** * Returns a HTML path * @param {{[key: string]: any}} attrs - a map of the HTML attributes * @param {Renderable[]} children - the elements children */ export function path(attrs, ...children) { return new Element("path", attrs, children) } /** * Returns a HTML g tah * @param {{[key: string]: any}} attrs - a map of the HTML attributes * @param {Renderable[]} children - the elements children */ export function g(attrs, ...children) { return new Element("g", attrs, children) } /** * Returns a HTML svg * @param {{[key: string]: any}} attrs - a map of the HTML attributes * @param {Renderable[]} children - the elements children */ export function svg(attrs, ...children) { return new Element("svg", attrs, children) } /** * Returns a HTML text area * @param {{[key: string]: any}} attrs - a map of the HTML attributes * @param {Renderable[]} children - the elements children */ export function textarea(attrs, ...children) { return new Element("textarea", attrs, children) }