-
-
Save evatrium/8e76d782570b51ddbca64809d0bf1669 to your computer and use it in GitHub Desktop.
1.26kb tiny vdom inspired by https://twitter.com/_developit/status/1232891191110389760
This file contains hidden or 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
import {h, render, Fragment, Host} from './little-vdom'; | |
import {someSubscription, anotherSub} from './store'; | |
const CounterAndSubscriber = (props, {count = 0}, update) => { | |
return( // in order to achieve hook-like functionality | |
// you can tie into the web component lifecycle | |
<Host lifeCycle={() => { // | |
// call update to rerender this component | |
let unsub = someSubscription(update); | |
return () => unsub(); //unsubscribes when unmounted*/ | |
}}> | |
<h1> | |
{someSubscription.latestValue} | |
</h1> | |
<button onClick={() => update({count: count + 1})}> | |
inc me!! : {count} | |
</button> | |
</Host> | |
) | |
} | |
const Subscriber = (props, state, update) => ( | |
<Host lifeCycle={() => [someSubcription(update), anotherSub(update)]}> | |
<h1> | |
{someSubscription.latestValue} | |
</h1> | |
</Host> | |
) | |
const Counter = (props, {count = 0}, update) => ( | |
<button onClick={() => update({count: count + 1})}> | |
Counter count: {count} | |
</button> | |
) | |
const App = (props, {show = false}, update) => { | |
return ( | |
<Fragment> | |
<Subscriber/> | |
<button onClick={() => update({show: !show})}> | |
toggle counter | |
</button> | |
{ | |
show && <Counter/> | |
} | |
{/* another counter*/} | |
<CounterAndSubscriber/> | |
</Fragment> | |
) | |
} | |
render(<App/>, document.body); |
This file contains hidden or 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
let isFunc = f => typeof f === 'function', | |
eventProxy = function (e) { | |
return this.__ev[e.type](e); | |
}, | |
setStyleProp = (style, key, value) => | |
key[0] === '-' | |
? style.setProperty(key, value) | |
: ( | |
style[key] = typeof value === 'number' | |
&& /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|^--/i.test(key) === false | |
? value + 'px' | |
: value == null ? '' : value | |
), | |
setStyle = (dom, value, oldValue, _style = dom.style) => { | |
if (typeof value === 'string') return _style.cssText = value; | |
typeof oldValue === 'string' && (_style.cssText = '', oldValue = null); | |
if (oldValue) for (let i in oldValue) if (!(value && i in value)) setStyleProp(_style, i, ''); | |
if (value) for (let i in value) if (!oldValue || value[i] !== oldValue[i]) setStyleProp(_style, i, value[i]); | |
}, | |
//preact-ish set property. not supporting SVG | |
setProperty = (dom, name, value, oldValue, _style, _useCapture, _nameLower, _newHTML, _skip) => | |
(( | |
// if style | |
name === 'style' ? setStyle(dom, value, oldValue) | |
// if event | |
: name[0] === 'o' && name[1] === 'n' ? ( | |
_useCapture = name !== (name = name.replace(/Capture$/, '')), | |
_nameLower = name.toLowerCase(), | |
name = (_nameLower in dom ? _nameLower : name).slice(2), | |
value ? ( | |
!oldValue && dom.addEventListener(name, eventProxy, _useCapture), | |
(dom.__ev || (dom.__ev = {}))[name] = value | |
) : dom.removeEventListener(name, eventProxy, _useCapture) | |
) | |
// if html | |
: name === 'dangerouslySetInnerHTML' && (value || oldValue) ? ( | |
(!value || !oldValue || value.__html != oldValue.__html) | |
&& (dom.innerHTML = (value && value.__html) || ''), | |
_skip = true /*skip diff children*/ | |
) | |
// if dom has property | |
: (!(['list', 'tagName', 'form', 'type', 'size'].includes(name)) && name in dom) | |
? dom[name] = value === null ? '' : value | |
// if ref | |
: name === 'ref' && isFunc(value) ? value(dom) | |
// if not function update attribute | |
: !isFunc(value) && ( | |
value === null || (value === false && !/^ar/.test(name)) | |
? dom.removeAttribute(name) : dom.setAttribute(name, value) | |
) | |
), _skip | |
), | |
diffProps = (dom, newProps, oldProps = {}, _newHTML) => { | |
for (let name in oldProps) if (!(name in newProps)) { | |
setProperty(dom, name, null, oldProps[name]) && (_newHTML = true) | |
} | |
for (let name in newProps) if (oldProps[name] !== newProps[name]) { | |
setProperty(dom, name, newProps[name], oldProps[name]) && (_newHTML = true) | |
} | |
return _newHTML | |
}, | |
h = (t, p, ...c) => ({t, p, c: c.filter(_ => ![false, true, null, undefined].includes(_)), key: p && p.key}), | |
assign = (obj, props) => { | |
for (let i in props) obj[i] = props[i]; | |
return (obj); | |
}, | |
remove = (v) => v.d ? v.d.remove() : v.patched && remove(v.patched), | |
render = (vnode, dom, old = dom.v || (dom.v = {}), position, _undefined, _skip) => | |
dom.v = vnode.pop ? vnode.map((vChild, index) => render(vChild, dom, old.c && old.c[index])) | |
: vnode.t.call ? ( | |
vnode.patched = render( | |
vnode.t( | |
//props | |
{children: vnode.c, ...vnode.p}, | |
// state | |
vnode.s = old.s || {}, | |
// update | |
newState => { | |
assign(vnode.s, newState); | |
render(vnode, dom, vnode) | |
}, | |
), | |
old.patched || dom, | |
old && old.patched || {} | |
), vnode | |
) | |
: ( | |
//assign old dom to new dom | |
vnode.d = old.d || ( // but if falsy then create the node | |
vnode.t ? document.createElement(vnode.t) : new Text(vnode.p) | |
), | |
// if new props then update them them | |
vnode.p != old.p && ( | |
vnode.t | |
// if it has a type then diff them | |
// diffProps will return true if we inserted innerHTML and want to skip diffing children | |
? (_skip = diffProps(vnode.d, vnode.p, old.p)) | |
: vnode.d.data = vnode.p | |
), | |
// insert at position | |
old.d && position == _undefined || dom.insertBefore(vnode.d, dom.childNodes[position + 1]), | |
// diff children (typed/keyed) // _skip && console.log('skiping diff'), | |
!_skip && (vnode.o = [].concat(...vnode.c).map((vChild, pos) => | |
render( | |
//vdom | |
vChild = vChild.c ? vChild : h('', vChild), | |
//dom | |
vnode.d, | |
//old dom | |
old.o && old.o.find((vnode, childPos) => | |
vnode && vnode.t == vChild.t && vnode.key == vChild.key | |
&& ( | |
childPos == pos && (pos = _undefined), | |
old.o[childPos] = 0, | |
vnode | |
)) || {}, | |
//position | |
pos | |
) | |
) | |
), | |
// remove stragglers | |
old.o && old.o.map(e => e && remove(e)), assign(old, vnode)//return the vnode | |
), | |
// for subscriptions handling | |
Host = ({children, ...props}) => h('z-host', props, children), | |
// Fragment = props => props.children, // - this doesnt fully work yet, | |
// using custom element with display:contents to keep familiar api until fragments work | |
Fragment = ({children, ...props}) => h('z-fragment', props, children), | |
define = (tag, elem) => customElements.define(tag, elem); | |
class Frag extends HTMLElement { | |
constructor() { | |
super(), this.attachShadow({mode: 'open'}).innerHTML = `<style>:host{display:contents}</style><slot></slot>` | |
} | |
} | |
define('z-fragment', Frag); | |
define('z-host', class extends Frag { | |
disconnectedCallback() { | |
let {isConnected: c, s} = this; | |
!c && s && s.map(u => isFunc(u) && u()) | |
} | |
connectedCallback(f) { | |
this.m || (this.m = 1, isFunc((f = this.f)) && (this.s = [].concat(f()))) | |
} | |
set lifeCycle(fn) { | |
this.f = fn | |
} | |
}); | |
export {h, render, Fragment, Host} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment