Skip to content

Commit

Permalink
feat: enable navigation to hashes
Browse files Browse the repository at this point in the history
Previously this was not officially supported, and would only work when moving within a page, or when initially loading a page with SSG enabled.
  • Loading branch information
iisakkirotko committed Oct 14, 2024
1 parent 541b4bc commit 172f50c
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 9 deletions.
12 changes: 11 additions & 1 deletion solara/server/static/main-vuetify.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ async function solaraInit(mountId, appName) {
window.navigator.sendBeacon(close_url);
}
});
let kernel = await solara.connectKernel(solara.jupyterRootPath, kernelId)
let kernel = await solara.connectKernel(solara.rootPath + '/jupyter', kernelId)
if (!kernel) {
return;
}
Expand All @@ -138,15 +138,25 @@ async function solaraInit(mountId, appName) {
});

window.addEventListener('solara.router', function (event) {
app.$data.urlHasChanged = true;
if(kernel.status == 'busy') {
app.$data.loadingPage = true;
}
});
kernel.statusChanged.connect(() => {
// When navigation is triggered from the front-end, kernel.status becoming busy and
// solara.router event happen in a different order than when navigating through Python, so
// if the URL has changed when the kernel becomes busy, we set loadingPage to true
if (kernel.status == 'busy' && app.$data.urlHasChanged) {
app.$data.loadingPage = true;
}
// the first idle after a loadingPage == true (a router event)
// will be used as indicator that the page is loaded
if (app.$data.loadingPage && kernel.status == 'idle') {
app.$data.loadingPage = false;
app.$data.urlHasChanged = false;
const event = new Event('solara.pageReady');
window.dispatchEvent(event);
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,16 @@ def LinkToIpywidgets():
return main
```

### Linking to Sections of a Page

The `solara.Link` component also supports linking to HTML elements identified by id. Although most Solara components don't directly support the id attribute, you can assign ids to all ipyvuetify components, using the `attributes` argument:

```python
solara.v.Btn(attributes={"id": "my-id"}, ...)
```

You can then link to a particular element by appending `#` followed by its id to your link, i.e. `solara.Link(route_or_path="/page#my-id")`.

## Fully manual routing

If you want to do routing fully manually, you can use the [`solara.use_router`](/documentation/api/routing/use_router) hook, and use the `.path` attribute.
Expand Down
54 changes: 46 additions & 8 deletions solara/widgets/vue/navigator.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,33 @@ modules.export = {
}
window.solara.router.push = (href) => {
console.log("external router push", href);
// take of the anchor
if (href.indexOf("#") !== -1) {
href = href.slice(0, href.indexOf("#"));
}
this.location = href;
const url = new URL(href, window.location.origin + solara.rootPath);
this.location = url.pathname + url.search;
this.hash = url.hash;
};
let location = window.location.pathname.slice(solara.rootPath.length);
this.location = location + window.location.search;
this.hash = window.location.hash;
window.addEventListener("popstate", this.onPopState);
window.addEventListener("scroll", this.onScroll);
window.addEventListener("hashchange", this.onHashChange);
window.addEventListener("solara.pageReady", this.onPageLoad);
},
destroyed() {
window.removeEventListener("popstate", this.onPopState);
window.removeEventListener("scroll", this.onScroll);
window.removeEventListener("hashchange", this.onHashChange);
window.removeEventListener("solara.pageReady", this.onPageLoad);
},
methods: {
onScroll() {
window.history.replaceState(
{ top: document.documentElement.scrollTop },
null,
solara.rootPath + this.location
this.makeFullRelativeUrl()
);
},
onPopState(event) {
console.log("pop state!", event.state, window.location.href);
if (!window.location.pathname.startsWith(solara.rootPath)) {
throw `window.location.pathname = ${window.location.pathname}, but it should start with the solara.rootPath = ${solara.rootPath}`;
}
Expand All @@ -55,6 +57,32 @@ modules.export = {
*/
}
},
onHashChange(event) {
if (!window.location.pathname.startsWith(solara.rootPath)) {
throw `window.location.pathname = ${window.location.pathname}, but it should start with the solara.rootPath = ${solara.rootPath}`;
}
this.hash = window.location.hash;
},
onPageLoad(event) {
if (!window.location.pathname.startsWith(solara.rootPath)) {
throw `window.location.pathname = ${window.location.pathname}, but it should start with the solara.rootPath = ${solara.rootPath}`;
}
// If we've navigated to a hash with the same name on a different page the watch on hash won't trigger
if (this.hash && this.hash === window.location.hash) {
this.navigateToHash(this.hash);
}
this.hash = window.location.hash;
},
makeFullRelativeUrl() {
const url = new URL(this.location, window.location.origin + solara.rootPath);
return url.pathname + this.hash + url.search;
},
navigateToHash(hash) {
const targetEl = document.getElementById(hash.slice(1));
if (targetEl) {
targetEl.scrollIntoView();
}
},
},
watch: {
location(value) {
Expand All @@ -81,7 +109,7 @@ modules.export = {
document.documentElement.scrollTop
);
if (oldLocation != this.location) {
window.history.pushState({ top: 0 }, null, solara.rootPath + this.location);
window.history.pushState({ top: 0 }, null, this.makeFullRelativeUrl());
if (pathnameNew != pathnameOld) {
// we scroll to the top only when we change page, not when we change
// the search string
Expand All @@ -91,6 +119,16 @@ modules.export = {
window.dispatchEvent(event);
}
},
hash(value) {
if (value) {
this.navigateToHash(value);
}
},
},
data() {
return {
hash: "",
};
},
};
</script>

0 comments on commit 172f50c

Please sign in to comment.