Building AlpineJS - Notes

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()