Using AlpineJS
<html>
<div x-data="{ count: 0 }">
<button @click="count++">+</button>
<button @click="count--">-</button>
<span x-text="count"></span>
</div>
<script defer src="<https://unpkg.com/alpinejs@3.10.5/dist/cdn.min.js>"></script>
</html>
Using Javascript
Walk through DOM
// with data
// <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/with>
walkDom(root, el => {
if (el.hasAttribute('x-text')) {
let expression = el.getAttribute('x-text')
el.innerText = eval(`with (data) {(${expression})}`)
}
})
// get first element then go to next
function walkDom(el, callback) {
callback(el)
el = el.firstElementChild
while(el) {
walkDom(el, callback)
el = el.nextElementSibling
}
}
Observe
// <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy>
let rawData = getInitialData()
let data = observe(rawData)
function observe(data) {
return new Proxy(data, {
set(target, key, value) {
target[key] = value
refreshDom()
}
})
}
Register Listener
registerListeners()
function registerListeners() {
walkDom(root, el => {
if (el.hasAttribute('@click')) {
let expression = el.getAttribute('@click')
el.addEventListener('click', () => {
eval(`with (data) {(${expression})}`)
})
}
});
}
Full js
window.Alpine = {
directives: {
'x-text': (el,value) => {
el.innerText = value
},
'x-show': (el,value) => {
el.style.display = value ? 'block' : 'none'
}
},
start() {
this.root = document.querySelector('[x-data]')
this.rawData = this.getInitialData()
this.data = this.observe(this.rawData)
this.registerListeners()
this.refreshDom()
},
registerListeners() {
this.walkDom(this.root, el => {
Array.from(el.attributes).forEach(attribute => {
if (! attribute.name.startsWith('@')) return
let event = attribute.name.replace('@','')
el.addEventListener(event, () => {
eval(`with (this.data) {(${attribute.value})}`)
})
})
});
},
observe(data) {
var self = this
return new Proxy(data, {
set(target, key, value) {
target[key] = value
self.refreshDom()
}
})
},
refreshDom() {
this.walkDom(this.root, el => {
Array.from(el.attributes).forEach(attribute => {
if (!Object.keys(this.directives).includes(attribute.name)) return
this.directives[attribute.name](el, eval(`with (this.data) {(${attribute.value})}`))
})
})
},
walkDom(el, callback) {
callback(el)
el = el.firstElementChild
while(el) {
this.walkDom(el, callback)
el = el.nextElementSibling
}
},
getInitialData() {
let dataString = this.root.getAttribute('x-data')
return eval(`(${dataString})`)
}
},
window.Alpine.start()