I’ve been writing a Svelte app that needs to install itself on each page. I figured out how to add my JS file into the head section and get it to load on the first page load. However, as I experimented, I realized that discourse is pulling in new content via XHR and replacing certain sections, and so my app was not getting reinitialized when a new page loads.
I played around with various attempts to get notified when the page changes, but it does not seem like Ember provides the hooks, and I was unable to find any custom events that I could listen for.
It looks like one way is to add a mutation observer to the DOM and watch for changes. I found that the "#topic div seems to be the one which is reloaded (and the attributes change so you can just watch for attribute changes). I setup a mutation observer there (mutation observers are the new-ish performant way to watch for DOM changes). The way it works is to watch for changes, and when they occur, run a callback to reload my Svelte app for that page.
I’m liking this approach, and would like feedback.
One question: should I instead watch for changes to the URL? Is that a better idea to register a listener for popstate?
To use it, do something like this in your theme/head:
<script src="https://files.extrastatic.dev/community/on-discourse.js"></script>
<script src="https://files.extrastatic.dev/community/index.js"></script>
<link rel="stylesheet" type="text/css" href="https://files.extrastatic.dev/community/index.css" ></link>
Then, inside your library you can call on-discourse like this:
function log(...msg) {
console.log('svelte', ...msg);
}
// This is the Svelte install code
function setup() {
try {
const ID = 'my-special-target-id';
log('Inside setup()');
const el = document.getElementById(ID);
if (el) {
log('Removed existing element', ID);
el.remove();
}
const target = document.createElement("div");
target.setAttribute("id", ID);
log('Created target');
document.body.appendChild(target);
log('Appended child to body');
const app = new App({
// eslint-disable-next-line no-undef
target
});
log('Created app and installed');
} catch(err) {
console.error('Unable to complete setup()', err.toString() );
}
}
(function start() {
log('Starting custom Svelte app');
// Re-install on changes
window.onDiscourse && window.onDiscourse( setup );
// Load the app the first page load
window.addEventListener('load', () => {
setup();
});
log('Finished custom Svelte app);
})();
Basically, you just call window.onDiscourse(callback)
with your callback (and you can run it multiple times to install multiple callbacks), and then when a mutation occurs, that callback is run to initialize your app.
Here is the full code for on-discourse.js. (edit: I updated this to use #topic, which seems like a good thing to watch, because the attributes change when the page loads, and then the mutation only needs to watch for attribute changes, rather than looking over the entirety of the DOM tree for #main-outlet)
let mutationObservers = [];
function log(...msg) {
console.log("on-discourse", ...msg);
}
function observeMainOutlet() {
log('Observing main outlet');
// Select the node that will be observed for mutations
const targetNode = document.getElementById("topic");
if (targetNode) {
// Options for the observer (which mutations to observe)
const config = { attributes: true };
// create an observer instance to reset when childList changes
const observer = new MutationObserver(function(mutations) {
let reset = false;
mutations.forEach(function(mutation) {
if (mutation.type === 'attributes') {
log('Found main-outlet mutation, running callbacks');
mutationObservers.forEach( (s,i) => {
try {
log(`Running div callback ${i+1}`);
s();
log(`Finished div callback ${i+1}`);
} catch( err ) {
log(`Div callback error (${i+1})`, err );
}
});
}
});
});
// Start observing the target node for configured mutations
observer.observe(targetNode, config);
// Later, you can stop observing
// observer.disconnect();
log('Done with outlet observer');
} else {
console.error('on-discourse FATAL ERROR: Unable to find main-outlet');
}
}
window.addDiscourseDivMutationObserver = (cb) => {
log('Adding on-discourse div mutation callback');
mutationObservers.push(cb);
log('Added on-discourse div mutation callback');
}
window.addEventListener("load", () => {
log('Setting up topic observer');
if (mutationObservers.length > 0) {
observeMainOutlet();
}
log('Created topic observer');
});
log('Completed setup of on-discourse.js');