frameworks
This commit is contained in:
commit
02e1011284
2 changed files with 767 additions and 0 deletions
639
framework.js
Normal file
639
framework.js
Normal 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
128
readme.md
Normal 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
|
Loading…
Reference in a new issue