Skip to content

Instantly share code, notes, and snippets.

@inopinatus
Created January 8, 2021 22:08
Show Gist options
  • Save inopinatus/3f48f2c8da79ee117c1b75829f467274 to your computer and use it in GitHub Desktop.
Save inopinatus/3f48f2c8da79ee117c1b75829f467274 to your computer and use it in GitHub Desktop.
Proof-of-concept, using a Stimulus wrapper for IntersectionObserver to drive Turbo lazy-loading via click events, with graceful fallback to plain HTML.
import { Controller } from "stimulus"
// Proof of concept for lazy loaded turbo frames
export default class extends Controller {
static targets = ["click", "events", "root"]
static values = {
rootMargin: String,
threshold: Number,
appearEvent: String,
disappearEvent: String
}
connect() {
// Crude, since it ignores the mutations returned and just
// refreshes contents. We can hopefully do better.
this.observeMutations(this.refresh)
this.start()
}
disconnect() {
this.stop()
}
start() {
if (!this.started) {
this.observedTargets = new Set
this.intersectionObserver = new IntersectionObserver((entries) => this.processIntersectionEntries(entries), this.observerOptions)
this.started = true
this.refresh()
}
}
stop() {
if (this.started) {
this.intersectionObserver.takeRecords()
this.intersectionObserver.disconnect()
this.started = false
}
}
restart() {
this.stop()
this.start()
}
rootMarginValueChanged = this.restart
thresholdValueChanged = this.restart
rootTargetChanged = this.restart
processIntersectionEntries(entries) {
entries.forEach(entry => {
this.processIntersectionEntry(entry)
})
}
processIntersectionEntry(entry) {
if (entry.isIntersecting) {
this.handleAppearance(entry)
} else {
this.handleDisappearance(entry)
}
}
handleAppearance(entry) {
const target = entry.target
if (this.clickTargets.includes(target)) {
target.click()
}
if (this.eventsTargets.includes(target)) {
this.dispatch(this.appearEvent, { target: target, detail: { intersectionObserverEntry: entry } })
}
}
handleDisappearance(entry) {
const target = entry.target
if (this.eventsTargets.includes(target)) {
this.dispatch(this.disappearEvent, { target: target, detail: { intersectionObserverEntry: entry } })
}
}
dispatch(eventName, { target = this.element, detail = {}, bubbles = true, cancelable = true } = {}) {
const event = new CustomEvent(eventName, { detail, bubbles, cancelable });
target.dispatchEvent(event)
return event
}
// If mutations are this simple, why not Intersections?
observeMutations(callback, target = this.element, options = { childList: true, subtree: true }) {
const observer = new MutationObserver(mutations => {
observer.disconnect()
Promise.resolve().then(start)
callback.call(this, mutations)
})
function start() {
if (target.isConnected) observer.observe(target, options)
}
start()
}
refresh() {
const scopeTargets = this.findAllScopeTargets()
if (!this.started) {
return
}
this.observedTargets.forEach(observedTarget => {
if (!scopeTargets.has(observedTarget)) {
this.removeTarget(observedTarget)
}
})
scopeTargets.forEach(scopeTarget => {
this.addTarget(scopeTarget)
})
}
addTarget(target) {
if (!this.observedTargets.has(target)) {
this.intersectionObserver.observe(target)
this.observedTargets.add(target)
}
}
removeTarget(target) {
if (this.observedTargets.has(target)) {
this.observedTargets.delete(target)
this.intersectionObserver.unobserve(target)
}
}
findAllScopeTargets() {
return new Set([
...this.clickTargets,
...this.eventsTargets,
])
}
get appearEvent() {
return this.hasAppearEventValue ? this.appearEventValue : "appear"
}
get disappearEvent() {
return this.hasDisappearEventValue ? this.disappearEventValue : "disappear"
}
get observerRootOption() {
return this.hasRootTarget ? { root: this.rootTarget } : null
}
get observerRootMarginOption() {
return this.hasRootMarginValue ? { rootMargin: this.rootMarginValue } : null
}
get observerThresholdOption() {
return this.hasThresholdValue ? { threshold: this.thresholdValue } : null
}
get observerOptions() {
return {
...this.observerRootOption,
...this.observerRootMarginOption,
...this.observerThresholdOption
}
}
}
<main class="max-w-full sm:bg-gray-300 bg-red-200" data-controller="bouquet" data-bouquet-root-margin-value="10px">
<article class="sm:max-w-4xl w-full mx-auto sm:px-6 py-12 space-y-96">
<%= turbo_frame_tag "f1", class: "block" do %>
<aside class="sm:px-10 px-6 py-6 bg-white sm:rounded-lg shadow-md">
<a href="/test/101" data-bouquet-target="click">Click for more about Test 101.</a>
</aside>
<% end %>
<%= turbo_frame_tag "f2", class: "block" do %>
<aside class="sm:px-10 px-6 py-6 bg-white sm:rounded-lg shadow-md">
<a href="/test/102" data-bouquet-target="click">Click for more about Test 102.</a>
</aside>
<% end %>
<%= turbo_frame_tag "f3", class: "block" do %>
<aside class="sm:px-10 px-6 py-6 bg-white sm:rounded-lg shadow-md">
<a href="/test/103" data-bouquet-target="click">Click for more about Test 103.</a>
</aside>
<% end %>
<%= turbo_frame_tag "f4", class: "block" do %>
<aside class="sm:px-10 px-6 py-6 bg-white sm:rounded-lg shadow-md">
<a href="/test/104" data-bouquet-target="click">Click for more about Test 104.</a>
</aside>
<% end %>
<%= turbo_frame_tag "f5", class: "block" do %>
<aside class="sm:px-10 px-6 py-6 bg-white sm:rounded-lg shadow-md">
<a href="/test/105" data-bouquet-target="click">Click for more about Test 105.</a>
</aside>
<% end %>
</article>
</main>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment