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

feat: enable navigation to hashes #814

Merged
merged 2 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions solara/server/static/main-vuetify.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
window.location.href
);
},
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>
Loading