Vue 3: Emit event from child to parent - javascript

I've recently started to create an app with Vue 3 and I'm struggling with authentication when trying to emit an event from child to parent. Here's a sample of my code:
Child
<template>
<form class="msform">
<input #click="goToLogin" type="button" name="next" value="Login" />
</form>
</template>
<script>
import Cookies from "js-cookie";
export default {
emits:['event-login'],
methods: {
goToLogin() {
this.$emit("event-login");
},
},
};
</script>
Parent
<template>
<login v-if='loggedIn' #event-login='logIn'/>
<div class="my-page">
<router-view/>
</div>
</template>
<script>
import Cookies from "js-cookie";
import Login from '../pages/Login'
export default {
name: "MainLayout",
components:{
"login":Login
},
data() {
return {
loggedIn: false,
};
},
methods: {
logIn() {
this.loggedIn = true;
}
}
}
I don't know exactly why the event is not handled in the parent, can I get some help, please?

You're listening for the emitted event on a separate instance of login.
router-view is still just a component like any other, so try moving the handler there instead:
<template>
<login v-if='loggedIn' />
<div class="my-page">
<router-view #event-login='logIn' />
</div>
</template>

Related

Update props in component through router-view in vue js

I want to pass a boolean value from one of my views in a router-view up to the root App.vue and then on to one of the components in the App.vue. I was able to do it but I am facing an error:
Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "drawer"
Below is my code:
Home.vue:
<template>
<div class="home" v-on:click="updateDrawer">
<img src="...">
</div>
</template>
<script>
export default {
name: "Home",
methods:{
updateDrawer:function(){
this.$emit('updateDrawer', true)
}
};
</script>
The above view is in the router-view and I am getting the value in the App.vue below:
<template>
<v-app class="">
<Navbar v-bind:drawer="drawer" />
<v-main class=" main-bg">
<main class="">
<router-view v-on:updateDrawer="changeDrawer($event)"></router-view>
</main>
</v-main>
</v-app>
</template>
<script>
import Navbar from '#/components/Navbar'
export default {
name: 'App',
components: {Navbar},
data() {
return {
drawer: false
}
},
methods:{
changeDrawer:function(drawz){
this.drawer = drawz;
}
},
};
</script>
I am sending the value of drawer by binding it in the navbar component.
Navbar.vue:
<template>
<nav>
<v-app-bar app fixed class="white">
<v-app-bar-nav-icon
class="black--text"
#click="drawer = !drawer"
></v-app-bar-nav-icon>
</v-app-bar>
<v-navigation-drawer
temporary
v-model="drawer"
>
...
</v-navigation-drawer>
</nav>
</template>
<script>
export default {
props:{
drawer:{
type: Boolean
}
},
};
</script>
This will work one time and then it will give me the above error. I appreciate if any one can explain what should I do and how to resolve this issue.
In Navbar.vue you are taking the property "drawer" and whenever someone clicks on "v-app-bar-nav-icon" you change drawer to be !drawer.
The problem here is that you are mutating (changing the value) of a property from the child side (Navbar.vue is the child).
That's not how Vue works, props are only used to pass data down from parent to child (App.vue is parent and Navbar.vue is child) and never the other way around.
The right approach here would be: every time, in Navbar.vue, that you want to change the value of "drawer" you should emit an event just like you do in Home.vue.
Then you can listen for that event in App.vue and change the drawer variable accordingly.
Example:
Navbar.vue
<v-app-bar-nav-icon
class="black--text"
#click="$emit('changedrawer', !drawer)"
></v-app-bar-nav-icon>
App.vue
<Navbar v-bind:drawer="drawer" #changedrawer="changeDrawer"/>
I originally missed the fact that you also used v-model="drawer" in Navbar.vue.
v-model also changes the value of drawer, which again is something we don't want.
We can solve this by splitting up the v-model into v-on:input and :value like so:
<v-navigation-drawer
temporary
:value="drawer"
#input="val => $emit('changedrawer', val)"
>
Some sort of central state management such as Vuex would also be great in this scenario ;)
What you could do, is to watch for changes on the drawer prop in Navbar.vue, like so:
<template>
<nav>
<v-app-bar app fixed class="white">
<v-app-bar-nav-icon
class="black--text"
#click="switchDrawer"
/>
</v-app-bar>
<v-navigation-drawer
temporary
v-model="navbarDrawer"
>
...
</v-navigation-drawer>
</nav>
</template>
<script>
export default {
data () {
return {
navbarDrawer: false
}
},
props: {
drawer: {
type: Boolean
}
},
watch: {
drawer (val) {
this.navbarDrawer = val
}
},
methods: {
switchDrawer () {
this.navbarDrawer = !this.navbarDrawer
}
}
}
</script>
Home.vue
<template>
<div class="home">
<v-btn #click="updateDrawer">Updrate drawer</v-btn>
</div>
</template>
<script>
export default {
name: 'Home',
data () {
return {
homeDrawer: false
}
},
methods: {
updateDrawer: function () {
this.homeDrawer = !this.homeDrawer
this.$emit('updateDrawer', this.homeDrawer)
}
}
}
</script>
App.vue
<template>
<v-app class="">
<Navbar :drawer="drawer" />
<v-main class="main-bg">
<main class="">
<router-view #updateDrawer="changeDrawer"></router-view>
</main>
</v-main>
</v-app>
</template>
<script>
import Navbar from '#/components/Navbar'
export default {
name: 'App',
components: { Navbar },
data () {
return {
drawer: false
}
},
methods: {
changeDrawer (newDrawer) {
this.drawer = newDrawer
}
}
}
</script>
This doesn't mutate the drawer prop, but does respond to changes.

