jsframework/framework.js
2024-01-22 18:18:28 -05:00

640 lines
12 KiB
JavaScript

/**
* @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)
}