Choose which component to show on certain route in Vue.js - vue.js

Is it possible to set a condition in a route to decide which component to show in it? Something like this:
let someBoolean = true;
const routes = [
{ path: '/foo', component: someBoolean ? Foo : Baz },
{ path: '/bar', component: Bar }
]
And boolean value can be changed. Depending on that component should also be chosen.

According to Codesandbox, it is possible.
However, the routes tree is built at the app startup, so if you change the variable through an action from your app, nothing should happen, the tree shouldn't be built again.
EDIT:
Vue Router has an API that allows you to update your tree after the tree was built.
You have Router.addRoute(), and this trick that allows you to reset the tree to its initial state and conditionally add the route you want to keep.

Related

How to catch all routes starting with admin

In my vuejs 3 application, I'm trying to add a navigation drawer to all admin routes. So, I need to catch all routes starting with admin (this includes "/admin", "/admin/users", "/admin/users/10" etc). I tried "/admin*", not working. I've tried googling around, no solution too.
One possible approach:
In the component where you render the navigation drawer, you could extract the route path portion that you need and then check if the current route is allowed to render the navigation drawer:
So you could create a function like this:
import { useRoute } from 'vue-router';
const route = useRoute()
//We will pass a parameter to reuse the function in case you need it.
const routeIsAllowed = (requiredRoute) => {
const routePath = route.path.substring(0, requiredRoute.lenght);
return routePath === requiredRoute
}
Then in your template you just render conditionally based on the route path name matching your criteria:
<NavigationDrawer v-if="routeIsAllowed('/admin')" />
There might be a different approach but I think is a valid solution with reutilization in mind.
Also if you want to get an array of routes in a nested route configuration in Vue Router, you just need to get it like this:
full route path: /admin/users
route.matched[0].path // '/admin'
route.matched[1].path // '/users'
Basically matched will give you an array of each route path, which can be accessed with the index value and .path at the end as a string.

(vue router) Is there a neat way to not duplicate code when creating two routes with an almost identical view?

I have a NewArticleView vue component that is mapped to /articles/new, like this:
const routes = [
...
{
path: '/articles/new',
name: 'New Article',
component: NewArticleView
},
...
]
The view is basically just a form.
I want to create an EditArticleView component but it feels wrong making a new component, since it will be almost identical to NewArticleView.
I really wish there was a way to send an edit flag or something when navigating to the route (something like router.push('/articles/new', editArticle=true)), which modifies the behaviour of the component. Like binding form values to the article I want to edit, instead of an empty form, and use an update api call instead of the one used for creating a new article.
Is there a neat way to do this?
You could make one component like ArticleView which accepts a prop named editArticle :
{
path: '/articles/new',
name: 'ArticleView',
component: NewArticleView,
props: route => ({ editArticle: route.query.editArticle})
},
then use it like /articles/new?editArticle=true

VueJS - updating URL with dynamic data

