Skip to content

Instantly share code, notes, and snippets.

@robertknight
Last active December 31, 2022 14:39
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save robertknight/b71af79a7dbe25712fad192e44bc6f5f to your computer and use it in GitHub Desktop.
Save robertknight/b71af79a7dbe25712fad192e44bc6f5f to your computer and use it in GitHub Desktop.
Using Hypothesis in a JavaScript SPA with client-side routing

Update 2022-12-31:

The latest version of Hypothesis will detect client-side URL changes via the HTML 5 History API or the Navigation API and update the loaded set of annotations automatically. For this to work, the web application must follow these best practices for web apps that do client-side navigation:

  1. Update the URL when the logical route in the app changes
  2. Use the path and query string of the URL to indicate the route, not the fragment. For example https://example.com/your-app/some/page is OK, https://example.com/your-app/#!/some/page is not.
  3. The content of the page must be updated by the time Hypothesis has fetched the new annotations for the new URL. The safest way to do this is by not updating the URL until the content has been updated or is just about to be updated (eg. content has been fetched and a React/Vue/Angular re-render is queued up). In browsers that implement the new Navigation API (as of Dec 2022, this is Chrome-only) Hypothesis will use the navigatesuccess event to know when a new route has loaded. In other browsers it relies on the URL change to signal that the new content is ready.

Criteria (1) and (2) are the most important. If not met, annotations from different "pages" in the app will get merged together and annotations will not be updated as you navigate around the web app. If criteria (3) is not met, annotations may incorrectly show up in the "Orphans" tab, but they should still load.


Original note (now out of date, see above):

This is a sample which demonstrates how to use Hypothesis <= v0.21.0 in a JavaScript Single Page Application which does client-side routing.

The current version of Hypothesis at the time of writing does not automatically update the loaded annotations when the page URL changes. This sample takes the heavy-handed approach of completely removing and reloading Hypothesis when the URL changes. In future we'll look to build this into Hypothesis automatically in a much more performant way.

Running the demo

  1. Download files from this gist and extract into a directory
  2. Run python -m SimpleHTTPServer 8000
  3. Navigate to http://locahost:8000/demo.html
  4. Switch chapters using Chapter links in left navbar
.container {
display: flex;
flex-direction: row;
margin: auto;
max-width: 800px;
}
.navbar {
max-width: 200px;
padding: 20px;
background-color: #ddd;
}
.content {
width: 500px;
border: 1px solid #ddd;
padding: 10px;
}
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="demo.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/page.js/1.7.1/page.js"></script>
</head>
<body>
<div class="container">
<nav class="navbar">
<a href="/demo.html">Home</a>
<br>
<a href="/chapters/1">Chapter 1</a>
<br>
<a href="/chapters/2">Chapter 2</a>
<br>
<a href="/chapters/3">Chapter 3</a>
<br>
</nav>
<main class="content js-content">
</main>
</div>
<script src="demo.js"></script>
</body>
</html>
var CONTENT = {
'1': 'This is the content for the first chapter',
'2': 'Content for the second chapter',
'3': 'Content for the third chapter',
};
function reloadHypothesis() {
var H_SERVER = 'https://hypothes.is';
// Remove existing Hypothesis instance (if any)
var links = [].slice.apply(document.querySelectorAll('link'));
links.forEach(function (linkEl) {
if (linkEl.type === 'application/annotator+html' ||
linkEl.href.startsWith(H_SERVER)) {
linkEl.remove();
}
});
var scripts = [].slice.apply(document.querySelectorAll('script'));
scripts.forEach(function (scriptEl) {
if (scriptEl.src.startsWith(H_SERVER)) {
scriptEl.remove();
}
});
if (window.annotator) {
// Remove event listeners for existing Hypothesis
// instance. This works around a bug in Hypothesis <= 0.21.0
// where window.annotator.destroy() fails to do this itself.
var jq = require('jquery');
var events = ['beforeAnnotationCreated',
'annotationCreated',
'annotationUpdated',
'annotationsLoaded',
'annotationsUnloaded',
'annotationDeleted',
'panelReady',
'setVisibleHighlights'];
events.forEach(function (event) {
jq(document.body).unbind(event);
});
// Remove the Hypothesis UI from the page
window.annotator.destroy();
}
// Install Hypothesis for current URL
var embedScriptEl = document.createElement('script');
embedScriptEl.src = H_SERVER + '/embed.js';
document.head.appendChild(embedScriptEl);
}
var contentEl = document.querySelector('.js-content');
page('/demo.html', function () {
contentEl.innerHTML = 'Click the chapter links on the left';
reloadHypothesis();
});
page('/chapters/:id', function (context) {
contentEl.innerHTML = CONTENT[context.params.id];
reloadHypothesis();
});
document.addEventListener('DOMContentLoaded', function () {
page.start();
});
@tingletech
Copy link

@robertknight is this still the recommended approach to deal with the issue?

@robertknight
Copy link
Author

The part from // Remove existing Hypothesis instance (if any) up until // Install Hypothesis for current URL is obsolete and should be replaced with the method at https://github.com/hypothesis/browser-extension/blob/master/src/chrome/lib/destroy.js. Otherwise yes, this approach is still the best available.

@matt-e-king
Copy link

@robertknight when https://github.com/hypothesis/browser-extension/blob/8c1548dda1c840016c0bbc876e051b6b1f4d810e/src/unload-client.js#L14 is called, is this also closing the websocket that was opened by that specific instance of the client?

@tminhduc2811
Copy link

Hi @robertknight is this still a valid way to let Hypothesis works with SPA applications?

@robertknight
Copy link
Author

@robertknight when https://github.com/hypothesis/browser-extension/blob/8c1548dda1c840016c0bbc876e051b6b1f4d810e/src/unload-client.js#L14 is called, is this also closing the websocket that was opened by that specific instance of the client?

Yes. The WebSocket connection belongs to the sidebar frame, so when the frame is removed, the connection goes away.

Hi @robertknight is this still a valid way to let Hypothesis works with SPA applications?

Hi @tminhduc2811, glad you asked. This information was out of date. I have updated it with new instructions for making Hypothesis play well with SPAs. The good news is that, provided your app follows some best practices, it should work automatically.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment