1. Principles of vue-router
- vue-router lets you build a vuejs SPA application in the manner of an API based on routes to determine which information is requested.
- vue-router therefore lets you define routes, in the form of a path some parts of which represent parameters (i.e. /a/:myparam/c)
- Each route is associated with one or more components and not with processing operations.
- Consequently, when you follow a route, it does not trigger a processing operation (e.g. a query in a database), but the display of the components associated with the route.
WARNING! The display locations of the components are the same for all routes, which constitutes a strong constraint for structuring the visuals and navigation in the application.
- Generally, you give a name to these locations and when you define a route, you indicate for the associated components the name of the location where they will be displayed.
- Fortunately, if there are 4 locations planned in the application, a route is not obliged to be associated with 4 components to fill the 4 locations. It can leave some empty, which gives flexibility to design the visuals of the application.
- using vue-router therefore consists of:
- in a special JS file: defining the possible routes, which components they allow to display and in which location,
- in the templates of the components:
- defining display locations for the components associated with routes,
- creating links or actions in the components of the application, which will allow these routes to be followed.
- in a special JS file: defining the possible routes, which components they allow to display and in which location,
- The huge advantage of this approach is that you can easily add routes, and therefore components to display, as development progresses.
- For example, an SPA application with a menu on the left and a central page becomes trivial to program thanks to vue-router.
- It is enough for each menu item to follow a different route and for the central page to serve to display the component associated with each route.
- No more need to use v-if, v-else-if, ... to display this or that component!
2. Integration into a project
- The simplest way to use vue-router is to include it at the time of creating a project with vite.
- For this, you simply use the manual selection of "plugins" in the menu that vite displays
- Right after, a list of different plugins is offered and you simply check Router, then press enter.
- This creates a "HelloWorld" type project but where vue-router is integrated.
- For example, in src, you can see the appearance of the views and router directories.
- views contains the components that will be associated with routes. Note that there is nothing else special about them: you could just as well put them in any directory, for example components. It is just for the sake of clarity that they are separated from the other components, not associated with routes.
- router contains a JS file that defines the routes and creates the router of the application (NB: there is only one router per application)
- this file is imported into main.js, which allows the vue instance to access the router.
- App.vue has also completely changed: you discover <RouterLink> and <RouterView> tags (or <router-link> and <router-view> which are the same), which are specific to vue-router and described below.
3. Basic syntax
3.1. Defining a route
- To create a route in a project created with vue-cli, you modify the index.js file located in the router directory.
- A route is a JSON object comprising at least 2 properties:
- path: the "path" describing the route
- components: the names of the locations used and the name of the components displayed there.
- NB: when the application is very simple and requires only a single location for all routes, you can use the component property (without s), which just indicates the name of the component to display in the location.
- You can use a regular expression for the value of path. For example, you can use * to replace a certain number of characters.
- This allows, among other things, the creation of a default route when no other matches.
WARNING! the order in which routes are defined is important. The first one encountered whose format matches the one you want to follow is taken.
- A route can also have a name thanks to the name property.
- This name can then be used to follow the route (rather than with a path).
- Example:
{
path: '/home',
components: {
main: HomeView,
left : Menu
},
name: 'home'
}
{
path: '*',
components: {
main: Error404
}
}
Remarks:
- we assume that there are two display locations named main and left.
- when you follow the route /home, you display the HomeView component in the main location and the Menu component in the left location.
- any other route followed will result in displaying the Error404 component in the main location and left will be empty.
3.2. Following a route
- To follow a route, vue-router offers two solutions:
- create clickable links that will appear in the components: <router-link> tag
- retrieve an object representing the router, to then call its push() function, which takes as a parameter a route to follow.
- In fact <router-link> will allow the creation of a <a href="/..."> tag and when you click the link, it calls the push() function
- The two solutions are therefore strictly equivalent in effect.
WARNING! There is an unavoidable third possibility: the user can directly type a route in the navigation bar. This is a simple way to carry out attacks against the application. You must therefore code your routes (notably those with parameters) taking this possibility into account.
3.2.1. <router-link>
- This tag has a to attribute that allows you to specify the route to follow.
- You can set the value of to with a constant string, or with an object giving the name and possibly the parameters of the route. In this case, you use v-bind to assign this object.
- Examples (based on the routes defined previously):
<router-link to="/home">Home</router-link>
<router-link :to="{path: '/home'}">Home</router-link>
- The first and the second line have an identical result.
- The difference is that with :to, you can pass dynamic parameter values (covered in 4.2)
3.2.2. push()
- You sometimes want to follow a route following an event that is not a click on a link.
- In this case, you use the push() function which takes as a parameter exactly the same thing as the to attribute.
- However, this function is a method of an object representing the router.
- To retrieve the latter, you have to import the useRouter() function and call it.
- NB: you can name the object obtained as you like, but custom dictates calling it router.
Example (we assume it is placed in a component having a user variable/prop):
<template>
...
<button @click="router.push({ name: 'useredit', params: { id: user.id, edit_mode: 'reset'} })"> Reset profile of {{user.name}} </button>
</template>
<script setup>
import {useRouter} from 'vue-router' // import function that returns the router object
const router = useRouter() // get the router object in router variable (can use another name if you want)
</script>
3.2.3. query
- In a URI, it is possible to specify query variables, by using after the path: ?var1=val1&var2=val2& ...
- This is also possible for following a vue-router route.
- The component that will be displayed can retrieve these variables, thanks to an object representing the current route.
- For this, it must import the useRoute() function, then call it to retrieve the object representing the route.
- NB: custom dictates naming this object route.
- The route object contains a query object with inside it the variables of the query and their value.
Example:
- in the template of a component:
<router-link to="/home?msg=hello">
- in the displayed component:
<template>
...
{{route.query.msg }}
</template>
<script setup>
import {useRoute} from 'vue-router'
const route = useRoute()
</script>
- {{ route.query.msg }} will allow the display of hello.
3.3. Displaying the components associated with a route.
- To define the locations where the components associated with routes will be displayed, you use the <router-view> tag.
- This tag has a name attribute that sets the name of the location, and which is used when defining the routes.
- REMINDER: the locations are fixed in the application. This is logical since they are defined by where you put the <router-view> tags.
Oddity: you can use <router-view> several times with the same name attribute. This simply allows the same component to be displayed several times in several places. What use is that??
- The location constraint is loosened by the fact that a route is not obliged to display a component for all the defined locations.
- Moreover, it is possible to create nested locations, thanks to multi-level routes (see section 4).
- Finally, if needed, v-if allows the location constraint to be circumvented: it allows <router-view> to be used at this or that place in a component depending on conditions.
- In conclusion, with a little practice, vue-router can be used with any visual structure of an application.
4. Advanced principles
4.1. multi-level routes and nested locations
- The very principle of components is to nest them.
- This is why vue-router lets you create nested locations.
- For example, if a component contains <router-view name="loc1">, this allows a component to be displayed in place of the tag.
- Suppose this component itself contains <router-view name="loc2">, you indeed get nested locations.
- In this situation, to display something in loc2, you must necessarily define a 2-level route.
- To create such a route, you use the children property when defining the route.
- This allows an existing route to be extended, by completing its path with new segments.
- Example:
- App.vue contains in its template <router-view name="loc1">.
- componentA.vue contains in its template <router-view name="loc2">.
- when you follow the route /level1, you want to display componentA in the loc1 location.
- when you follow the route /level1/level2.1, you want to display componentA in loc1 AND componentB in loc2.
- when you follow the route /level1/level2.2, you want to display componentA in loc1 AND componentC in loc2.
- In router/index.js, you put:
{
path: '/level1',
components: {
loc1 : componentA
},
children: [
{
path: 'level2.1',
components: {
loc2 : componentB
}
},
{
path: 'level2.2',
components: {
loc2 : componentC
}
}
]
}
Remarks:
- This mechanism is applicable to N levels. You simply create children of children of etc.
- In this example, there are two possible routes for level 2 but they both start with /level1. This implies that it is necessarily componentA that is displayed in loc1 for both routes. In other words, it is impossible to change the component of level n-1 when you define level n.
- Generally, the path variables of level > 1 never start with /. Otherwise, you "overwrite" the segments of lower level. For example, if you write path: '/level2.1', then to follow this level-2 route, you use only /level2.1 instead of /level1/level2.1, which can be a source of confusion (or even incorrect if level 1 is a parameter).
WARNING ! it is advised to use different location names regardless of the level. Indeed, you could very well use loc1 instead of loc2 for the second level because vue-router does not mix the locations of different levels. But with identical names, it is quite difficult to find your way around
4.2. parameterized routes
- As for REST, some segments of the route can correspond to parameters that you can use in the component that will be displayed.
4.2.1. Creation
- To specify that a segment corresponds to a parameter, the segment simply needs to be a word starting with :
- You can put several parameters in a route.
- To retrieve the value of the parameters in the component and use it in its template, you use the route.params object (assuming you have retrieved the route object in <script>, as seen in 3.2.3) when you are in a method of the component)
- Example: in index.js
{
path: '/users/:id/edit/:edit_mode',
component: UserEdit,
name: 'useredit'
}
- in UserEdit.vue:
<template>
<div> edit user {{ route.params.id}} in mode {{route.params.edit_mode}} </div>
</template>
...
- in a component allowing the route useredit to be followed:
<router-link to="/user/23/edit/reset">User profile reset</router-link>
<router-link :to="{ name:'useredit', params: { id:23, edit_mode:'reset'} }">User profile reset</router-link>
Remarks:
- the two lines above have the same effect, but thanks to :to, you could specify an id that is not fixed, whose value is drawn from one of the component's variables.
- using vue-router parameters in a component makes this component dependent on the use of vue-router. This can pose a problem for components intended to be reusable.
4.2.2. Changes of parameter values
- If the value of the parameters changes, vue-router considers that it is still the same route and will not reload the components associated with the route.
- If the component uses vuejs instructions manipulating the parameters, these instructions will be automatically re-evaluated, which will no doubt produce a change on the screen.
- For example, for the UserEdit.vue component above:
- if you follow the route /user/23/edit/reset, the component displays: edit user 23 in mode reset.
- if afterwards you follow the route /user/12/edit/changename, the display changes automatically to: edit user 12 in mode changename.
- This would also work with instructions of the v-for, v-if, ... type. For example, if you want to display a different sub-component depending on the value of a parameter:
<div v-if="route.params.edit_mode=='reset'"><FormReset /></div>
<div v-else><FormEdit /></div>
- This also works with computed-type methods.
- On the other hand, since the component is not reloaded, all the functions linked to the setting up of the component (onMounted(), ...) are not called again.
- Now, it is common to put code in these functions to go fetch external data (e.g., via axios) needed to display the component.
- If the fetching of external data must be restarted when the parameters change, you have to put it in a method that observes changes of parameters, i.e. a watch-type function.
- For example, suppose a route of the type /user/:id exists, allowing the following component to be displayed:
<template>
{{user.name}} {{user.age}}
</template>
<script>
export default {
...
data: () => {
return { user: {name:'', age:''} }
},
watch: {
$route(to, from) {
this.user = ... // use axios to get user's infos according to $route.params.id
}
}
}
</script>
- The watch section contains methods that have the same name as the variables whose changes you want to observe. These methods have 2 parameters, to and from giving the value of the variable after and before the change.
- In this instance, you want to observe the changes of $route. Note that the to and from parameters are not useful in this case since the processing is done depending on the id parameter.
4.3. Passing props to components
4.3.1. via the parameters of a route
- As said above, a component using route.params is not really reusable.
- Fortunately, vue-router provides a mechanism allowing the parameters to be transformed into props.
- You can thus code the component in a way totally independent of vue-router.
- To use the automatic conversion, you have to add a props property to the routes, and this for each location linked to a route.
- Indeed, each location can potentially display a component. You therefore have to give the conversion rules for each one so that the components receive the right props.
- There are 3 conversion principles:
- take the parameters and transform them into props of the same name.
- not use the parameters but provide the component with an object containing the props you want.
- use a function whose parameter is the current route object and which will create the props object provided to the component.
- The first solution is not very generic: when you create a component, you set the name of its props independently of anything else. You therefore have to create routes with parameters whose names correspond to the props, which can be complicated, especially with multi-level routes.
- The second solution is little used because if you create routes with parameters, it is generally to use them in the displayed components.
- The third solution combines the previous two and allows the parameters to be used, renamed, other props to be passed, etc.
Solution 1 example, based on the parameterized routes of section 4.2.
- The UserEdit.vue component becomes:
<template>
<div>edit user {{id}} in mode {{edit_mode}}</div>
</template>
<script>
export default {
name: 'UserEdit',
props: [ 'id','edit_mode' ]
}
</script>
- router/index.js contains:
{
path: '/users/:id/edit/:edit_mode',
name: 'useredit',
components: {
locProfile : UserEdit
},
props: {
locProfile : true
}
}
- It is the fact of putting true as the value of locProfile in props that tells vue-router to transform the id and edit_route parameters into props of the same name for the component that will be displayed in locProfile.
- In the present case, it is UserEdit that is displayed, which is consistent since it has id and edit_mode as props.
Solution 3 example.
- This time, we assume that UserEdit.vue has already been written as follows:
<template>
<div>
<h3>{{title}}</h3>
edit user {{userId}} in mode {{editMode}}
new user name : {{newName}}
</div>
</template>
<script setup>
const props = defineProps([ 'title', 'userId','editMode','newName' ])
...
</script>
- We also assume that the format of the route stays the same as previously but that a rename query variable allows the new name to be specified (example route: /user/46/edit/chgname?rename=toto)
- This gives for router/index.js:
{
path: '/users/:id/edit/:edit_mode',
name: 'useredit',
components: {
locProfile : UserEdit
},
props: {
locProfile : route => { return {title:'User profile', userId: route.params.id, editMode: route.params.edit_mode, newName: route.query.rename } }
}
}
- This solution relies on the definition of an arrow function taking as a parameter a variable, named route in this example (NB: you can use whatever name you want).
- The return of this function is an object that will be provided as props to the displayed component, in this instance UserEdit.
- The value of the route variable is set by vue-router and corresponds to the current route object. It is therefore the same value as that of $route in a component.
- You can therefore access the parameters and query of the current route, thanks to route.params and route.query.
- The returned object can thus contain properties corresponding to the props of UserEdit, with values either constant (e.g., title), or drawn from the current route.
4.3.2. via <router-view>
- Like any tag created via vuejs, you can add any attribute to <router-view>.
- This implies that you can add an attribute corresponding to a prop of the component that will be displayed at the location of <router-view>
- If this location is used to display different components, you simply add as many attributes as you want, corresponding to the different props of these components, giving them a fixed value, or with v-bind.
- Indeed, a component can very well be instantiated with attributes that will not be of use to it: this does not cause an error.
- Remark: this also works with the v-on attribute
- Example, based on the illustrative example of section 3.4:
- In MainPage.vue, a <router-view> tag allows the display of either the Home component, or the Cours component, depending on the route followed.
- If we assume that Home and Cours have respectively props named news and matieres, you can give them a value thanks to v-bind in <router-view>
<template>
<div>
<router-view name="locCentral" :news="infos" :courses="courses"/>
</div>
</template>
<script setup>
...
const infos = ref("Hello U all")
const courses = ref([ {name: "Algo"}, {name: "Java"}])
...
</script>