Watch props from children component vue.js - vue.js

I have two components "number-iput" and "basket input". How i can watch props from number-input (value) in basket-input?
Number Input component:
<template lang="pug">
.field
.number-input
button.number-input__btn(#click.prevent="minus")
i.i.i-minus
input(type="number" v-model="value" #input="valuecheck")
button.number-input__btn(#click.prevent="plus")
i.i.i-plus
</template>
<script>
export default {
name: "number-input",
props: {
value: {
type: Number,
default: 1
},
min: {
type: Number,
default: 1
},
max: {
type: Number,
default: 999999999
},
current: {
type: Number,
default: 1
}
},
methods: {
plus() {
if(this.value < this.max) this.value++;
},
minus() {
if(this.value > this.min) this.value--;
},
valuecheck() {
if(this.value > this.max) this.value = this.max
}
},
watch: {
value: function() {
if(parseInt(this.value) > parseInt(this.max)) this.value = this.max
}
}
}
</script>
Basket-input
<template lang="pug">
.basket-item
a.basket-item__image(href="")
img(:src="image", :alt="title")
.basket-item__info
span.basket-item__caption {{ code }}
a.basket-item__title(href="") {{ title }}
span.basket-item__instock(v-if="instock && instock > 0") В наличии ({{ instock }} шт)
span.basket-item__instock(v-else) Нет в наличии
.basket-item__numbers
number-input(min="1" max="99" :value="numberofitems")
.basket-item__lastcol
.column-price(v-if="price")
b {{ numberofitems * price }} ₽
span(v-if="numberofitems > 1") {{ numberofitems }} × {{ price }} ₽
button.basket-item__remove Удалить товар
</template>
<script>
export default {
name: 'basketitem',
props: {
image: {
type: String,
required: true
},
title: {
type: String,
required: true
},
code: {
type: String,
required: true
},
instock: {
type: Number
},
price: {
type: Number
},
numberofitems: {
type: Number,
default: 1
}
},
}
</script>
In basket input i need to watch number-input(value) and write it in numberofitems prop..
Im trying all, but my knowledge of vue is too low for that (

Props are mechanism to pass data only in one way - that means you can pass a value from parent to child but child is not allowed to change the value. If you do, Vue will warn you.
Way around it to use events. The principle is wildly know as "props-down, events-up".
You pass value to Child via prop
When Child want to change the value, instead of changing it directly, it will emit the event with new value
Parent component needs to handle that event and change its internal value (change will propagate into child item via prop)
You can read about various ways to do it for example here
Computed properties can help with that:
<template>
<input type="number" v-model="internalValue" />
</template>
<script>
export default {
props: {
value: {
type: Number,
default: 1
}
},
computed: {
internalValue: {
get: function() {
return this.value
},
set: function(newValue) {
this.$emit('valueChanged', newValue)
}
}
}
}
</script>
And in your parent component:
<template>
<mycomponent :value="value" #valueChanged="value = $event"/>
<mycomponent :value="value" #valueChanged="onValueChanged"/>
</template>
<script>
export default {
data: {
value: 0
},
methods: {
onValueChanged(newValue) {
this.value = newValue;
}
}
}
</script>
Notes
with larger and more complicated applications it can be better to use some solutions with shared global state like Vuex
Your Basket-input component has same problem because its receives numberofitems via prop

Related

Can't pass value prop to custom input component

I have a custom input component that generally works well. But since yesterday I want to pass to it a value from the parent component in certain cases (namely if a cookie is found for that field, pre-fill the field with the cookie value).
Parent component (simplified):
<custom-input
v-model="userEmail"
value="John Doe"
/>
But for a reason I cannot comprehend, the value prop doesn't work. Why not?
My custom input component (simplified):
<template>
<input
v-bind="$attrs"
:value="value"
#blur="handleBlur"
>
</template>
<script>
export default {
inheritAttrs: false,
props: {
value: {
type: String,
default: ''
}
},
mounted () {
console.log(this.value) // displays nothing, whereas it should display "John Doe"
},
methods: {
handleBlur (e) {
this.$emit('input', e.target.value)
}
}
}
</script>
value prop is used with the emitted event input to do the v-model job, so you should give your prop another name like defaultValue to avid this conflict:
<custom-input
v-model="userEmail"
defaultValue="John Doe"
/>
and
<template>
<input
v-bind="$attrs"
:value="value"
#blur="emitValue($event.target.vaklue)"
>
</template>
<script>
export default {
inheritAttrs: false,
props: {
value: {
type: String,
default: ''
},
defaultvalue: {
type: String,
default: ''
},
},
mounted () {
this.emitValue(this.defaultValue)
},
methods: {
emitValue(val) {
this.$emit('input', val)
}
}
}
</script>

Update child component value on axios response using v-model

Vue 3
I am trying to update the value of the data variable from the Axios response. If I print the value in the parent component it's getting printed and updates on the response but the variable's value is not updating in the child component.
What I am able to figure out is my child component is not receiving the updated values. But I don't know why is this happening.
input-field is a global component.
Vue 3
Parent Component
<template>
<input-field title="First Name" :validation="true" v-model="firstName.value" :validationMessage="firstName.validationMessage"></input-field>
</template>
<script>
export default {
data() {
return {
id: 0,
firstName: {
value: '',
validationMessage: '',
},
}
},
created() {
this.id = this.$route.params.id;
this.$http.get('/users/' + this.id).then(response => {
this.firstName.value = response.data.data.firstName;
}).catch(error => {
console.log(error);
});
},
}
</script>
Child Component
<template>
<div class="form-group">
<label :for="identifier">{{ title }}
<span class="text-danger" v-if="validation">*</span>
</label>
<input :id="identifier" :type="type" class="form-control" :class="validationMessageClass" :placeholder="title" v-model="inputValue">
<div class="invalid-feedback" v-if="validationMessage">{{ validationMessage }}</div>
</div>
</template>
<script>
export default {
props: {
title: {
type: String,
required: true,
},
validation: {
type: Boolean,
required: false,
default: false,
},
type: {
type: String,
required: false,
default: 'text',
},
validationMessage: {
type: String,
required: false,
default: '',
},
modelValue: {
required: false,
default: '',
}
},
emits: [
'update:modelValue'
],
data() {
return {
inputValue: this.modelValue,
}
},
computed: {
identifier() {
return this.title.toLowerCase().replace(/ /g, '-').replace(/[^\w-]+/g, '');
},
validationMessageClass() {
if (this.validationMessage) {
return 'is-invalid';
}
return false;
}
},
watch: {
inputValue() {
this.$emit('update:modelValue', this.inputValue);
},
},
}
</script>
The reason your child will never receive an update from your parent is because even if you change the firstName.value your child-component will not re-mount and realize that change.
It's bound to a property that it internally creates (inputValue) and keeps watching that and not the modelValue that's been passed from the parent.
Here's an example using your code and it does exactly what it's supposed to and how you would expect it to work.
It receives a value once (firstName.value), creates another property (inputValue) and emits that value when there's a change.
No matter how many times the parent changes the firstName.value property, the child doesn't care, it's not the property that the input v-model of the child looks at.
You can do this instead
<template>
<div class="form-group">
<label :for="identifier"
>{{ title }}
<span class="text-danger" v-if="validation">*</span>
</label>
<input
:id="identifier"
:type="type"
class="form-control"
:class="validationMessageClass"
:placeholder="title"
v-model="localValue" // here we bind localValue as v-model to the input
/>
<div class="invalid-feedback" v-if="validationMessage">
{{ validationMessage }}
</div>
</div>
</template>
<script>
export default {
... // your code
computed: {
localValue: {
get() {
return this.modelValue;
},
set(value) {
this.$emit("update:modelValue", value);
},
},
},
};
</script>
We remove the watchers and instead utilize a computed property which will return the modelValue in it's getter (so whenever the parent passes a new value we actually use that and not the localValue) and a setter that emits the update event to the parent.
Here's another codesandbox example illustrating the above solution.

Select first option by default using v-model

Can someone help with this problem? I have a dynamic select dropdown. The first default option of "Choose your location" is not visible by default. This is because of v-model. How can I get that working?
My parent component:
<template>
<div>
<Segments #select-segment-location-event="updateSegmentLocation" :segmentLocation="segmentLocation" />
</div>
</template>
My child component:
<template>
<div class="container">
<select v-model="segmentLocation" #change="selectSegmentLocation">
<option selected disabled>Choose your location...</option>
<option v-for="(segLoc, index) in segmentLocations"
:key="index"
:value="segLoc">{{ segLoc.name }}</option>
</select>
</div>
</template>
<script>
export default {
data() {
return {
segmentLocations: [
{ value: "Residential", name: 'Residential building' },
{ value: "Workplace", name: 'Workplace' },
{ value: "Hospitality", name: 'Hospitality or Retail' },
{ value: "Real Estate", name: 'Real Estate' },
{ value: "Commercial Parking", name: 'Commercial Parking' },
{ value: "Fleets", name: 'Fleets' },
{ value: "Cities & Governments", name: 'Cities & Governments' },
{ value: "Corridor", name: 'Highway, Corridor or Petrol Station' }
],
}
}
methods: {
selectSegmentLocation() {
this.$emit('select-segment-location-event', this.segmentLocation);
}
}
};
</script>
From what I see, the segmentLocation variable is passed as a prop to the Segments component.
You should avoid mutating a prop with v-model in the child component, because each component should only mutate it's own state.
I propose a few changes, add a new selectedSegmentLocation variable in your data method, with a default value of "NOT_SELECTED" :
data() {
return {
selectedSegmentLocation: "NOT_SELECTED",
segmentLocations: []
}
}
Then, in your template, update the Choose your location... option to have a value attribute:
<option selected value="NOT_SELECTED" disabled>Choose your location...</option>
Now, update the v-model and the selectSegmentLocation method to use the new variable:
<select v-model="selectedSegmentLocation" #change="selectSegmentLocation">
selectSegmentLocation() {
this.$emit('select-segment-location-event', this.selectedSegmentLocation);
}
If you still want to use the passed prop segmentLocation as a default value, use it in the created() hook, but you should have 'NOT_SELECTED' as the default value if you want to show the 'Choose your location' option.
created() {
this.selectedSegmentLocation = this.segmentLocation ? this.segmentLocation : "NOT_SELECTED";
}
And, if you want to be able to change the selected value from the parent, you can watch the segmentLocation property:
watch: {
segmentLocation() {
if (this.segmentLocation) {
this.selectedSegmentLocation = this.segmentLocation;
}
}
}

why vue v-for doesn't rerender child component when data change?

parent
export default {
name: "App",
components: {ChildComponent},
data: function() {
return {
itemList: []
};
},
methods: {
fetchData() {
callApi().then(res => { this.itemList= res; })
}
}
};
<template>
<ol>
<template v-for="(item) in itemList">
<li :key="item.id">
<child-component :item="item"></card>
</li>
</template>
</ol>
</template>
child
export default {
name: "ChildComponent",
props: {
item: { type: Object }
},
data: function() {
const {
name,
address,
.......
} = this.item;
return {
name,
address,
......
};
},
};
</script>
child get the item props which is an object.
I'm confused why the itemList point to another array, but the child doesn't update?
is it because key doesnt change? (but other data changed..)
thanks
It is because of this part of your code:
const {
name,
address,
} = this.item;
return {
name,
address,
};
What it does it copies name and address from item and return them.
It happens only once in your code while component is created.
If after that your prop item change, your data doesn't copy it and still returns the first values.
Solution:
If you don't change name or address in a child component, just use a prop
this.item.name in a script or {{ item.name }} in a template
It is already reactive so your component will update when prop changes.
Not sure what you do in your child component but this is enough to do and should react to your parent component changes. Basically the idea is to use the prop. I made some tweaks also.
export default {
name: "App",
components: {ChildComponent},
data: function() {
return {
itemList: []
};
},
methods: {
fetchData() {
callApi().then(res => { this.itemList= res; })
}
}
};
<template>
<ol>
<li v-for="(item) of itemList" :key="item.id">
<card :item="item"></card>
</li>
</ol>
</template>
export default {
name: "ChildComponent",
props: {
item: {
type: Object,
required: true
}
}
};
</script>
<template>
<div>
{{ item.name }} {{ item.address }}
</di>
</template>
It is because the data function is called only once on the creation of the component.
The recommended way to get data that depend on other data is to use computed.
export default {
name: "ChildComponent",
props: {
item: { type: Object }
},
computed: {
name() {
return this.item.name;
},
address() {
return this.item.address;
}
}
};
https://v2.vuejs.org/v2/guide/computed.html#Basic-Example

How to fire on change event of select while setting v-model?

I have Dropdown.vue and Sprint.vue use Dropdown.vue
Dropdown.vue code
<template>
<div>
<select
v-model="selected"
#input="$emit('input', $event.target.value)"
#change="emitChangeEvent"
>
<option
v-for="(option, idx) in options"
:key="`option-${idx}`"
:value="option.value"
>
{{ option.text }}
</option>
</select>
</div>
</template>
<script>
export default {
name: 'Dropdown',
props: {
options: {
type: Array,
default: () => [],
},
value: {
required: false,
},
},
computed: {
selected: {
get() {
return this.value;
},
set(newValue) {},
},
},
methods: {
emitChangeEvent() {
this.$emit('change', this.selected);
},
},
};
</script>
In Sprint.vue
<template>
<div>
<Dropdown
v-model="sprint"
:options="sprints"
#change="sprintChanged"
/>
{{ sprint }}
<button #click="change">
Change Sprint
</button>
</div>
</template>
<script lang="ts">
import {
Component, Prop, Vue, Emit,
} from 'vue-property-decorator';
import Dropdown from '#/components/Dropdown.vue';
#Component({
components: {
Dropdown,
},
})
export default class Sprint extends Vue {
private sprint = {};
private sprints = [
{ text: 'Sprint A', value: 'A' },
{ text: 'Sprint B', value: 'B' },
{ text: 'Sprint C', value: 'C' },
];
change() {
this.sprint = 'B';
}
sprintChanged(value: any) {
console.log(`value=${value}, sprint=${this.sprint}`);
}
}
</script>
When we select an option, an event was fired. But when I click on the button to set selected option by programming, there is no event of select is fired.
What am I missing?
As far as I can see, there are two main problems in your Dropdown.vue component.
1) You are binding the v-model of the element to the computed property selected, and emitting selected in your emitChangeEvent
2) and the selected computed properties doesn't have a setter so nothing happens when the value change.
To check if this is the only problem, a quick solution is to change your emitChangeEvent to this:
emitChangeEvent(event) {
this.$emit('change', event.target.value);
}
Emitting directly the value from event will skip any problem in the definition of the computed property.