How to pass state and events handlers as props to children components with Vue.js's Router? - vue.js

Before using routing, I had the following Dashboard component:
<template>
<div>
<RegistrationForm v-on:add-employee="addEmployee" />
<EmployeeTable
v-bind:employees="employees"
v-on:delete-employee="deleteEmployee"
/>
</div>
</template>
<script>
...
export default {
components: {
EmployeeTable,
RegistrationForm
},
data() {
return {
employees: []
};
},
methods:{
deleteEmployee() {...}
addEmployee() {...}
}
}
</script>
I want to have to separate routes one for registering a new employee and one for listing all employees, so what I did first is updating the above template:
<template>
<div>
<router-view/>
</div>
</template>
Then I defined a router object:
export default new Router({
mode: 'history',
routes: [
{
path: "/dashboard",
name: "dashboard",
component: Dashboard,
children: [
{
path: 'add-employee',
component: RegistrationForm,
},
{
path: 'list-employees',
component: EmployeeTable,
}
]
}
]
});
My question is how to pass the employees state variable and deleteEmployee, addEmployee methods from the Dashboard component to its children components?
Update:
I do not know why I did not receive any response on this question, although this is a common and trivial task to do in other frameworks, for instance in React:
...
export default function BasicExample() {
const [x, setX] = useState("World");
return (
<Router>
<div>
<ul>
<li>
<Link to="/">Home</Link>
</li>
</ul>
<Switch>
<Route exact path="/">
<Home x={x}/>
</Route>
</Switch>
</div>
</Router>
);
}
function Home({x}) {
return (
<div>
<h2>Hello {x}</h2>
</div>
);
}

After using Vue-router for a short period of time, I concluded that this is not a limitation, but more like a design decision:
React-router: is more like a condional renderer, e.i from within the parent component and based on the current path, render the apporopriate child.
Vue-router: is more like a wiring solution.
Also I have to say: a similar behavior to React-router can be achieved using Vue-router. This by combining the router's currentRoute property with the v-if directive, which truly insert/remove a component from the DOM (not hiding it using css as v-show do).
Finally: seprating routes configuration is advantegeous, as it yield more modular project and better SPA. Of-course this separation can be done in both Vue-router and React-router (look here for React-router).

Related

this.$route.name is undefined inside the app.vue when console it inside created()

