commit 02e1011284477656ad306db0f4c7542456e70675 Author: Freya Murphy Date: Mon Jan 22 18:18:28 2024 -0500 frameworks diff --git a/framework.js b/framework.js new file mode 100644 index 0000000..084a9a7 --- /dev/null +++ b/framework.js @@ -0,0 +1,639 @@ +/** + * @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) +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..957bcc3 --- /dev/null +++ b/readme.md @@ -0,0 +1,128 @@ +## framework +another js framework + +### components + +there are 5 major components for this framework + +#### element + +The most basic component. An `element` represends any HTMLElement. +These can be created with any of the functions named after their html functions, i.e. `a`, `div`, `textarea`, etc. Elements can hold other elements, contents, and routers as children. + +To make a component, (most) require 2 paramaters. First, its attributes, and second its children. + +```js +# Usage +# element(attrs, ...children) + +div({ + style: 'background-color: red;' +}, + a({ href: 'google.com'}) +) +``` + +#### content + +The content type is how you add dynamic strings and insert content into the dom. +You cannot just add text as a child directly to any element, it must be wrapped in a +content tag. + +The content tag tags a single callback that returns a string. Its called on every render. + +```js +// Usage +// content(callback, ...states) + +content(() => 'Hello world!') +``` + +#### state + +State is how you create signals and store variables a reactive way. When a state object is updated, +it causes every content and element its attached to, to update. + +It takes one argument, the default value of the state; + +```js +// Usage +// state(defaultValue) + +let test = state('test') +test.value // get value +test.update('test2') // update value +``` + +Also you need to attach the state object to any element or content that uses it. Otherwise they wont update when changed. To do this add them to the `signals` attribute on any `element`, or pass them in after the callback in any `content`. + +```js +// Usage + +content(() => `${test.value}`, test); + +// or + +div({ + signals: [test], + style: `background-color: ${test.value}` +}) +``` + +#### router + +The router is what allows you to switch between pages reactivly based on the location. + +To change location use the following functions + +```js +backLocation() // go back +forwardLocation() // go forward + +pushLocation('/newPath') // append location to history, and goto newPath +replaceLocation('/newPath') // replace location in history, and goto newPath +``` + +To create a router you must pass in a callback that takes in the current location, and returns either a element, content, or another router. Routers will auto update on location change. + +```js +// Usage +// router(callback) + +const component() => { + return div({}, content(() => 'hello world!')) +} + +const notFound() => { + return div({}, content(() => 'not found :(')) +} + +router((path) => { + switch(path) { + case: '/secret': + return component() + default: + retrun notFound() + } +}) +``` + +#### app + +The final type is the App. This is the highest component in the component tree, and requires a signle router, element, or content to manage at the top of the tree. + +You are only allowed to make a single app, since they arent gurenteed to work with multiple. + +```js +// Usage +// app(root) + +let mySite = app(router((path) => { /* ... */ })) +mySite.render(document.body) +``` + +### todo + +- page titles +- component arrays +- likly other things