Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Single Page Applications (SPA) for addons that depend on the URL #157

Open
humitos opened this issue Sep 26, 2023 · 14 comments · May be fixed by #455
Open

Support Single Page Applications (SPA) for addons that depend on the URL #157

humitos opened this issue Sep 26, 2023 · 14 comments · May be fixed by #455
Assignees
Labels
Improvement Minor improvement to code Needed: design decision A core team decision is required

Comments

@humitos
Copy link
Member

humitos commented Sep 26, 2023

Material for MkDocs and other doctools (I think that Docusaurus too) implement a feature that load the content of the page without making a regular request to the server 1:

When instant loading is enabled, clicks on all internal links will be intercepted and dispatched via XHR without fully reloading the page.

This makes addons that depend on the actual URL to not work as expected (e.g. docdiff). When loading the page, the addons API is hit with url= but then the user clicks on an internal link, which uses the instant reload feature to load the page content but our addons doesn't do another request with the url= field updated, making docdiff to use the old URL to compare against.

I think we could listen to an event to check if the URL has changed and if so, make a request to the addons API again with the updated url= attribute. It seems there is a navigator API to do this, but it's experimental for now, tho: https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API

Footnotes

  1. https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/#instant-loading

@humitos humitos added Improvement Minor improvement to code Needed: design decision A core team decision is required labels Sep 26, 2023
@humitos humitos changed the title Support "instant load" concept for addons that depend on the URL Support Single Page Applications (SPA) for addons that depend on the URL Nov 25, 2024
@humitos
Copy link
Member Author

humitos commented Nov 25, 2024

Besides DocDiff, now that we support "keeping on the same page when changing version/language" in the Flyout, this addon also suffers this problem. This happens on Docusaurus and Markdoc too.

@humitos
Copy link
Member Author

humitos commented Nov 27, 2024

I realized that Links Preview also suffer from this. We will need to re-trigger the setup once the URL changes. Otherwise, previews are not installed on the new links of the page.

Steps to reproduce:

  1. Go to https://test-builds.readthedocs.io/en/docusaurus/docs/tutorial-basics/create-a-blog-post/
  2. Click on "Next: Markdown Features".
  3. You will see that links like "Create page" and "the extra guides" does not have Links Preview.

@humitos
Copy link
Member Author

humitos commented Nov 27, 2024

I think it would be good to implement this as an utils function that triggers an event PAGE_CHANGED (or similar) that addons can subscribe themselves to re-run their setup if required.

@zanderle
Copy link
Collaborator

@humitos so if I understand correctly, you want to use Navigation API? The support for this isn't looking to good yet :/ https://developer.mozilla.org/en-US/docs/Web/API/Navigation/navigate_event

