Skip to content

Instantly share code, notes, and snippets.

@bschrag620
Created February 28, 2024 13:22
Show Gist options
  • Save bschrag620/cb7abcf14f99490379e53b0e212fd739 to your computer and use it in GitHub Desktop.
Save bschrag620/cb7abcf14f99490379e53b0e212fd739 to your computer and use it in GitHub Desktop.
Stimulus Page Refreshing
<%# Create a partial for generating a trigger element to be appended to the container in a turbo_stream %>
<% selectors ||= '' %>
<%
# As a convenience, we can use Array.wrap to turn the selector value into an array if it is not one, then use join(' ') to
# create a list that works nicely with the stimulus controller.
%>
<div data-refresh-target='trigger' data-selctors='<%= Array.wrap(selectors).join(" ") %>'
<%# In your layout file(s), add an empty tag that can be appended to by broadcasts, turbo_streams, etc...%>
<div id='stimulus-refresher' data-controller='refresher'></div>
# Now you can use broadcasts from turbo_streams as long as the users page is subscribe to a known turbo channel
#
#
user.broadcast_append(
target: 'stimulus-refresher',
partial: 'stimulus_refresher/trigger',
locals: {selectors: ['your', 'ids', 'to', 'refresh']}
)
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static get targets() {
return ['trigger']
}
static get values() {
return {
selectors: {
type: String,
default: ''
}
}
}
triggerTargetConnected(ele) {
this.refresh(ele);
ele.remove(); // we don't need this element on the DOM once it has triggered the StimulusRefresh so we can remove it
}
/**
* Refreshes the users current page
*/
refresh(element) {
const ids = element.dataset.selectors.split(' ');
if (ids.length === 0) return;
window.StimulusRefresh.refresh(this.selectorsValues.split(' '));
}
}
/**
* If you have the convenience of upgrading to the latest versions of turbo and stimulus, this isn't for you. With Turbo 8
* there is available support for refreshing the page. However, for those of us that are maybe in a place where we can't simply
* upgrade to have the latest and greatest this is a simply implementation that allows for a turbo_stream broadcast from the backend
* to selectively updated a list of elements on the users page through means of fetching the users existing page.
*
* This sample code also makes the assumption that the code base does NOT support custom turbo stream actions. Using custom turbo_stream
* actions could simplify things significantly, but again, sometimes an upgrade isn't a viable option. If custom actions are
* available in the app, you can trim out the partials and the stimulus_refresher controller and from the backend then broadcast
* using that custom action. That said, I don't have much experience with custom turbo actions. I seem to remember that custom actions
* were not available in broadcasts - but that could be incorrect.
**/
export class StimulusRefresh {
static start() {
// in application.js, import this file and run StimulusRefresh.start()
window.StimulusRefresh = new StimulusRefresh();
}
constructor () {
this.selectors = [];
// debouncing the handler to avoid many broadcasts from various services/models forcing many updates to a page at once
this.handle = _.debounce(this._handle.bind(this), 500);
}
/**
* Adds the selectors to the current array of selectors to be refreshed and calls the handler
**/
refresh(selectors) {
selectors = Array.isArray(selectors) ? selectors : [selectors]
this.selectors = this.selectors.concat(selectors)
this.handle()
}
/**
* Fetches the page content if there are elements on the current page that match the selectors that need to be refreshed
**/
_handle() {
const elementsToHandle = this.elementsWithSelectors;
if (elementsToHandle.length === 0) return;
const joiner = window.location.search.length === 0 ? '?' : '&';
// passing some extra params to the backend to let the server know it doesn't need to render the content in a layout
const fetchLocation = [window.location.href, 'layoutless=true'].join(joiner)
fetch(fetchLocation).then(
resp => resp.text()
).then(html => {
// now that we have the content, parse it and replace the elements that need to be replaced.
const page = new DOMParser().parseFromString(html, 'text/html');
let newElement;
elementsToHandle.forEach( ({existingElement, selector}) => {
newElement = page.getElementById(selector);
if (newElement) existingElement.replaceWith(newElement);
})
})
}
/**
* Builds a nested array of selectors and their corresponding element - currently only supporting id selectors but this
* could easily be changed to utilize querySelector instead of getElementById.
**/
get elementsWithSelectors() {
let ret = [];
let element;
const uniqSelectors = [...new Set(this.selectors)];
this.selectors = [];
uniqSelectors.forEach(selector => {
element = document.getElementById(selector)
if (element) {
ret.push({existingElement: element, selector: selector})
}
})
return ret
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment