Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optionally debounce updates #151

Merged
3 changes: 2 additions & 1 deletion app/helpers/cable_ready_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ def stream_from(*keys, html_options: {})
tag.stream_from(**build_options(*keys, html_options))
end

def updates_for(*keys, url: nil, html_options: {}, &block)
def updates_for(*keys, url: nil, debounce: nil, html_options: {}, &block)
options = build_options(*keys, html_options)
options[:url] = url if url
options[:debounce] = debounce if debounce
tag.updates_for(**options) { capture(&block) }
end

Expand Down
78 changes: 43 additions & 35 deletions javascript/updates_for_element.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import morphdom from 'morphdom'
import { shouldMorph } from './morph_callbacks'
import activeElement from './active_element'
import actionCable from './action_cable'
import { assignFocus, dispatch } from './utils'
import { Boris, assignFocus, dispatch } from './utils'

const template = `
<style>
Expand All @@ -18,6 +18,7 @@ class UpdatesForElement extends HTMLElement {
super()
const shadowRoot = this.attachShadow({ mode: 'open' })
shadowRoot.innerHTML = template
this.update = Boris(this.update.bind(this), this.debounce)
}

async connectedCallback () {
Expand All @@ -30,40 +31,7 @@ class UpdatesForElement extends HTMLElement {
identifier: this.getAttribute('identifier')
},
{
received: () => {
const identifier = this.getAttribute('identifier')
const query = `updates-for[identifier="${identifier}"]`
const blocks = document.querySelectorAll(query)
if (blocks[0] !== this) return

const template = document.createElement('template')
fetch(
this.hasAttribute('url')
? this.getAttribute('url')
: window.location.href
)
.then(response => response.text())
.then(html => {
template.innerHTML = String(html).trim()
const fragments = template.content.querySelectorAll(query)
for (let i = 0; i < blocks.length; i++) {
activeElement.set(document.activeElement)
const operation = {
element: blocks[i],
html: fragments[i],
permanentAttributeName: 'data-ignore-updates',
focusSelector: null
}
dispatch(blocks[i], 'cable-ready:before-update', operation)
morphdom(blocks[i], fragments[i], {
childrenOnly: true,
onBeforeElUpdated: shouldMorph(operation)
})
dispatch(blocks[i], 'cable-ready:after-update', operation)
assignFocus(operation.focusSelector)
}
})
}
received: this.update
}
)
} else {
Expand All @@ -77,12 +45,52 @@ class UpdatesForElement extends HTMLElement {
if (this.channel) this.channel.unsubscribe()
}

async update () {
const identifier = this.getAttribute('identifier')
const query = `updates-for[identifier="${identifier}"]`
const blocks = document.querySelectorAll(query)
if (blocks[0] !== this) return

const template = document.createElement('template')
const response = await fetch(this.url)
const html = await response.text()

template.innerHTML = String(html).trim()
const fragments = template.content.querySelectorAll(query)
for (let i = 0; i < blocks.length; i++) {
activeElement.set(document.activeElement)
const operation = {
element: blocks[i],
html: fragments[i],
permanentAttributeName: 'data-ignore-updates',
focusSelector: null
}
dispatch(blocks[i], 'cable-ready:before-update', operation)
morphdom(blocks[i], fragments[i], {
childrenOnly: true,
onBeforeElUpdated: shouldMorph(operation)
})
dispatch(blocks[i], 'cable-ready:after-update', operation)
assignFocus(operation.focusSelector)
}
}

get url () {
return this.hasAttribute('url') ? this.getAttribute('url') : location.href
}

get preview () {
return (
document.documentElement.hasAttribute('data-turbolinks-preview') ||
document.documentElement.hasAttribute('data-turbo-preview')
)
}

get debounce () {
return this.hasAttribute('debounce')
? parseInt(this.getAttribute('debounce'))
: 20
}
}

if (!window.customElements.get('updates-for')) {
Expand Down
12 changes: 11 additions & 1 deletion javascript/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,15 @@ const after = (target, operation) =>
operation
)

// Boris de bouncer
function Boris (func, timeout) {
let timer
return (...args) => {
clearTimeout(timer)
timer = setTimeout(() => func.apply(this, args), timeout)
}
}

export {
isTextInput,
assignFocus,
Expand All @@ -110,5 +119,6 @@ export {
processElements,
operate,
before,
after
after,
Boris
}