Created
January 8, 2021 22:08
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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