I am using vue3 and I am a newcomer to this. With the use of vue-router I created the routes. But I want to get the this.$route.name inside the app.vue. Therefore tried to get the value inside the created method. But at the beginning that value is undefined.
You get null because $route.name is not intialized yet in the created method.
You can instead use a computed variable, so this way $route.name will be initialized.
<template>
<div class="main-view">
<Navigation v-if="enableNavigation" />
<router-view />
<Footer />
</template>
export default {
name: 'App',
computed: {
enableNavigation() {
const disabledNavRoutes = ['login', 'register', 'resetPassword'];
return disabledNavRoutes.indexOf(this.$route.name) === -1;
}
}
}
In your case you want to display the Navigation component but only in some routes (the route when the user is not authenticated i assume). Here some alternatives:
Method 1: nested routes
router.js
const router = new Router({
mode: 'history',
base: process.env.BASE_URL,
routes: [
{
path: '/login',
name: 'login',
component: LoginView,
},
{
path: '/home',
name: 'home-shell',
component: HomeShell,
children: [
{
path: 'home',
name: 'home',
component: HomeView
},
]
},
],
});
Remove the Navigation from App.js to remove it from login / register component
App.js
<template>
<div class="main-view">
<router-view></router-view>
</div>
</template>
Put the navigation and footer component in the HomeShell
HomeShell.js
<template>
<Navigation />
<--! Nested router. In our case HomeView !-->
<router-view />
<Footer />
</template>
And the HomeView component will be injected in the router-view of the HomeShell
Method 2: v-if
An other way would be to have a variable isAuthenticated in the store and conditionally show the Navigation component in the App component
App.js
<template>
<Navigation v-if="isAuthenticated" />
<router-view />
<Footer v-if="isAuthenticated" />
</template>
...
computed: {
isAuthenticated() {
return this.$store.getters.isAuthenticated
}
The limitation here is that if later you want to disable the navigation for routes where the user can authenticated (i.e. error 404 route). This won't work anymore.
I recommend you to check out this SO question for more info

Vue props undefined on component

I am struggling with passing props to my child component and reading through many many examples, there are quiet a few in my position. Seems this shouldn't be so complicated, right?
Ideally, when I drop my component on a html page I want to be able to pass a url as an attribute. Example
<landingpage myUrl="http://localhost"><landingpage> but when I inspect with the Vue Dev Tools in browser, it is always undefined. I've seen a hack using JQuery to select the element and then get the attribute but I would like to do it in pure Vue.
In my code below, no variation of "title" is passed to my component.
In my index.html page I have this
<body>
<p>Hello world, this is some text. Howdy.</p>
<div id="NewWidget">
<div id="app" data-title="mario" :data-title="luigi" :title="princess">
<landingpage title="hello!" :title="spaghetti" v-bind:title="Nervos"></landingpage>
</div>
</div>
<!-- built files will be auto injected -->
</body>
In my App.vue I have
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
And in my landingpage.vue I have this
export default {
name: 'landingpage',
data () {
return {
categories: [],
}
},
props: {
title: {
type: String
}
},
...
My router index.js
export default new Router({
routes: [
{
path: '/',
name: 'LandingPage',
component: LandingPage,
props: true
},
...
In my LandingPage component, this.title is always null/undefined.
I am using Vue 2.5.2 / Vue Router 3.0.1
Only thing I can think of is my VueRouter usage in App.vue is burning the props?

How to pass props to a vue component at initialization inside single file vue components (dependency injection in vue-loader)?

I'm building a TabbedDetailView reusable component in vue. The idea is that the tab-detail component receives a list of objects which have a title and a component. It then does the logic so that when you click on a tab, then the component is displayed. The problem is that this components have a prop that is a user_id. How do I insert this prop into the components from outside of the template (directly in the script)?
For example (using single file vue components with webpack):
TabDetail.vue
<template>
<div>
<nav class="tabs-nav">
<ul class="tabs-list">
<li class="tabs-item" v-for='tab in tabs'>
<a v-bind:class="{active: tab.isActive, disabled: !tab.enabled}" #click="switchTab(tab)">{{tab.title}}</a>
</li>
</ul>
</nav>
<div v-for='tab in tabs'>
<component :is="tab.detail" v-if='tab.isActive'></component>
</div>
</div>
</template>
<script>
export default {
name: 'NavigationTabs',
props: ['tabs'],
created: function() {
this.clearActive();
this.$set(this.tabs[0], 'isActive', true);
},
methods: {
clearActive: function() {
for (let tab of this.tabs) {
this.$set(tab, 'isActive', false);
}
}, switchTab: function(tab) {
if (tab.enabled) {
this.clearActive();
tab.isActive = true;
}
},
},
};
</script>
The idea is that this can be reused by only passing a props object with titles and components. eg. tabs = [{title: 'Example1', component: Component1}{title: 'Example2', component: Component2}] I want to be able to instantiate this components with props before passing them. eg. tabs = [{title: 'Example1', component: Component1({user_id: 5})}{title: 'Example2({user_id: 10})', component: Component2}]).
SomeComponent.vue
import Vue from 'vue';
import TabDetail from '#/components/TabDetail'
import Component1 from '#/components/Component1';
const Componenet1Constructor = Vue.extend(Component1);
export default {
data() {
return {
tabs: [
{title: 'Componenent 1', detail: new Component1Constructor({propsData: {user_id: this.user_id}})}
{title: 'Component 2', detail: Component2},
{title: 'Component 3', detail: Component3},
],
};
}, props: ['user_id'],
components: {'tab-detail': TabbedDetail},
}
<template>
<div>
<tab-detail :tabs='tabs'></tab-detail>
</div>
</template>
Component1.vue
export default {
props: ['user_id'],
};
<template>
<div>
{{ user_id }}
</div>
</template>
The approach above raises de error:
[Vue warn]: Failed to mount component: template or render function not defined.
I think this is a good idea because I'm trying to follow the dependency injection design pattern with components. Is there a better approach to this problem without using global state?
This is could be done via Inject Loader when using vue loader with single file vue components but it adds a lot of unnecessary complexity and it's mostly meant for testing. It seems like the preferred way of managing state is by using a global state management store like Vuex.

Navigating vuejs SPA via routes that share component does not refresh component data as expected

I have a couple routes in my vuejs SPA that I have set up using vue-router:
/create/feedback
/edit/feedback/66a0660662674061b84e8ea2fface0e4
The component for each route is the same form with a bit of smarts to change form values based on the absence or present of the ID in the route (feedbackID, in my example).
I notice that when I click from the edit route to the create route, the data in my form does not clear.
Below is the gist of my route file
import FeedbackFormView from './components/FeedbackForm.vue'
// Routes
const routes = [
{
path: '/create/feedback',
component: FeedbackFormView,
name: 'FeedbackCreate',
meta: {
description: 'Create Feedback',
}
},
{
path: '/edit/feedback/:feedbackId',
component: FeedbackFormView,
name: 'FeedbackEdit',
meta: {
description: 'Edit Feedback Form'
},
props: true
}
]
export default routes
Below is the gist of my component
<template lang="html">
<div>
<form>
<input v-model="model.someProperty">
</form>
</div>
</template>
<script>
export default {
data() => ({model: {someProperty:''}}),
props: ['feedbackId'],
created() => {
if (!this.$props['feedbackId']) {
return;
}
// otherwise do ajax call and populate model
// ... details omitted
}
}
</script>
However, if I modify my component as follows, everything works as expected
<template lang="html">
<div>
<form>
<input v-model="model.someProperty">
</form>
</div>
</template>
<script>
export default {
data() => ({model: {someProperty:''}}),
props: ['feedbackId'],
created() => {
if (!this.$props['feedbackId']) {
return;
}
// otherwise do ajax call and populate model
// ... details omitted
},
watch: {
'$route' (to, from) {
if (to.path === '/create/feedback') {
this.model = {}
}
}
}
}
</script>
Why is this? Why do I need watch?
I would have though that changing routes would be sufficient as the purpose of routing is to mimic the semantic behavior of page navigation
You have same component for different routes, when you go to edit route from the create route component is already created and mounted so the state of the component doesn't clear up.
Your component can listen to route changes using $router provided by vue-router every time the route changes the watcher is called.
For those who come this later, the following answer addresses the issue I was facing:
Vue-Router: view returning to login page after page refresh

vuejs application with different layouts (e.g. login layout, page layout, signup etc.)

I generated a project using vue-cli. I see project has one App.vue which is kinda main layout of the app - if I'm not mistaken. Here I put my basic HTML layout and <router-view></router-view>. Now the issue is that I need completely different layout for login (different wrappers , body has different classes) but I can't change it since App.vue has template which is kinda "fixed" as a layout. How to approach this issue? Is there recommended way?
Should I create new component that represents layout so in that case my App.vue template would only have <router-view></router-view> and then LoginLayout.vue would be included into it?
I think I found a solution. The approach has App.vue containing only <router-view></router-view> and then including different components that represent layout (if needed, containing <router-view> and subroutes). I found a project using it in that way here.
I think it keeps things more clean and organised. IMHO, hiding all elements which define layout structure (all the divs) would be too messy - especially for bigger apps.
A nice solution for this is using slots
First create your "layout component"
src/components/layouts/basic.vue
<template>
<div class="basic-layout">
<header>[Company logo]</header>
<hr>
<slot/>
<hr>
<footer>
Made with ❤ at Acme
</footer>
</div>
</template>
Then use it in another component:
<template>
<layout-basic>
<p>Hello world!</p>
</layout-basic>
</template>
<script>
import LayoutBasic from '#/components/layouts/basic'
export default {
components: {
LayoutBasic
}
}
</script>
"Hello world" will appear where the <slot/> tag is.
You can also have multiple slots with names, see the complete docs.
I find another solution by using router meta. I just have a few components need another layout.
I added a plainLayout meta key in src/router/index.js.
export default new Router({
mode: 'history',
linkExactActiveClass: 'app-head-menu--active',
routes: [
{
path: '/',
component: Features,
},
{
path: '/comics/:id',
component: Comic,
props: true,
},
{
path: '/comics/:comic_id/:chapter_index',
component: Chapter,
props: true,
meta: {
plainLayout: true,
},
},
],
});
Then render layout conditionally with playLayout in src/App.vue.
<template>
<div>
<div v-if="!$route.meta.plainLayout">
<div class="app-head">
</div>
<div class="app-content">
<router-view/>
</div>
</div>
<div v-if="$route.meta.plainLayout">
<router-view/>
</div>
</div>
</template>
<script>
export default {
name: 'app',
};
</script>
See a demo project here.
Utilizing Routes, and in particular, children routes is a great way to approach having common layouts in Vue.
All of this code is utilizing Vue 2.x
Start by having a really simple vue component called App that has no layout.
app.vue
<template>
<router-view></router-view>
</template>
Then have a Routes file that you'll bring into your Vue instance.
Routes.(ts|js)
import Vue from 'vue'
import VueRouter from 'vue-router'
const NotFoundComponent = () => import('./components/global/notfound.vue')
const Login = () => import('./components/account/login.vue')
const Catalog = () => import('./components/catalog/catalog.vue')
export default new VueRouter({
mode: 'history',
linkActiveClass: 'is-active',
routes: [
//Account
{ path: '/account', component: () => import('./components/account/layout.vue'),
children: [
{ path: '', component: Login },
{ path: 'login', component: Login, alias: '/login' },
{ path: 'logout',
beforeEnter (to: any, from: any, next: any) {
//do logout logic
next('/');
}
},
{ path: 'register', component: () => import('./components/account/register.vue') }
]
},
//Catalog (last because want NotFound to use catalog's layout)
{ path: '/', component: () => import('./components/catalog/layout.vue'),
children: [
{ path: '', component: Catalog },
{ path: 'catalog', component: Catalog },
{ path: 'category/:id', component: () => import('./components/catalog/category.vue') },
{ path: 'product', component: () => import('./components/catalog/product.vue') },
{ path: 'search', component: () => import(`./components/catalog/search.vue`)} ,
{ path: 'basket', component: () => import(`./components/catalog/basket.vue`)} ,
{ path: '*', component: NotFoundComponent }
]
}
]
})
The code is using lazy loading (with webpack) so don't let the () => import(...) throw you. It could have just been import(...) if you wanted eager loading.
The important bit is the children routes. So we set the main path of /account to utilize the /components/account/layout.vue but then the very first two children specify the main content vue (Login). I chose to do it this way because if someone just browses to /account I want to greet them with the login screen. It may be appropriate for your app that /account would be a landing page where they could check the order history, change passwords, etc...
I did the same thing for catalog... / and /catalog both load the catalog/layout with the /catalog/catalog file.
Also notice that if you don't like the idea of having "subfolders" (i.e. account/login instead of just /login) then you can have aliases as I show in the login.
By adding , alias: '/login' it means users can browse to /login even though the actual route is /account/login.
That is the key to the whole thing, but just to try and make the example complete...
Here is my boot file which hooks up my app.vue and routes:
boot.(ts|js)
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
import App from './components/app.vue';
import router from './routes';
new Vue({
el: '#app',
router,
render: h => h(App)
});
I created a layout.vue file for each of my main sections of my app (account, catalog, etc).
account/layout.vue
<template>
<div>
<cc-header></cc-header>
<div class="container">
<main>
<router-view></router-view>
</main>
<aside>
</aside>
</div>
<cc-footer></cc-footer>
</div>
</template>
<script lang="ts">
import ccHeader from "../common/cc-header.vue"
import ccFooter from "../common/cc-footer.vue"
export default {
components: {
ccHeader,
ccFooter
}
}
</script>
<style lang="scss" scoped>
.container {
display: flex;
}
main {
flex: 3;
order: 2;
}
aside {
flex: 1;
order: 1;
}
</style>
And the layout for catalog...
catalog/layout.vue
<template>
<div>
<cc-header></cc-header>
<div class="catalog-container">
<main class="catalog">
<router-view></router-view>
</main>
<cc-categories></cc-categories>
</div>
<cc-footer></cc-footer>
</div>
</template>
<script lang="ts">
import ccHeader from "../common/cc-header.vue"
import ccFooter from "../common/cc-footer.vue"
import ccCategories from "./cc-categories.vue"
export default {
components: {
ccCategories,
ccHeader,
ccFooter
},
data : function() : any {
return {
search: ''
}
},
}
</script>
<style lang="scss" scoped>
.catalog-container {
display: flex;
}
.category-nav {
flex: 1;
order: 1;
}
.catalog {
flex: 3;
order: 2;
}
</style>
Both layouts use common components like header and footer, but they don't need to. The catalog layout has categories in the side nav, while the account layout doesn't. I put my common components under components/common.
common/footer.vue
<template>
<div>
<hr />
<footer>
<div class="footer-copyright">
<div>© Copyright {{year}} GlobalCove Technologies, LLC</div>
<div>All rights reserved. Powered by CoveCommerce.</div>
</div>
</footer>
</div>
</template>
<script lang="ts">
import Vue from "vue";
export default Vue.component('cc-footer', {
data : function() : any {
return {
year: new Date().getFullYear()
}
},
})
</script>
<style lang="scss">
</style>
Overall file structure
src/
boot.ts
routes.ts
components/
app.vue
catalog/
layout.vue
catalog.vue
category.vue
product.vue
search.vue
basket.vue
account/
layout.vue
login.vue
register.vue
global/
notfound.vue
common/
cc-header.vue
cc-footer.vue
The combination of routes, a plain app.vue, and specific layout files, along with common components should get you to where you want to be.
I route my apps through a layout. Eg login requires no structure, just the login component, but other pages require, header footer etc, so here is an example of how I do this in my routes:
// application routes
'/secure': {
name: 'secure',
component: require('../components/layouts/default'),
subRoutes: {
'/home': {
name: 'home',
component: require('../components/home/index')
}
}
}
//- public routes
'/insecure': {
name: 'insecure',
component: require('../components/layouts/full-bleed'),
subRoutes: {
'/login': {
name: 'login',
component: require('../components/session/login')
}
}
}
Both of these layout templates have a router-view tag, so you can them build your layouts as you require for different parts of the app.
I dynamically check the route globally on App.vue and use that to determine what needs to be shown.
App.vue
<template>
<div id="app">
<top :show="show" v-if="show.header"></top>
<main>
<router-view></router-view>
</main>
<bottom v-if="show.footer"></bottom>
</div>
</template>
<script>
export default {
mounted: function() {
if(window.location.hash == "#/" || window.location.hash.indexOf('route')) {
vm.show.header = true
vm.show.footer = true
vm.show.slideNav = true
}
}
watch: {
$route: function() {
// Control the Nav when the route changes
if(window.location.hash == "#/" || window.location.hash.indexOf('route')) {
vm.show.header = true
vm.show.footer = true
vm.show.slideNav = true
}
}
}
}
</script>
That way I'm also able to control what's shown in the top and bottom navs through props.
Hope this helps!
I don't know about any "recommended way" but my app is structured like this:
App.vue - just top menu bar (which is not rendered when user is not authenticated) and <router-view></router-view> for each component (page)
So every page could have totally different layouts.
Comment to the accepted answer
Kind of disagree with this. Had the same issue and this answer confused me. Basically when you have a component which you'd like to reuse everywhere (e.g. footer, header) in your application then you can keep it in the App.vue. It was my case, I wanted to have footer and header in every page, finding this answer put me into the wrong direction, but you can do it and it does works, for example App.vue:
<template>
<div id="app">
<app-header />
<router-view />
<app-footer />
</div>
</template>
<script lang="ts">
// Imports related to Vue.js core.
import { Component, Vue } from "vue-property-decorator";
// Imports related with custom logic.
import FooterComponent from "#/components/Footer.vue";
import HeaderComponent from "#/components/Header.vue";
#Component({
components: {
"app-footer": FooterComponent,
"app-header": HeaderComponent
}
})
export default class App extends Vue {}
</script>
<style lang="scss" scoped>
</style>
Footer.vue (located in components/Footer.vue):
<template>
<div>
<footer>
<div>© {{ year }} MyCompany</div>
</footer>
</div>
</template>
<script lang="ts">
// Imports related to Vue.js core.
import { Component, Vue } from "vue-property-decorator";
#Component({})
export default class FooterComponent extends Vue {
public year = new Date().getFullYear();
}
</script>
<style lang="scss" scoped>
</style>
Header.vue (located in components/Header.vue):
<template>
<div>
<header>
<router-link to="/">Home</router-link>
<router-link to="/about">About</router-link>
<router-link to="/contact">Contact</router-link>
</header>
</div>
</template>
<script lang="ts">
// Imports related to Vue.js core.
import { Component, Vue } from "vue-property-decorator";
#Component({})
export default class HeaderComponent extends Vue {}
</script>
<style lang="scss" scoped>
</style>