I have a single page VueJS app with a number of data variables I want to encode into the URL.
As an example if my route configuration is (I'm not sure if the syntax is correct):
routes: [
{
path: '/:foo/:bar/:oof/:rab',
component: App
}
And the Component is:
<script>
export default {
name: "App",
data: function() {
return {
foo: 1,
bar: 2,
oof: 3,
rab: 4
}
}
}
Then the URL would be http://www.example.com/#/1/2/3/4
And if foo is changed to 9999 the URL would automatically update: http://www.example.com/#/9999/2/3/4
It would also respond to the user changing the URL, or opening the app with a different URL by loading the URL values into the data.
I thought this would be relatively straightforward but after a Google I'm utterly confused by the correct way of going about this.
Any help/ examples/ solutions greatly appreciated.
Whatever you use to change the values would need to trigger a route push. For example
methods: {
changeFoo (newFoo) {
// you can set foo but it probably won't matter because you're navigating away
this.foo = newFoo
this.$router.push(`/${newFoo}/${this.bar}/${this.oof}/${this.rab}`)
}
See https://router.vuejs.org/guide/essentials/navigation.html
It might be easier if you name your route, eg
routes: [{
name: 'home',
path: '/:foo/:bar/:oof/:rab',
component: App
}]
then you could use something like
this.$router.push({name: 'home', params: this.$data})
It seems that there are two things you're trying to achieve, but I'm not sure from your example which comes first in your ideal order of operations:
Getting the values from your URL
This can be achieved through this.$route.params, which will be an object containing the values you're looking for.
Setting the URL based on your foo, bar, oof, rab variables
You can use this.$router.push() to do this: vue-router docs

explain vue-router component as a function

I have seen in several different places the following type of route definition:
{ path : '/dashboard',
component: { render (c) { return c('router-view') }},
children:[{
path:"",
component: Dashboard
}]
},
I am trying to understand how this is different then
{ path : '/dashboard',
component: Dashboard
},
I think it is related to the optional addition of child routs (e.g. /dashboard/user) so that and the children array here just explains that the Dashboard component renders the path /dashboard whereas if I had the second piece of code then it can only render /dashboard.
What I do want to know is what exactly this does
component: { render (c) { return c('router-view') }},
I assume this is some form of a degenerated component but I don't understand what exactly does it do and how.
In Vue, a component is created using an object containing its configuration.
The simplest possible component may look something like this
componentConfig = {
template: '<div>test</div>'
};
Vue.component('test', componentConfig);
In some cases, a developer might not want to use template, and would want to create element from scratch using pure Javascript. That's where render function comes in.
Vue recommends using templates to build your HTML in the vast majority
of cases. There are situations however, where you really need the full
programmatic power of JavaScript. That’s where you can use the render
function, a closer-to-the-compiler alternative to templates.
from https://v2.vuejs.org/v2/guide/render-function.html#Basics
To change the example above to using render function:
componentConfig = {
render: function(createElement) {
return createElement('div', 'test')
}
};
Vue.component('test', componentConfig);
They would produce the exact same result:
https://codepen.io/jacobgoh101/pen/ZoKwKb?editors=1010
https://codepen.io/jacobgoh101/pen/PemVmy?editors=1010
In other words, render function is simply an alternative to using template.
{
component: {
render(c) {
return c('router-view')
}
}
}
is equal to
{
component: {
render(createElement) {
return createElement('router-view')
}
}
}
is equal to
{
component: {
template: `<router-view></router-view>`
}
}
Because render function is closer-to-the-compiler, it's faster compared to using template. That's probably why the author of your code does it this way.
I don't know the rest of your code, but it looks like this might be an implementation of the vue-router Lazy Loading functionality. Basically, Vue + Webpack are going to split your code into chunks and only load those chunks whenever the user attempts to navigate to those routes, rather than loading them all and creating a bigger bundle to download than necessary.
When building apps with a bundler, the JavaScript bundle can become quite large, and thus affect the page load time. It would be more efficient if we can split each route's components into a separate chunk, and only load them when the route is visited.
Combining Vue's async component feature and webpack's code splitting feature, it's trivially easy to lazy-load route components.

In Vue.js, how do you prevent navigation for a subroute?

The nice thing about beforeRouteLeave is that you can prevent navigating away under certain conditions.
I have a setup that uses a subroute to render part of the page. I would like a navigation guard on the subroute to prevent switching to another one if the data is not saved.
{
path: '/customers/view',
component: ViewCustomerShell,
children: [
{path: ':id', name: 'ViewCustomer', component: ViewCustomer}
]
},
So when I visit /customers/view/12 and make a change, if they try to load /customers/view/13, I want to pop up the usual confirmation and potentially stop navigation. Since beforeRouteLeave is not called in this situation, what is the recommended approach for preventing navigation? It seems that watching $route would be too late, because then the navigation has already occurred.
Note: As mentioned above, beforeRouteLeave is not called in this situation; it doesn't work.
Note: Using onbeforeunload doesn't work because it only triggers when the entire page changes.
I have also posted the same answer here.
Dynamic route matching is specifically designed to make different paths or URLs map to the same route or component. Therefor, changing the argument does not technically count as leaving (or changing) the route, therefor beforeRouteLeave rightly does not get fired.
However, I suggest that one can make the component corresponding to the route responsible for detecting changes in the argument. Basically, whenever the argument changes, record the change then reverse it (hopefully reversal will be fast enough that it gets unnoticed by the user), then ask for confirmation. If user confirms the change, then use your record to "unreverse" the change, but if the user does not confirm, then keep things as they are (do not reverse the reverse).
I have not tested this personally and therefor I do not gurantee it to work, but hopefully it would have cleared up any confusion as to which part of the app is responsible for checking what change.
I know that this post is very old. but it was the first one I found when looking for the same problem.
I have no idea if there is a better solution nowadays but for those who are looking for a solution, I can share mine:
1. Define a global state
let isRouteChangeBlocked: boolean = false;
export function blockRouteChange(set?: boolean): boolean {
if (arguments.length == 1) {
isRouteChangeBlocked = !!set;
return isRouteChangeBlocked;
}
return isRouteChangeBlocked;
}
2. Replace the route function
const originalPush = VueRouter.prototype.push;
VueRouter.prototype.push = function(location: RawLocation) {
if (blockRouteChange()) {
if (confirm("Du hast ungespeicherte Änderungen, möchtest du fortfahren?")) {
blockRouteChange(false);
return originalPush.call(this, location) as any;
}
return;
}
return originalPush.call(this, location) as any;
};
3. Set the state
#Watch("note.text")
private noteTextChanged() {
blockRouteChange(true);
}
This does exactly what I want. If nowadays there is a better solution, let me know. You can get the full runnable example here: https://github.com/gabbersepp/dev.to-posts/tree/master/blog-posts/vuejs-avoid-routes/code/example
You could use a $route object inside your component to watch if it changes and then raise up the confirmation modal... This will get called whenever your route changes!
const Baz = {
data () {
return { saved: false }
},
template: `
<div>
<p>baz ({{ saved ? 'saved' : 'not saved' }})<p>
<button #click="saved = true">save</button>
</div>
`,
watch: {
'$route': function () {
if (this.saved || window.confirm('Not saved, are you sure you want to navigate away?')) {
// do something ...
}
}
}