Vue / Nuxt access function from global component

I have a globally registered component (modal) in my NUXT app default layout which I would like to be able to trigger a method from any other component on a click event. I had it initially setup by importing the component into each page but I would rather this be a global component and not have to import explicitly on each page and just have one register call in the default layout. Is this achievable my using $root.$emit? or should I look into a vue store?
Default Layout:
<template>
<div class="layout-wrapper">
<Nuxt />
<Modal />
</div>
</template>
<script>
import Modal from "~/components/Modal.vue";
export default {
name: "DefaultLayout",
components: {
Modal
}
};
</script>
Modal:
<template>
<div>
<el-dialog :visible.sync="modal">
<div class="dialog-content">
<p>
Modal Content
</p>
</div>
</el-dialog>
</div>
</template>
<script>
import { Dialog } from "element-ui";
export default {
name: "Modal",
components: {
"el-dialog": Dialog
},
data() {
return {
modalOne: false
};
},
methods: {
openModal() {
this.modalOne = true;
}
}
};
</script>
other component to call method from:
Open
Using a store is a a good solution for your need:
1/ create a store file store/index.js
export const state = () => ({
modal: false
});
export const mutations = {
SET_MODAL (state, value) {
state.modal = value
}
};
2/ update your layout to set store data to the modal component:
<template>
<div class="layout-wrapper">
<Nuxt />
<Modal open="$store.state.modal" />
</div>
</template>
3/ update your component to read the open value:
<template>
<div>
<el-dialog :visible.sync="open">
<div class="dialog-content">
<p>
Modal Content
</p>
</div>
</el-dialog>
</div>
</template>
<script>
import { Dialog } from "element-ui";
export default {
props: {
open: Boolean // see https://v2.vuejs.org/v2/guide/components-props.html
},
name: "Modal",
components: {
"el-dialog": Dialog
}
};
</script>
4/ toggle the store value from a nuxt page (see https://nuxtjs.org/docs/2.x/directory-structure/store)
<script>
export default {
methods: {
openModal (e) {
this.$store.commit('SET_MODAL', true);
}
},
...
}
</script>

Vue 3: emit warning even though emits is present

I receive the following warning:
[Vue warn]: Extraneous non-emits event listeners (addData) were passed to component but could not be automatically inherited because component renders fragment or text root nodes. If the listener is intended to be a component custom event listener only, declare it using the "emits" option.
at <UserData onAddData=fn<bound dataSubmit> >
at <App>
in my Vue3 app. I use emits:["add-data"] in UserData.vue but the warning still appears.
Here are the relevant parts of the vue project:
app.vue
<template>
<div class="columns">
<div class="column">
<user-data #add-data="dataSubmit" />
</div>
<div class="column">
<active-user #delete-user="deleteUser" v-for="user in users" :key="user.id" :name="user.name" :age="user.age" :id="user.id" />
</div>
</div>
</template>
<script>
export default {
data() {
return {
users: []
}
},
methods: {
dataSubmit(name, age) {},
deleteUser(id) {}
}
}
</script>
UserData.vue
<template>
<h2>Add new user:</h2>
<form #submit.prevent="submitData">
<label>Name*</label>
<input type="text" v-model="name" placeholder="Name" />
<label>Age*</label>
<input type="text" v-model="age" placeholder="Age" />
<button>add</button>
</form>
</template>
<script>
export default {
emits: ["add-data"],
data() {
return {
name: "",
age: ""
}
},
methods: {
submitData() {
this.$emit("add-data", this.name, this.age)
}
}
}
</script>
main.js
import { createApp } from 'vue'
import UserData from './components/UserData.vue'
import ActiveUser from './components/ActiveUser.vue'
import App from './App.vue'
const app = createApp(App);
app.component("active-user", ActiveUser);
app.component("user-data", UserData);
app.mount('#app')
It works fine, but it just displays the warning.
If I change the emits part to emits: ["add-data", "addData"] the warning disappears.
There is an open bug for this:
https://github.com/vuejs/vue-next/issues/2540
For now you'll need to use emits: ['addData'] to avoid the warning.

Change a property's value in one component from within another component

I'm trying to wrap my head around hoe Vue.js works, reading lots of documents and tutorials and taking some pluralsight classes. I have a very basic website UI up and running. Here's the App.vue (which I'm using kinda as a master page).
(To make reading this easier and faster, look for this comment: This is the part you should pay attention to)...
<template>
<div id="app">
<div>
<div>
<CommandBar />
</div>
<div>
<Navigation />
</div>
</div>
<div id="lowerContent">
<!-- This is the part you should pay attention to -->
<template v-if="showLeftContent">
<div id="leftPane">
<div id="leftContent">
<router-view name="LeftSideBar"></router-view>
</div>
</div>
</template>
<!-- // This is the part you should pay attention to -->
<div id="mainPane">
<div id="mainContent">
<router-view name="MainContent"></router-view>
</div>
</div>
</div>
</div>
</template>
And then in the same App.vue file, here's the script portion
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import CommandBar from './components/CommandBar.vue';
import Navigation from './components/Navigation.vue';
#Component({
components: {
CommandBar,
Navigation,
}
})
export default class App extends Vue {
data() {
return {
showLeftContent: true // <--- This is the part you should pay attention to
}
}
}
</script>
Ok, so the idea is, one some pages I want to show a left sidebar, but on other pages I don't. That's why that div is wrapped in <template v-if="showLeftContent">.
Then with the named <router-view>'s I can control which components get loaded into them in the `router\index.ts\ file. The routes look like this:
{
path: '/home',
name: 'Home',
components: {
default: Home,
MainContent: Home, // load the Home compliment the main content
LeftSideBar: UserSearch // load the UserSearch component in the left side bar area
}
},
So far so good! But here's the kicker. Some pages won't have a left side bar, and on those pages, I want to change showLeftContent from true to false. That's the part I can't figure out.
Let's say we have a "Notes" component that looks like this.
<template>
<div class="notes">
Notes
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
#Component
export default class Notes extends Vue {
data() {
return {
showLeftContent: false // DOES NOT WORK
}
}
}
</script>
Obviously, I'm not handling showLeftContent properly here. It would seem as if the properties in data are scoped only to that component, which I understand. I'm just not finding anything on how I can set a data property in the App component and then change it in a child component when that child is loaded through a router-view.
Thanks!
EDIT:
I changed the script section of the Notes component from:
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
#Component
export default class Notes extends Vue {
data() {
return {
showLeftContent: false // DOES NOT WORK
}
}
}
</script>
to:
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
#Component
export default class Notes extends Vue {
mounted() {
this.$root.$data.showLeftContent = false;
}
}
</script>
And while that didn't cause any compile or runtime errors, it also didn't have the desired effect. On Notes, the left side bar still shows.
EDIT 2:
If I put an alert in the script section of the Notes component:
export default class Notes extends Vue {
mounted() {
alert(this.$root.$data.showLeftContent);
//this.$root.$data.showLeftContent = false;
}
}
The alert does not pop until I click on "Notes" in the navigation. But, the value is "undefined".
EDIT 3:
Struggling with the syntax here (keep in mind this is TypeScript, which I don't know very well!!)
Edit 4:
Inching along!
export default class App extends Vue {
data() {
return {
showLeftContent: true
}
}
leftContent(value: boolean) {
alert('clicked');
this.$root.$emit('left-content', value);
}
}
This does not result in any errors, but it also doesn't work. The event never gets fired. I'm going to try putting it in the Navigation component and see if that works.
As it says on #lukebearden answer you can use the emit event to pass true/false to the main App component on router-link click.
Assuming your Navigation component looks like below, you can do something like that:
#Navigation.vue
<template>
<div>
<router-link to="/home" #click.native="leftContent(true)">Home</router-link> -
<router-link to="/notes" #click.native="leftContent(false)">Notes</router-link>
</div>
</template>
<script>
export default {
methods: {
leftContent(value) {
this.$emit('left-content', value)
}
}
}
</script>
And in your main App you listen the emit on Navigation:
<template>
<div id="app">
<div>
<Navigation #left-content="leftContent" />
</div>
<div id="lowerContent">
<template v-if="showLeftContent">
//...
</template>
<div id="mainPane">
//...
</div>
</div>
</div>
</template>
<script>
//...
data() {
return {
showLeftContent: true
}
},
methods: {
leftContent(value) {
this.showLeftContent = value
}
}
};
</script>
A basic approach in a parent-child component relationship is to emit events from the child and then listen and handle that event in the parent component.
However, I'm not sure that approach works when working with the router-view. This person solved it by watching the $route attribute for changes. https://forum.vuejs.org/t/emitting-events-from-vue-router/10136/6
You might also want to look into creating a simple event bus using a vue instance, or using vuex.
If you'd like to access the data property (or props, options etc) of the root instance, you can use this.$root.$data. (Check Vue Guide: Handling Edge)
For your codes, you can change this.$root.$data.showLeftContent to true/false in the hook=mounted of other Components, then when Vue creates instances for those components, it will show/hide the left side panel relevantly.
Below is one demo:
Vue.config.productionTip = false
Vue.component('child', {
template: `<div :style="{'background-color':color}" style="padding: 10px">
Reach to root: <button #click="changeRootData()">Click me!</button>
<hr>
<slot></slot>
</div>`,
props: ['color'],
methods: {
changeRootData() {
this.$root.$data.testValue += ' :) '
}
}
})
new Vue({
el: '#app',
data() {
return {
testValue: 'Puss In Boots'
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<h2>{{testValue}}</h2>
<child color="red"><child color="gray"><child color="green"></child></child></child>
</div>

VueJS pass all props to child component

I am quite new to VueJS. In react you can easily use rest params for passing props to children. Is there a similar pattern in Vue?
Consider this parent component that has a few children components:
<template>
<header class="primary-nav">
<search-bar :config="searchBar"><search-bar>
// ^^^^ this becomes the key on the props for SearchBar
header>
</template>
export default {
data(){
return {
... a few components ...
searchBar : {
model: '',
name: 'search-bar',
placeholder: 'Search Away',
required:true,
another: true
andanother: true,
andonemoreanotherone:true,
thiscouldbehundredsofprops:true
}
}
}
}
<template>
<div :class="name">
<form action="/s" method="post">
<input type="checkbox">
<label :for="config.name" class="icon-search">{{config.label}}</label>
<text-input :config="computedTextInputProps"></text-input>
//^^^^ and so on. I don't like this config-dot property structure.
</form>
</div>
</template>
export default {
props: {
name: String,
required: Boolean,
placeholder: String,
model: String,
},
computed: {
computedtextInputProps(){
return justThePropsNeededForTheTextInput
}
}
...
What I don't like is that the props are named-spaced with the key config, or any other arbitrary key. The text-input component ( not shown ) is a glorified input field that can take a number of attributes. I could flatten the props when the component is created, but is that generally a good idea?
I am surprised this question hasn't been asked before.
Thanks.
Edit: 10-06-2017
Related: https://github.com/vuejs/vue/issues/4962
Parent component
<template>
<div id="app">
<child-component v-bind="propsToPass"></child-component>
</div>
</template>
<script>
import ChildComponent from './components/child-component/child-component'
export default {
name: 'app',
components: {
ChildComponent
},
data () {
return {
propsToPass: {
name: 'John',
last_name: 'Doe',
age: '29',
}
}
}
}
</script>
Child Component
<template>
<div>
<p>I am {{name}} {{last_name}} and i am {{age}} old</p>
<another-child v-bind="$props"></another-child> <!-- another child here and we pass all props -->
</div>
</template>
<script>
import AnotherChild from "../another-child/another-child";
export default {
components: {AnotherChild},
props: ['name', 'last_name', 'age'],
}
</script>
Another Child component inside the above component
<template>
<h1>I am saying it again: I am {{name}} {{last_name}} and i am {{age}} old</h1>
</template>
<script>
export default {
props: ['name', 'last_name', 'age']
}
</script>
Parent component
You can pass as many props as you want to child component
Child Component
Once you are satisfied with all the props, then you can use v-bind="$props" inside your child component to retrieve all the props.
Final Result:
Done:)
In Vue 2.6+, in addition to attributes passing,
events/listeners can be also passed to child components.
Parent component
<template>
<div>
<Button #click="onClick">Click here</Button>
</div>
</template>
<script>
import Button from "./Button.vue";
export default {
methods:{
onClick(evt) {
// handle click event here
}
}
}
</script>
Child component
Button.vue
<template>
<button v-bind="$attrs" v-on="$listeners">
<slot />
</button>
</template>
Update [2021-08-14]:
In Vue 3, $listeners will be removed. $attrs will have both listeners and attributes passing to the child component.
Child component
Button.vue
<template>
<button v-bind="$attrs">
<slot />
</button>
</template>
Migration Guide
In the child component, you can bind $attrs and $props.
<template>
<button v-bind="[$props,$attrs]" v-on="$listeners">
<slot />
</button>
</template>
v-bind="[$attrs, $props]"
This one works for me. $attrs doesn't include props.

Categories

Resources