Last active
March 24, 2020 11:32
-
-
Save sc0ttj/e5949f68befe097f7e2bda105e5429c4 to your computer and use it in GitHub Desktop.
Vanilla JS components
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Related JS features/topics
Data binding
Batched rendering
Related projects:
Template literals to DOM nodes:
DOM diffing:
Template literals to VDOM nodes:
HTML to VDOM nodes:
VDOM diffing:
VDOM to HTML
naistran/vdom-to-html
JSX renderers:
All-round:
JSX to VDOM to DOM
JSX to real DOM nodes:
JSX to HTML string:
html
function)Routing
State handlers (like redux)
Component frameworks:
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