Skip to content

Commit

Permalink
Wrap updates-for-element in an IntersectionObserver (#285)
Browse files Browse the repository at this point in the history
  • Loading branch information
julianrubisch committed Nov 9, 2023
1 parent 58a9799 commit 951d5dc
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 3 deletions.
3 changes: 2 additions & 1 deletion app/helpers/cable_ready/view_helper.rb
Expand Up @@ -27,12 +27,13 @@ def cable_ready_stream_from(*keys, html_options: {})
tag.cable_ready_stream_from(**build_options(*keys, html_options))
end

def cable_ready_updates_for(*keys, url: nil, debounce: nil, only: nil, ignore_inner_updates: false, html_options: {}, &block)
def cable_ready_updates_for(*keys, url: nil, debounce: nil, only: nil, ignore_inner_updates: false, observe_appearance: false, html_options: {}, &block)
options = build_options(*keys, html_options)
options[:url] = url if url
options[:debounce] = debounce if debounce
options[:only] = only if only
options[:"ignore-inner-updates"] = "" if ignore_inner_updates
options[:"observe-appearance"] = "" if observe_appearance
tag.cable_ready_updates_for(**options) { capture(&block) }
end

Expand Down
51 changes: 49 additions & 2 deletions javascript/elements/updates_for_element.js
Expand Up @@ -8,6 +8,7 @@ import ActiveElement from '../active_element'
import CableConsumer from '../cable_consumer'
import Log from '../updatable/log'
import { BoundedQueue } from '../utils'
import { AppearanceObserver } from '../observers/appearance_observer'

const template = `
<style>
Expand Down Expand Up @@ -36,6 +37,11 @@ export default class UpdatesForElement extends SubscribingElement {

this.triggerElementLog = new BoundedQueue(10)
this.targetElementLog = new BoundedQueue(10)

this.appearanceObserver = new AppearanceObserver(this)

this.visible = false
this.didTransitionToVisible = false
}

async connectedCallback () {
Expand All @@ -51,6 +57,16 @@ export default class UpdatesForElement extends SubscribingElement {
'The `cable_ready_updates_for` helper cannot connect. You must initialize CableReady with an Action Cable consumer.'
)
}

if (this.observeAppearance) {
this.appearanceObserver.start()
}
}

disconnectedCallback () {
if (this.observeAppearance) {
this.appearanceObserver.stop()
}
}

async update (data) {
Expand All @@ -77,7 +93,8 @@ export default class UpdatesForElement extends SubscribingElement {
}

// first <cable-ready-updates-for> element in the DOM *at any given moment* updates all of the others
if (blocks[0].element !== this) {
// if the element becomes visible though, we have to overrule and load it
if (blocks[0].element !== this && !this.didTransitionToVisible) {
this.triggerElementLog.push(
`${new Date().toLocaleString()}: ${Log.cancel(
this.lastUpdateTimestamp,
Expand Down Expand Up @@ -128,6 +145,19 @@ export default class UpdatesForElement extends SubscribingElement {
})
}

appearedInViewport () {
if (!this.visible) {
// transition from invisible to visible forces update
this.didTransitionToVisible = true
this.update({})
}
this.visible = true
}

disappearedFromViewport () {
this.visible = false
}

get query () {
return `${this.tagName}[identifier="${this.identifier}"]`
}
Expand All @@ -141,6 +171,10 @@ export default class UpdatesForElement extends SubscribingElement {
? parseInt(this.getAttribute('debounce'))
: 20
}

get observeAppearance () {
return this.hasAttribute('observe-appearance')
}
}

class Block {
Expand Down Expand Up @@ -186,6 +220,7 @@ class Block {
onBeforeElUpdated: shouldMorph(operation),
onElUpdated: _ => {
this.element.removeAttribute('updating')
this.element.didTransitionToVisible = false
dispatch(this.element, 'cable-ready:after-update', operation)
assignFocus(operation.focusSelector)
}
Expand Down Expand Up @@ -237,7 +272,11 @@ class Block {

shouldUpdate (data) {
// if everything that could prevent an update is false, update this block
return !this.ignoresInnerUpdates && this.hasChangesSelectedForUpdate(data)
return (
!this.ignoresInnerUpdates &&
this.hasChangesSelectedForUpdate(data) &&
(!this.observeAppearance || this.visible)
)
}

hasChangesSelectedForUpdate (data) {
Expand Down Expand Up @@ -272,4 +311,12 @@ class Block {
get query () {
return this.element.query
}

get visible () {
return this.element.visible
}

get observeAppearance () {
return this.element.observeAppearance
}
}
59 changes: 59 additions & 0 deletions javascript/observers/appearance_observer.js
@@ -0,0 +1,59 @@
export class AppearanceObserver {
constructor (delegate, element = null) {
this.delegate = delegate
this.element = element || delegate
this.started = false
this.intersecting = false

this.intersectionObserver = new IntersectionObserver(this.intersect)
}

start () {
if (!this.started) {
this.started = true
this.intersectionObserver.observe(this.element)
this.observeVisibility()
}
}

stop () {
if (this.started) {
this.started = false
this.intersectionObserver.unobserve(this.element)
this.unobserveVisibility()
}
}

observeVisibility = () => {
document.addEventListener('visibilitychange', this.handleVisibilityChange)
}

unobserveVisibility = () => {
document.removeEventListener(
'visibilitychange',
this.handleVisibilityChange
)
}

intersect = entries => {
entries.forEach(entry => {
if (entry.target === this.element) {
if (entry.isIntersecting && document.visibilityState === 'visible') {
this.intersecting = true
this.delegate.appearedInViewport()
} else {
this.intersecting = false
this.delegate.disappearedFromViewport()
}
}
})
}

handleVisibilityChange = event => {
if (document.visibilityState === 'visible' && this.intersecting) {
this.delegate.appearedInViewport()
} else {
this.delegate.disappearedFromViewport()
}
}
}

0 comments on commit 951d5dc

Please sign in to comment.