How to send data to parent component using v-slot in vue - vue.js

I am trying to use slot-scopes in my component but I am not really successful on these. So in general what I am trying to do is I have a parent component and as a slot I am sending another component (child-component). In child component there is a button which changes the boolean.
So child component is:
<button #click="changeEditMode">Change edit mode</c-button>
methods: {
changeEditMode() {
this.$emit('edit-mode', true);
},
}
Parent component:
<div>
<slot :edit-mode="editMode"></slot>
</div>
props: {
editMode: {
type: Boolean,
required: true,
},
},
And here where both of them exist:
<parent-component>
<template v-slot="scope">
{{ scope.editMode }}
<child-component
#edit-mode="scope.editMode"
/>
</template>
</parent-component>
data() {
return {
editMode: false,
};
},
So I am expecting the value of {{ scope.editMode }} will change when I click the button but nothing changes. So where I am doing wrong?

If you simply want to update the editMode variable in your third component then you don't need to use scoped slot. Simply use named slots and you can update the data like this-
Parent component-
Give your slot a name "scope" where you are planning to inject the data.
<div>
<slot name="scope"></slot>
</div>
Another component-
Put your child component inside the named slot of the parent and simply listen to the emitted event from the child and update your editMode variable.
<parent-component>
<template #scope>
{{ editMode }}
<child-component
#edit-mode="editMode = $event"
/>
</template>
</parent-component>
data() {
return {
editMode: false,
};
},
Now, as in the question you are passing data to the slots (which I feels less required according to your use case), you can do it like this-
Parent component-
<div>
<slot :editMode="editMode"></slot>
</div>
data() {
return {
editMode: false,
}
}
Another component-
<parent-component>
<template #scope="scopeProps">
{{ scopeProps }}
</template>
</parent-component>
To know more about the named slots and pass data to named slots, read here- https://vuejs.org/guide/components/slots.html#named-slots

Related

Vue: All components rerender upon unrelated data property change only if a prop comes from a method that returns an object or array

(Vue 3, options API)
The problem: Components rerender when they shouldn't.
The situation:
Components are called with a prop whose value comes from a method.
The method cannot be replaced with a computed property because we must make operations on the specific item (in a v-for) that will send the value processed for that component.
The method returns an Array. If it returned a primitive such as a String, components wouldn't rerender.
To reproduce: change any parent's data property unrelated to the components (such as showMenu in the example below).
Parent
<template>
<div>
<div id="menu">
<div #click="showMenu = !showMenu">Click Me</div>
<div v-if="showMenu">
Open Console: A change in a property shouldn't rerender child components if they are not within the props. But it does because we call myMethod(chart) within the v-for, and that method returns an array/object.
</div>
</div>
<div v-for="(chart, index) in items" :key="index">
<MyComponent :table="myMethod(chart)" :title="chart.title" />
</div>
</div>
</template>
<script>
import MyComponent from './MyComponent.vue';
export default {
components: {
MyComponent,
},
data: function () {
return {
showMenu: false,
items: [{ value: 1 }, { value: 2 }],
};
},
methods: {
myMethod(item) {
// Remove [brackets] and it doesn't rerender all children
return ['processed' + item.value];
}
}
};
</script>
Child
<template>
<div class="myComponent">
{{ table }}
</div>
</template>
<script>
export default {
props: ['table'],
beforeUpdate() {
console.log('I have been rerendered');
},
};
</script>
<style>
.myComponent {
width: 10em;
height: 4em;
border: solid 2px darkblue;
}
</style>
Here's a Stackblitz that reproduces it https://stackblitz.com/edit/r3gg3v-ocvbkh?file=src/MyComponent.vue
I need components not to rerender. And I don't see why they do.
Thank you!
To avoid this unnecessary rerendering which is the default behavior try to use v-memo directive to rerender the child component unless the items property changes :
<div v-for="(chart, index) in items" :key="index" v-memo="[items]">
<MyComponent :table="myMethod(chart)" :title="chart.title" />
</div>

Calling function inside child component without an event?

