-
-
Save sc0ttj/e5949f68befe097f7e2bda105e5429c4 to your computer and use it in GitHub Desktop.
// Vanilla JS components example | |
// | |
// Note: This is just a demo/experiment | |
// Goals: Super easy to setup, small code base, automatic re-renders on state update | |
// | |
// Component() features: | |
// * a state management thing | |
// * a state history | |
// * automatic re-rendering on state change (no diffing!) | |
// * ability to "time travel" to prev/next states | |
// * update states using simple "actions" | |
function Component(state) { | |
this.reactive = true | |
this.log = false | |
this.state = state | |
this.history = [ { index: 0, state: state, action: "init" } ] | |
this.setState = newState => { | |
this.previousState = this.state | |
this.state = { ...this.state, ...newState } | |
if (this.reactive) this.render(this.container) | |
if (this.currentIndex === this.history.length) { | |
this.history.push({ index: this.history.length, state: this.previousState, action: this.action || 'setState' }) | |
} | |
this.currentIndex = this.history.length | |
if (this.log) console.log(this.currentIndex, [this.state, ...this.history]) | |
} | |
this.travel = function(num, direction) { | |
var newIndex; | |
if (direction === "f") { | |
newIndex = this.currentIndex + num | |
} | |
else { | |
newIndex = this.currentIndex - num | |
} | |
this.setState(this.history[newIndex].state) | |
this.currentIndex = newIndex | |
} | |
this.rewind = function(num) { | |
if (!num) { | |
this.setState(this.history[0].state) | |
this.currentIndex = 0 | |
return true | |
} | |
this.travel(num, "b") | |
} | |
this.forward = function (num) { | |
if (!num) { | |
this.setState(this.history[this.history.length - 1].state) | |
this.currentIndex = this.history.length - 1 | |
return true | |
} | |
this.travel(num, "f") | |
} | |
this.undo = function() { this.rewind(1); } | |
this.redo = function() { this.forward(1); } | |
this.render = function(container) { | |
var el = container | |
var view = this.view(this.state) | |
if (typeof document !== "undefined" && document.querySelector) { | |
if (typeof el === "string") el = document.querySelector(el) | |
// should do diffing here! | |
el.innerHTML = view | |
} | |
this.container = el | |
} | |
return this; | |
} | |
// ----------------------------------------------------------------------------- | |
// -------- USAGE: Defining components ----------- | |
// Define our app state | |
var state = { | |
count: 0, | |
incrementBy: 5, | |
id: "foo-id", | |
items: [ | |
{ name: "Item one" }, | |
{ name: "Item two" }, | |
] | |
} | |
// Define some generic, re-usable, stateless "sub-components" | |
var Heading = (text) => `<h1>${text}</h1>` | |
var List = (items) => `<ul>${items.map(item => `<li>${item.name}</li>`).join('')}</ul>` | |
var Button = (label, fn) => `<button onclick=${fn}>${label}</button>` | |
// Define a stateful main component | |
var App = new Component(state); | |
// Define some events | |
App.clickBtn = (props) => App.setState({ count: App.state.count + props}) | |
// Define a view - include our sub-components | |
App.view = (props) => ` | |
<div id=${props.id}> | |
${Heading(`Total so far = ${props.count}`)} | |
${List(props.items)} | |
${Button('Click here', `App.clickBtn(${props.incrementBy})`)} | |
</div>` | |
// ---- USAGE: Using the component ----- | |
// Add our app to the page | |
App.render('body') | |
// Using setState() to trigger a full re-render | |
App.setState({ | |
items: [ { name: "nob" } ], | |
}) | |
App.log = true // enable logging of state changes in console/DevTools | |
App.log = false // disable logging of state changes in console/DevTools | |
App.reactive = false // disable auto re-render on state changes | |
App.reactive = true // enable auto re-render on state changes | |
// ---- OPTIONAL: Using "actions" to update the state more easily ---- | |
// Define "actions" that will update our App state in specific ways... | |
App.update = function(action, newState) { | |
this.action = action | |
switch (action) { | |
case 'state': | |
this.setState(newState) | |
break | |
case 'increment': | |
this.setState({ ...this.state, count: this.state.count + newState}) | |
break | |
case 'decrement': | |
this.setState({ ...this.state, count: this.state.count - newState}) | |
break | |
case 'items': | |
this.setState({ ...this.state, items: newState }) | |
break | |
case 'addItems': | |
this.setState({ ...this.state, items: [ ...this.state.items, ...newState ] }) | |
break | |
} | |
this.action = undefined | |
} | |
// ...Then call the 'action' to trigger a re-render | |
App.update('state', { title: "333" }) | |
App.update('state', { title: "666" }) | |
App.update('increment', 105) | |
App.update('decrement', 5) | |
App.update('items', [ { name: "one" }, { name: "two" } ]) | |
App.update('addItems', [ { name: "Item three"}, { name: "Item four" } ]) | |
// ---- OPTIONAL: Using the state "timeline" ---- | |
// Take a "snapshot" (we'll use it later) | |
var snapshot = App.state | |
App.rewind() // go to initial state | |
App.forward() // go to latest state | |
App.rewind(2) // rewind two steps to a previous state | |
App.forward(2) // fast-forward two steps to a more current state | |
App.undo() // same as App.rewind(1) | |
App.redo() // same as App.forward(1) | |
// Set a previous state | |
App.setState(App.history[2].state) | |
// Set a "named" state, from a previous point in time | |
App.setState(snapshot) |
Related JS features/topics
Data binding
- Overwrite an objects getters and getters
- Observables
- Mutation Observer
- choojs/object-change-callsite - determine where a change to the given object comes from
Batched rendering
- use requestAnimationFrame to queue render() calls
Related projects:
-
Template literals to DOM nodes:
- bel
- choojs/nanohtml - very nice, small, can use JSDOM in node environments
- loilo-archive/domify-template-string
- kapouer/dom-template-strings
-
DOM diffing:
- morphdom - faster than nanomorph, optionally supports diffing VDOMs (see marko-vdom, below)
- choojs/nanomorph = hyper fast diffing of real DOM nodes
- diffhtml
-
Template literals to VDOM nodes:
- hyperx
- gvergnaud/vdom-tag
- yoshuawuyts/vel
- mreinstein/snabby - small, fast, uses snabbdom (used by Vue.js), support diffing/patching
- marko-js/marko-vdom - very fast, for use with morphdom only... see Dom Diffing->morphdom, above..
-
HTML to VDOM nodes:
- yoshuawuyts/virtual-html
- hemanth/to-vdom - supports from Node or string to VDOM
- TimBeyer/html-to-vdom - supports from Node or string to VDOM
- marcelklehr/vdom-virtualize - from DOM Node to VDOM only (no string)
-
VDOM diffing:
- Matt-Esch/virtual-dom - quite large, but fast
- Raynos/main-loop - batched VDOM diffing, using requestAnimationFrame
- marko-js/marko-vdom - very fast, for use with morphdom only!
-
VDOM to HTML
-
naistran/vdom-to-html
-
JSX renderers:
-
All-round:
- krakenjs/jsx-pragmatic - renders JSX to HTML string, DOM nodes or React Element
-
JSX to VDOM to DOM
- composor/nano-byte - 1kb, JSX to VDOM with 'h()', VDOM to DOM with 'render()'
-
JSX to real DOM nodes:
- proteriax/jsx-dom - Use JSX to create DOM elements
- spaceface777/JSXLite - Use JSX to create DOM elements, zero deps, 350 bytes gzipped
-
JSX to HTML string:
- developit/vhtml - provides 'h' pragma, which converts JSX to HTML Strings (no need to tag templates), no VDOM, adds support for child Components
- AntonioVdlC/html-template-tag
- zspecza/common-tags (see
html
function)
-
-
Routing
- krasimir/navigo - nice little router, with fallback to "hashchange" if no History API
- narirou/hasher - uses "hashchange" API, simple API
- chrisdavies/rlite - zero deps, 800 bytes, parses query strings, wildcard support
- choojs/nanorouter - small router, 1kb or so
- choojs/wayfarer, similar to above, bit more complex
- flatiron/director - similar to above, also supports server-side (process HTTP requests) and CLI (process.argv) routing
- visionmedia/page.js - bigger, more complex, has plugins, advanced features
-
State handlers (like redux)
- ?
-
Component frameworks:
- maxogden/yo-yo (usese bel, morphdom)
- choojs/choo (uses yo-yo, adds routing, etc)
- stanchino/tiny-jsx - includes JSX pragma, router, mimics Reacts useState() and UseEffect()
- i-like-robots/hyperons - JSX to HTML string, 2 functions, equivalent to React.createElement() and ReactDOM.renderToString()
-
Other:
-
schwarzkopfb/is-tagged - see if your function is running as a template tag or not
-
shama/yo-yoify - transpiles bel/yo-yo/choo template literals into native
document.createElement()
calls,
can be used to compile fast, pure JS templates, that also work in older browsers -
Converting HTML strings to DOM nodes using Fragments: https://davidwalsh.name/convert-html-stings-dom-nodes
-
developit/htm - accepts template literals instead of JSX.. bind it to something that renders VDOM or DOM
-
Changelog:
v0.0.2
mount
function (userender
)App.rewind()
,App.undo()
,App.forward()
,App.redo()
App.reactive = true|false
andApp.log = true|false
v0.0.1