summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFreya Murphy <freya@freyacat.org>2024-01-22 18:18:28 -0500
committerFreya Murphy <freya@freyacat.org>2024-01-22 18:18:28 -0500
commit02e1011284477656ad306db0f4c7542456e70675 (patch)
treeaa79b6fa9275a680467f3e1f621a2b7e7e26faf0
downloadjsframework-02e1011284477656ad306db0f4c7542456e70675.tar.gz
jsframework-02e1011284477656ad306db0f4c7542456e70675.tar.bz2
jsframework-02e1011284477656ad306db0f4c7542456e70675.zip
frameworks
-rw-r--r--framework.js639
-rw-r--r--readme.md128
2 files changed, 767 insertions, 0 deletions
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