Currently trying to use a method belonging to the parent
<p class="message-date text-center">
{{ $emit('format_date_day_month_year_time', message.date) }}
</p>
However I am getting the error.
Converting circular structure to JSON
--> starting at object with constructor 'Object'
How can I call a function inside a child component that does not rely on an event? I apologize for asking such a simple question but everything I was able to find on google is using $emit and using an event.
$emit was designed to only trigger an event on the current instance of vue. Therefore, it is not possible to receive data from another component this way.
For your case, I would suggest to use Mixins especially if you need to use certain functions among multiple vue components.
Alternately, let the child component call the the parent through $emit then receive the result from the parent through a prop.
Your code could be something as follows:
Child component
<template>
<p class="message-date text-center">
{{ date }}
</p>
</template>
<script>
export default {
name: 'Child',
props: {
date: String,
},
mounted() {
this.$emit("format-date", message.date);
},
}
</script>
Parent component
<template>
<Child :date="childDate" #format-date="formatChildDate" />
</template>
<script>
import Child from '#/components/Child';
export default {
components: {
Child,
},
data: () => ({
childDate: '',
}),
methods: {
formatChildDate(date) {
this.childDate = this.formatDateDayMonthYearTime(date)
},
formatDateDayMonthYearTime(date) {
//return the formatted date
},
},
}
</script>
with $emit you call a function where the Parent can listento.
where you are using it i would suggest a computed prop of the function.
But back to your Question here is a example of emiting and listen.
//Parent
<template>
<MyComponent #childFunction="doSomethingInParent($event)"/>
</template>
//Child
<template>
<button #click="emitStuff">
</template>
.....
methods:{
emitStuff(){
this.$emit(childFunction, somedata)
}
with the event you can give Data informations to a Parentcomponent.

VueJS2: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders

I'm creating very simple Popup Modal Using Vuejs2 and TailwindCss. However i encounter the error like below, when i'm tring to click on Button..
Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders.
In Parent Component
// CardModal
<template>
<div class="bg-white">
<div v-if="showing">
Modal
</div>
</div>
</template>
<script>
export default {
data() {
return {
showing: false,
}
}
}
</script>
Child Components
<button #click="showing = true" class="px-4 my-4 mx-3 bg-blue-400 py-1 font-bold rounded text-white">
Add Product
</button>
<!-- Modal -->
<cardModal :showing="showing" />
// Script
props: {
showing: {
// required: true,
type: Boolean,
}
},
Thanks in advance...
It's hard to understand your code but you can't change value of prop in your child component directly instead you can emit an event to your parent which change the value of prop for you.
e.g your child component which has
<template>
#click="$emit('show',true)"
</template>
//
props: {
showing: {
// required: true,
type: Boolean,
}
}
your parent
<cardModal :showing="showing" #show="showing=$event" />

How to use v-model and props passed to parent from child?

I've been learning vue for a few days now and I'm trying out passing data/props between child and parent.
Now I have the following child:
<template>
<div>
<input v-model="name1" placeholder="string">
<input v-model="number1" placeholder="number">
<p v-text="name1"></p>
<p v-text="number1"></p>
</div>
</template>
<script>
export default {
name: "child",
props: {
name1 : String,
number1 : Number
}
}
</script>
And then parent:
<template>
<div>
<child/>
</div>
</template>
<script>
import child from "#/components/complexComponent4/child.vue"
export default{
name: "parent",
components: {
child
}
}
</script>
Now when I enter some text into the input fields, it displays correctly in the paragraphs, since the props bound to the paragraphs have changed.
However, I get this warning:
[Vue warn]: 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: "name1"
found in
---> <Child>
<Parent> at src/components/complexComponent4/parent.vue
<MyComplexView4.vue> at src/views/myComplexView4.vue
<App> at src/App.vue
<Root>
I read about this error in multiple places on the internet and also in the documentation, and I found that mutating props is deemed an anti-pattern now:
https://michaelnthiessen.com/avoid-mutating-prop-directly
Unfortunately I didn't really find anything specific and/or helpful on how to deal with this problem. Especially in context of vue handling primitive data and objects/arrays differently (objects/arrays are passed by reference).
v-model seems to play an important role in leveraging the power of vue, since it enables two-way binds. Therefore I wouldn't want to omit it entirely, unless its use has become so difficult that it doesnt justify the gains.
As the warning says, you should avoid mutating props directly in child component.
So you should emit an event from child to parent to let parent know that prop value had been changed. Parent will change the prop and pass it down to child.
For such purpose there is a syntactic sugar in Vue called .sync modifier.
So your components could be something like this.
Child:
<template>
<div>
<input
:value="name1"
#change="$emit('update:name1', $event.target.value)"
placeholder="string"
/>
<input
:value="number1"
#change="$emit('update:number1', $event.target.value)"
placeholder="number"
/>
<p v-text="name1"></p>
<p v-text="number1"></p>
</div>
</template>
<script>
export default {
name: "child",
props: {
name1 : String,
number1 : Number
}
}
</script>
And parent:
<template>
<div>
<child :name1.sync="name1" :number1.sync="number1"/>
</div>
</template>
<script>
import child from "#/components/complexComponent4/child.vue"
export default{
name: "parent",
components: {
child
},
data() {
return {
name1: '',
number1: ''
}
}
}
</script>
Or for more complicated cases you can either use v-model and computed properties with setters in child component:
<template>
<div>
<input
v-model="computedName1"
placeholder="string"
/>
<input
v-model="computedNumber1"
placeholder="number"
/>
<p v-text="name1"></p>
<p v-text="number1"></p>
</div>
</template>
<script>
export default {
name: "child",
props: {
name1 : String,
number1 : Number
},
computed: {
computedName1: {
get() { return this.name1 },
set(value) {
// some logic
this.$emit('update:name1', value)
},
computedNumber1: {
get() { return this.number1 },
set(value) {
// some logic
this.$emit('update:number1', value)
}
}
}
}
</script>
If you intend to change prop passed down to child assign it first to child data.
<template>
<div>
<input v-model="name" placeholder="string">
<input v-model="number" placeholder="number">
<p v-text="name"></p>
<p v-text="number"></p>
</div>
</template>
<script>
export default {
name: "child",
data() {
return {
name: null,
number: null
}
},
props: {
name1 : String,
number1 : Number
},
mounted() {
this.name = this.name1;
this.number = this.number1;
}
}
</script>
When data is changed you can $emit those changes to parent component
With sync
Parent
<child :number1.sync="number1" :name1.sync="name1" />
Child
watch: {
name: value => this.$emit('update:name1', value)
number : value => this.$emit('update:number1', value)
},
With events
Parent
<child :number1="number1" :name1="name1" #changeNumber="value => number1 = value" #changeName="value => name1 = value" />
Child
watch: {
name: value => this.$emit('changeName', value)
number : value => this.$emit('updateNumber', value)
},
A guideline for vue.js is that you can use props to automatically alter data in the child from the parent, but not vice-versa. For altering data of the parent-component, the child-component is supposed to use events. You could consider using two different components for name1 and number1 respectively and bind the values in a two-way-manner by making these components applicable for v-model, as it is described here.

Extending Vue.js SFC components

I haven't found a good resource on extending Vue.js components. In every project I've worked on, regardless of the UI component library that's used, there are application Base components which extend the UI library components to enforce company/application defaults and standards.
I'm trying to extend Vue-Multiselect: https://vue-multiselect.js.org/ which has about 30 props and 12 slots. The component I'm extending doesn't matter -- I only mention it because ideally I don't want to have to repeat 30 props and 12 slots in my implementation.
I simply want to make two changes to the behavior of the component:
Make disabled prop a bit smarter
The Vue-Multiselect component has a standard disabled prop which works as expected:
<Multiselect :disabled="isDisabled" ...>
In our application, we have global state in Vuex which determines if the application is read-only. What I want to avoid is requiring developers to pass this state to every form field:
<Multiselect :disabled="readOnly || isDisabled" ...>
<OtherComponent :disabled="readOnly || someOtherCondition" ...>
...
So the user of my base component should only need to be concerned about their local UI state which affect the disabled status:
<BaseCombo :disabled="!emailValid" ...>
This would handle the 90% case of form fields that are locked down when the application is read-only and I can use an additional prop for cases where we want to ignore the global read-only status.
<BaseCombo :disabled="!emailValid" :ignoreReadOnly="true" ...>
Provide defaults
Secondly, I simply want to override some of the default prop values. This post addresses the question of supplying defaults:
https://stackoverflow.com/a/52592047/695318
And this works perfectly until I tried to modify the behavior of the disabled prop I mentioned previously.
My attempt to solve this was to either wrap or extend the component. I'd really want to avoid redeclaring all of the props if possible.
<template>
<Multiselect
:disabled="myCustomDisabled"
:value="value"
#input="$emit('input', $event)"
:options="options"
:label="label"
:track-by="trackBy"
:placeholder="placeholder"
... repeat for all 30 options
<script>
import Multiselect from 'vue-multiselect'
export default {
name: "BaseCombo",
extends: Multiselect, // extend or simply wrap?
computed: {
myCustomDisabled() {
this.props.disabled || ... use disabled from Vuex state
}
},
props: {
disabled: Boolean,
placeholder: {
type: String,
default: 'My Default Value',
},
... repeat for all props
The problem I ran into is I don't know how to handle the slots. The user of this BaseCombo should still be able to use all 12 slots in the VueMultiselect component.
Is there a better solution for extending components?
You can use this.$props to access props defined in the props attribute. Similarly you can access attributes (things you haven't defined as props) with this.$attrs. Finally you can bind props with v-bind="someVariable".
If you combine this you can do something like this:
<!-- App.vue -->
<template>
<component-a msg="Hello world" :fancy="{ test: 1 }" />
</template>
<!-- ComponentA.vue -->
<template>
<component-b v-bind="$attrs" />
</template>
<script>
export default {
name: 'componentA'
}
</script>
<!-- ComponentB.vue -->
<template>
<div>
{{ msg }}
{{ fancy }}
</div>
</template>
<script>
export default {
props: {
msg: String,
fancy: Object
},
mounted () {
console.log(this.$props);
}
}
</script>
In this example, component B would be the component you try to extend.
Here's a complete example based on Sumurai8's answer and motia's comments.
<template>
<Multiselect v-bind="childProps" v-on="$listeners">
<slot v-for="(_, name) in $slots" :name="name" :slot="name" />
<template v-for="(_, name) in $scopedSlots" :slot="name" slot-scope="slotData">
<slot :name="name" v-bind="slotData" />
</template>
</Multiselect>
</template>
<script>
import Multiselect from 'vue-multiselect'
export default {
name: "BaseCombo",
props: {
placeholder: {
type: String,
default: 'This is my default',
},
disabled: {
type: Boolean,
default: false,
},
},
components: {
Multiselect,
},
computed: {
childProps() {
return { ...this.$props, ...this.$attrs, disabled: this.isDisabled };
},
appReadOnly() {
return this.$store.state.appReadOnly;
},
isDisabled() {
return this.disabled || this.appReadOnly;
}
},
}
</script>