hopsoft/turbo_ready

View on GitHub
app/javascript/invoke.js

Summary

Maintainability
A
2 hrs
Test Coverage
import morph from './morph'

const invokeEvents = {
  before: 'turbo-boost:stream:before-invoke',
  after: 'turbo-boost:stream:after-invoke',
  finish: 'turbo-boost:stream:finish-invoke'
}

// Invokes the callback on a single receiver with before/after events
function withInvokeEvents(receiver, detail, callback) {
  const { object, target } = receiver
  detail = detail || {}
  detail = { ...detail, object: receiver.object }
  const options = { detail, bubbles: true }

  target.dispatchEvent(new CustomEvent(invokeEvents.before, options))

  // the before event can provide invoke instructions my modifying the event detail
  // For example, { delay: 1000 } will delay the invocation by 1000ms
  let { delay } = detail.invoke || {}
  delay = delay || 0

  const execute = () => {
    const result = callback(object)
    options.detail.result = result
    target.dispatchEvent(new CustomEvent(invokeEvents.after, options))

    let promise
    if (result instanceof Animation) promise = result.finished
    if (result instanceof Promise) promise = result

    if (promise)
      promise
        .then(() => {
          options.detail.promise = 'fulfilled'
          target.dispatchEvent(new CustomEvent(invokeEvents.finish, options))
        })
        .catch(error => {
          options.detail.promise = 'rejected'
          options.detail.error = error
          target.dispatchEvent(new CustomEvent(invokeEvents.finish, options))
        })
    else target.dispatchEvent(new CustomEvent(invokeEvents.finish, options))
  }

  if (delay > 0) setTimeout(execute, delay)
  else execute()
}

function invokeDispatchEvent(method, args, receivers) {
  const eventName = args[0]
  const eventOptions = args[1]
  const detail = { method, eventName, eventOptions }
  receivers.forEach(receiver =>
    withInvokeEvents(receiver, detail, object =>
      object.dispatchEvent(new CustomEvent(eventName, eventOptions))
    )
  )
}

function invokeMorph(method, args, receivers) {
  const html = args[0]
  const detail = { method, html }
  receivers.forEach(receiver => withInvokeEvents(receiver, detail, object => morph.method(object, html)))
}

function invokeAssignment(method, args, receivers) {
  const property = method.slice(0, -1).trim()
  const value = args[0]
  const detail = { method, property, value }
  receivers.forEach(receiver => withInvokeEvents(receiver, detail, object => (object[property] = value)))
}

function invokeMethod(method, args, receivers) {
  const detail = { method, args }
  receivers.forEach(receiver =>
    withInvokeEvents(receiver, detail, object => object[method].apply(object, args))
  )
}

// Performs an invocation on all receivers for the given method and args
function performInvoke(method, args, receivers) {
  // dispatch ................................................................................................
  if (method.match(/^dispatch(Event)?$/)) return invokeDispatchEvent(method, args, receivers)

  // morph ...................................................................................................
  if (method.match(/^morph|mutate$/)) return invokeMorph(method, args, receivers)

  // assignment ..............................................................................................
  if (method.endsWith('=')) return invokeAssignment(method, args, receivers)

  // method ..................................................................................................
  return invokeMethod(method, args, receivers)
}

function invoke() {
  const payload = JSON.parse(this.templateContent.textContent)
  const { id, selector, receiver, method, args, delay } = payload
  let receivers = [{ object: self, target: self }]
  if (selector)
    receivers = Array.from(document.querySelectorAll(selector)).map(el => ({
      object: el,
      target: el
    }))

  if (receiver) {
    receivers = receivers.map(r => {
      let { object, target } = r
      const chain = receiver.split('.')
      while (chain.length > 0) {
        object = object[chain.shift()]
        if (object.dispatchEvent) target = object
      }
      return { object, target }
    })
  }

  if (delay > 0) setTimeout(() => performInvoke(method, args, receivers), delay)
  else performInvoke(method, args, receivers)
}

export { invoke, invokeEvents }