An alternative (which I'm not a fan of) would be to monkey-patch history.pushState, so that it would fire up PAGE_CHANGED every time it's called.

Another possibility (not sure how feasible this would be) could be to add a script to every page that fires some event that addons listen to and act accordingly?

OR could we make this a responsibility of the theme instead? If your theme is using SPA, you need to fire ADDONS_PAGE_CHANGED event, to make addons work correctly?

@humitos
Copy link
Member Author

humitos commented Nov 28, 2024

@humitos so if I understand correctly, you want to use Navigation API? The support for this isn't looking to good yet :/ developer.mozilla.org/en-US/docs/Web/API/Navigation/navigate_event

Hrm, OK. That sounds like a no go, then. Support is pretty limited 😞

An alternative (which I'm not a fan of) would be to monkey-patch history.pushState, so that it would fire up PAGE_CHANGED every time it's called.

I'm not a fan either, but I think we can probably start here for now to have an initial implementation and see how it behaves. By the way, is it possible the change the URL skipping the history?

Another possibility (not sure how feasible this would be) could be to add a script to every page that fires some event that addons listen to and act accordingly?

I'm not sure to understand how this would work. Can you expand on this idea?

OR could we make this a responsibility of the theme instead? If your theme is using SPA, you need to fire ADDONS_PAGE_CHANGED event, to make addons work correctly?

Do you mean Docusaurus and/or Material for MkDocs triggering this event? If so, I don't think these frameworks will ever implement such integration, so I would try to not rely on them if possible.

@zanderle
Copy link
Collaborator

@humitos so if I understand correctly, you want to use Navigation API? The support for this isn't looking to good yet :/ developer.mozilla.org/en-US/docs/Web/API/Navigation/navigate_event

Hrm, OK. That sounds like a no go, then. Support is pretty limited 😞

Yeah, it doesn't seem viable at this point.

An alternative (which I'm not a fan of) would be to monkey-patch history.pushState, so that it would fire up PAGE_CHANGED every time it's called.

I'm not a fan either, but I think we can probably start here for now to have an initial implementation and see how it behaves.

Yeah, we can try that 👍

By the way, is it possible the change the URL skipping the history?

You mean, somehow change the URL without using history API? Or do you mean change the URL, without placing it in history?
The answer to former is: not without (re)loading the page (except with Navigation API, I believe, which isn't normally used anyway). The answer to latter is: yes, but it will still use the history API: https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState so we can monkey-patch that as well.

Another possibility (not sure how feasible this would be) could be to add a script to every page that fires some event that addons listen to and act accordingly?

I'm not sure to understand how this would work. Can you expand on this idea?

I looked through the source code of how mkdocs do instant-loading, and it seems that any script that's on the page will be executed when the page is loaded. So if we could have a script on each of their pages, that would be executed, that could be a way to trigger the event? And to be clear: just thinking out loud here, no idea if this would work.

OR could we make this a responsibility of the theme instead? If your theme is using SPA, you need to fire ADDONS_PAGE_CHANGED event, to make addons work correctly?

Do you mean Docusaurus and/or Material for MkDocs triggering this event? If so, I don't think these frameworks will ever implement such integration, so I would try to not rely on them if possible.

Ah, too bad.

@humitos
Copy link
Member Author

humitos commented Nov 28, 2024

I looked through the source code of how mkdocs do instant-loading, and it seems that any script that's on the page will be executed when the page is loaded. So if we could have a script on each of their pages, that would be executed, that could be a way to trigger the event? And to be clear: just thinking out loud here, no idea if this would work.

Hrm, interesting. If it works like you are describing, it should work for us without doing anything specific on the Material for MkDocs case, since we are including a script tag with our readthedocs-addons.js file on each page that is returned from the server.

I created an example for Material for MkDocs with "Instant loading" enabled at https://test-builds.readthedocs.io/en/material-mkdocs-instant-loading/. It seems it doesn't re-run our addons code when switching pages 🤔

... maybe, it is re-running our code, but since the readthedocs- elements already exist in the DOM we skip reloading them due to this code:

addons/src/flyout.js

Lines 376 to 389 in c389f31

// If there are no elements found, inject one
let elems = document.querySelectorAll("readthedocs-flyout");
if (!elems.length) {
elems = [new FlyoutElement()];
// We cannot use `render(elems[0], document.body)` because there is a race conditions between all the addons.
// So, we append the web-component first and then request an update of it.
document.body.append(elems[0]);
elems[0].requestUpdate();
}
for (const elem of elems) {
elem.loadConfig(config);
}
-- I don't think that's the case tho, since there are addons where we don't have that check.

@zanderle
Copy link
Collaborator

I looked through the source code of how mkdocs do instant-loading, and it seems that any script that's on the page will be executed when the page is loaded. So if we could have a script on each of their pages, that would be executed, that could be a way to trigger the event? And to be clear: just thinking out loud here, no idea if this would work.

Hrm, interesting. If it works like you are describing, it should work for us without doing anything specific on the Material for MkDocs case, since we are including a script tag with our readthedocs-addons.js file on each page that is returned from the server.

I created an example for Material for MkDocs with "Instant loading" enabled at https://test-builds.readthedocs.io/en/material-mkdocs-instant-loading/. It seems it doesn't re-run our addons code when switching pages 🤔

... maybe, it is re-running our code, but since the readthedocs- elements already exist in the DOM we skip reloading them due to this code:

addons/src/flyout.js

Lines 376 to 389 in c389f31

// If there are no elements found, inject one
let elems = document.querySelectorAll("readthedocs-flyout");
if (!elems.length) {
elems = [new FlyoutElement()];
// We cannot use `render(elems[0], document.body)` because there is a race conditions between all the addons.
// So, we append the web-component first and then request an update of it.
document.body.append(elems[0]);
elems[0].requestUpdate();
}
for (const elem of elems) {
elem.loadConfig(config);
}

-- I don't think that's the case tho, since there are addons where we don't have that check.

Hm, yeah it probably doesn't work the way I would want it to. I looked at this section, but I'm not sure it does the trick:

https://github.com/squidfunk/mkdocs-material/blob/60c0dc1b8af71cef004eb7a91ca6ea8bae1e76cc/src/templates/assets/javascripts/integrations/instant/index.ts#L217-L244

Maybe it would work if the script was in body instead of head, but I'm not sure.

However upon further research, I found this:

https://github.com/squidfunk/mkdocs-material/blob/60c0dc1b8af71cef004eb7a91ca6ea8bae1e76cc/docs/customization.md?plain=1#L59-L78

The problem here is that I assume you want a general solution. Not a solution that would work just for mkdocs

@humitos
Copy link
Member Author

humitos commented Nov 28, 2024

The problem here is that I assume you want a general solution

Yes, we want a general solution that works independently from the documentation tool and/or theme.

@zanderle
Copy link
Collaborator

@humitos monkey-patching until we find something better then?

@humitos
Copy link
Member Author

humitos commented Nov 29, 2024

Yes. I think it's fine for now.

@humitos
Copy link
Member Author

humitos commented Dec 3, 2024

We talked about Lit context pattern, https://lit.dev/docs/data/context/. I think we may be able to put the config object in the context and share it between all the addons. If we need to perform a new request to the API to get fresh data, we update the config object in the context and it should refreash all the addons automatically.

This is just an idea to explore... 😄

@humitos
Copy link
Member Author

humitos commented Dec 4, 2024

The pattern using Lit context should be useful to for #356 as well, because we will be able to initialize addons when new responses arrives from the server. Only those addons with all the required data will be enabled and they will be updated with new data as it arrives 💯

@agjohnson
Copy link
Contributor

Re context API, this is maybe where ContextRoot can be used.

https://lit.dev/docs/data/context/#contextroot

However, the docs here aren't clear and there aren't great examples for ContextRoot 🤷

humitos added a commit that referenced this issue Dec 5, 2024
A new event `READTHEDOCS_ROOT_DOM_CHANGED` is triggered when the "Visual diff"
is enabled/disabled after the DOM was modified.

This allows other addons to subscribe to this event to re-initialize if
required. I used this event from links preview to call `setupTooltips` again to
re-install tooltips on these links.

I migrated links preview to a `LitElement` to be able to make usage of this
pattern and follow what we already had.

We will be able to do something similar on #157
@humitos humitos moved this from Planned to In progress in 📍Roadmap Dec 9, 2024
humitos added a commit that referenced this issue Dec 10, 2024
A new event `READTHEDOCS_ROOT_DOM_CHANGED` is triggered when the "Visual
diff" is enabled/disabled after the DOM was modified.

This allows other addons to subscribe to this event to re-initialize if
required. I used this event from links preview to call `setupTooltips`
again to re-install tooltips on these links.

I migrated links preview to a `LitElement` to be able to make usage of
this pattern and follow what we already had.

We will be able to do something similar on #157

Closes #460
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Improvement Minor improvement to code Needed: design decision A core team decision is required
Projects
Status: In progress
Development

Successfully merging a pull request may close this issue.

3 participants