Skip to content

Instantly share code, notes, and snippets.

@sc0ttj
Last active March 24, 2020 11:32
Show Gist options
  • Save sc0ttj/e5949f68befe097f7e2bda105e5429c4 to your computer and use it in GitHub Desktop.
Save sc0ttj/e5949f68befe097f7e2bda105e5429c4 to your computer and use it in GitHub Desktop.
Vanilla JS components
// 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)
@sc0ttj
Copy link
Author

sc0ttj commented Mar 21, 2020

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment