I’m currently developing an app, using the vue stack – vue, vue router and vuex. The user is browsing articles which are open in modal windows. You can open article while you are browsing a folder (url “/folder/name”) or while you are browing search results (url “/search/term”). If the user opens the modal URL /article/{id} he should see the article open in the main <router-view> without the modal.
This looks like a simple problem but trying to implement it I stumbled upon a simple vue router limitation – you cannot keep the context (component) when navigating to different URL. There is a discussion in github from 2016 without any “good” solution to this “simple” problem – https://github.com/vuejs/vue-router/issues/703. Since this is very important for me I started to try to find a solution to the problem and I will try to share my findings in this blog post.
The articles modal is triggered from an url like “/article/3a9c6e7e97fd73c1-discord-has-a-new-problem-revenge-porn”. So how do I keep the current browsed/searched components and open modal on top !?
At first I developed the BrowseComponent so I found an easy solution – add the route with the same component and add some logic to the BrowseComponent. This was the first iteration of routes :
{ path: '/folder/:name', component: BrowseComponent, name: 'folder' }, { path: '/article/:article', component: BrowseComponent, name: 'article_view', }
This worked well until I implemented the SearchComponent and got stuck :). How was I supposed to open the modal and keep the context !? Some ideas went through my head:
- make BrowseComponent the base of search and browse pages and implement some logic inside to change the component depending on the route – this become very complex to handle
- dynamically add a route “/article/:id” when opening search or browse component with the corresponding component in the route definition – this was no go since you cannot check for existing routes and you cannot modify/delete routes
- add logic to open the modal without changing the URL and change it via history.pushstate({}, null, url) to the desired format “/article/{id}” – these caused some errors thrown from vue router about missing routes and there were some problems with the navigation back/forward … and it doesnt look write. This was proposed by some of the comments in github and is closest to a real solution
- another solution was this https://github.com/vuejs/vue-router/issues/703#issuecomment-389894487 – but I find it way too “hack”-y and i’m not sure how this can work with more than one parent context
So I posted a comment in the issue section and started waiting for “someone” to find a soltuion. As I was writing the comment an idea came to me
What about we create a functional component – that checks what is the current viewed component and return it in the render function – but execute the logic to open the modal. I thought that the Vue cache logic would keep the instance so nothing in the parent will change – and it worked 🙂
Here is
//ArticleViewBase.js let parentComponent = null export default { functional: true, render (createElement, context) { return createElement( parentComponent || BrowseComponent, context.data, context.children ) }, beforeRouteEnter (to, from, next) { parentComponent = router.getMatchedComponents()[0] next() }, }
I’ll try to explain what is happening here.
First I create a functional component which I set in the router for the modal view:
{ path: '/article/:article', component: ArticleViewBase, name: 'article_view' },
In the component I create beforeRouteEnter hook in order to get the current rendered component (the context) that I need to preserve and save it to local var parentComponent.
Then in the render function I just return the saved componet and Vue is smart enough to detect this component and reuse it – so the state is preserved.
Finally in the render function I use:
parentComponent || BrowseComponent,
because if you open the link directly vue router still needs to render something in the <router-view> and this plays role as a default component.
Hope this helps someone. May be there are better solutions – i’m open for discussion if you find something wrong or may be another solution.
NOTE: This solutions is not working with nested routes yet. I’m still trying to find a solution with no luck. If you have any idea please share it in the comments below.