frameworks

This commit is contained in:
Murphy 2024-01-22 18:18:28 -05:00
commit 02e1011284
No known key found for this signature in database
GPG key ID: 988032A5638EE799
2 changed files with 767 additions and 0 deletions

639
framework.js Normal file
View file

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

128
readme.md Normal file
View file

@ -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