Skip to content

Instantly share code, notes, and snippets.

@dux
Last active February 1, 2022 02:11
Show Gist options
  • Save dux/89c59570228d745128aafef98164de1f to your computer and use it in GitHub Desktop.
Save dux/89c59570228d745128aafef98164de1f to your computer and use it in GitHub Desktop.
Creates custom DOM element and passes props. Bare bones custom nodes
// https://gist.github.com/dux/89c59570228d745128aafef98164de1f
// micro custom dom elements, no shadow dom
// exposes props: root, props and state
window.CustomElement = {
// we need to find custom node in exact name
// CustomElement.find(domNode, 'foo-bar')
find: (node, uid) => {
while(node) {
if (node.customNode && node.customNode.UID == uid) {
return node.customNode
} else {
node = node.parentNode
}
}
alert('Custom node not found')
},
// expose node attributes as object
attributes: (node) => {
// if you want to send nested complex data, best to define as data-props encoded as JSON
let props = node.getAttribute('data-props')
if (props) {
props = JSON.parse(props)
} else {
props = Array.prototype.slice.call(node.attributes).reduce(function(h, el) {
h[el.name] = el.value;
return h;
}, {});
}
if (node.innerHTML) {
props.html = node.innerHTML
node.innerHTML = ''
}
return props;
},
// define custom element
define: (name, klass) => {
if (!customElements.get(name)) {
window.addEventListener('DOMContentLoaded', () => {
customElements.define(name, class extends HTMLElement {
connectedCallback() {
let props = CustomElement.attributes(this)
let el = new klass(name, this, props)
this.customNode = el
if (el['init']) {
el.init(this, props)
} else {
el.render()
}
}
})
})
}
}
};
class CustomNode {
static counter = 0
// shuld not be overloaded
constructor(name, node, props) {
this.UID = ++CustomNode.counter
this.name = name
this.root = node
this.props = props
this.state = {}
}
// set state shortcut
set(name, value) {
this.state[name] = value
this.render()
}
// get innerHTML child nodes as an array of objects
children() {
let node = document.createElement('div')
node.innerHTML = this.props.html
return Array.prototype.slice
.call(node.children)
.map((slot)=>CustomElement.attributes(slot))
}
// replace double $$ with pointer to current custom element, and pass plain html
html(data) {
data = data.replace(/\$\$\./g, `CustomElement.find(this, ${this.UID}).`)
this.root.innerHTML = data
}
// placeholder
render() {}
}
window.CustomNode = CustomNode
// this.root : custome element dom node
// this.props : custome element attributes
// this.html(data) renders node innerHTML and parses $$. to current node pointer
// this.set(name, value) : sets value to state and calls render
// <foo-bar time="11:55"></foo-bar>
CustomElement.define('foo-bar', class FooBarNode extends CustomNode {
// init is called if defined, otherwise render() is called
init(rootNode, props) {
render()
}
toggle() {
this.state.foo = !this.state.foo;
this.render()
// or
this.set('foo', !this.state.foo)
}
render() {
this.html(`<div onclick="$$.toggle()">${this.props.time} : ${this.state.foo ? 1 : 0}</div>`)
}
})
// %nav-main{ class: 'navbar-nav' }
// %slot{ href:'/heimdall', name:'Tasks' }
// %slot{ href:'/heimdall/schedules', name:'Schedules' }
// %slot{ href:'/heimdall/ques', name:'Ques' }
// %slot{ href:'/heimdall/sys', name:'Sys' }
CustomElement.define('nav-main', class extends CustomNode {
render() {
let children = this.children().map((el)=>
tag('li.nav-item',
tag('a.nav-link', el.name, {href: el.href, class: el.href == location.pathname ? 'active' : null})
)
)
this.html(tag('ul', children.join(''), {class: this.props.class}))
}
})
# can be compiled here https://coffeescript.org/#try
# tag 'a', { href: '#'}, 'link name' -> <a href="#">link name</a>
# tag 'a', 'link name' -> <a>link name</a>
# tag '.a', 'b' -> <div class="a">b</div>
# tag '#a.b', ['c','d'] -> <div class="b" id="a">cd</div>
# tag '#a.b', {c:'d'} -> <div c="d" class="b" id="a"></div>
tag_events = {}
tag_uid = 0
window.tag = (name, args...) ->
return tag_events unless name
# evaluate function if data is function
args = args.map (el) -> if typeof el == 'function' then el() else el
# swap args if first option is object
args[1] ||= undefined # fill second value
[opts, data] = if typeof args[0] == 'object' && !Array.isArray(args[0]) then args else args.reverse()
# set default values
opts ||= {}
data = '' if typeof(data) == 'undefined'
data = data.join('') if Array.isArray(data)
# haml style id define
name = name.replace /#([\w\-]+)/, (_, id) ->
opts['id'] = id
''
# haml style class add with a dot
name_parts = name.split('.')
name = name_parts.shift() || 'div'
if name_parts[0]
old_class = if opts['class'] then ' '+opts['class'] else ''
opts['class'] = name_parts.join(' ') + old_class
node = ['<'+name]
for key in Object.keys(opts).sort()
val = opts[key]
# hide function calls
if typeof val == 'function'
uid = ++tag_uid
tag_events[uid] = val
val = "tag()[#{uid}](this)"
node.push ' '+key+'="'+val+'"'
if ['input', 'img'].indexOf(name) > -1
node.push ' />'
else
node.push '>'+data+'</'+name+'>'
node.join('')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment