Multiple dependant variables in Vue - vue.js

What is the simplest way to implement multiple changeable variables that depend on each other?
For example we have a price before discount which cannot change and we can:
apply the discount and the price after discount should update,
or change the price after discount and the discount should update accordingly.
I have came up with the following solution for this example: https://jsfiddle.net/2wh6cgq5/1/
Can it be done without having to create separate handlers for each #input event and applying the v-model directive?

One option is to use the v-model binding with computed properties so you can control the set logic.
new Vue({
el: '#app',
data: {
priceBeforeDiscount: 100,
discount: 50,
priceAfterDiscount: 50,
},
computed: {
priceAfterDiscountInput: {
get() {
return this.priceAfterDiscount;
},
set(val) {
this.priceAfterDiscount = val;
this.discount = this.priceBeforeDiscount - this.priceAfterDiscount;
}
},
discountInput: {
get() {
return this.discount;
},
set(val) {
this.discount = val;
this.priceAfterDiscount = this.priceBeforeDiscount - this.discount;
}
}
},
})
Another possibility is to use watchers on discount and priceAfterDiscount. It doesn't lead to an infinite loop in this case because the values reach an equilibrium and watchers only run if the value changes. I'd be cautious about using co-dependent watchers in general though.
new Vue({
el: '#app',
data: {
priceBeforeDiscount: 100,
discount: 50,
priceAfterDiscount: 50,
},
watch: {
discount() {
this.priceAfterDiscount = this.priceBeforeDiscount - this.discount;
},
priceAfterDiscount() {
this.discount = this.priceBeforeDiscount - this.priceAfterDiscount;
}
},
})
However, I don't really think there's an issue with your solution. If you aren't required to use the v-model directive (e.g. for vee-validate), I'd just convert it to v-bind and do the assignment in the input handler.

Related

Accessing data from array in rendered list in vue.js

I am trying to use a property from a rendered list which may change depending on if a checkbox is filled or not. However, mathSkill and scienceSkill always show 0. I feel like I'm doing something very wrong in trying to access booleanValue but I am not sure what else I could put in the if statement to allow it to update the values. Thank you in advance if you have any insight!
const app = new Vue({
el: '#app',
data: {
abilities: [
{ value: 'math', id: 'math', booleanValue:'no' },
{ value: 'science', id: 'science', booleanValue:'no'},
{ value: 'english', id: 'english', booleanValue:'no'},
],
// VARIABLES
mathSkill: 0,
scienceSkill: 0,
},
computed: {
addToMath: function() {
if (this.abilities[0] === 'yes' )
mathSkill = mathSkill +1,
scienceSkill = scienceSkill + 1;
}
}
I don't know exactly what you are trying to accomplish.
Don't define the variables, if you are going to calculate them.
Use .filter to make a new array based on some condition, and use .length to get how many objects in that array
computed: {
matchSkill() { return this.abilities.filter(ability => ability.booleanValue === "yes").length},
}
Example code

Unexpected side effect in "isExpiryComing" computed property

I tried to make the nearExpiry attribute to become TRUE if it is within the range of 30 days. But I hit an error which is Unexpected side effect in "isExpiryComing" computed property, is there any way I can overcome this?
I'm not sure how do I use slice at the isExpiryComing computed properties. Is there any workaround for this error?
<template>
<div class="container wrapper d-flex flex-column justify-content-center align-items-center">
<h1 class="text-info">Ingredients List</h1>
<ingredients-list class="justify-content-center"
v-for="(ingredient,index) in sortedItems"
:key="index"
:index='index'
:food-name="ingredient.food"
:food-expiry="ingredient.expiryDate"
:is-expiry="isExpiryComing.nearExpiry"></ingredients-list>
</div>
</template>
<script>
export default {
data() {
return {
ingredients: [
{
food: 'CARROT',
expiryDate: '2020-12-12',
nearExpiry: false
},
{
food: 'PAPAYA',
expiryDate: '2018-1-15',
nearExpiry: false
},
{
food: 'ORANGE',
expiryDate: '2021-10-13',
nearExpiry: false
},
{
food: 'CHICKEN',
expiryDate: '2019-4-23',
nearExpiry: false
},
{
food: 'MEAT',
expiryDate: '2021-5-23',
nearExpiry: false
},
],
}
},
computed: {
sortedItems() {
return this.ingredients.slice().sort((a, b) => {
return new Date(a.expiryDate) - new Date(b.expiryDate);
});
},
isExpiryComing() {
const now = new Date().getTime()
const expiryDate = new Date(this.expiryDate).getTime()
if (now - expiryDate > (30 * 24 * 60 * 60 * 1000)) {
this.nearExpiry = false
} else {
this.nearExpiry = true
}
return this.nearExpiry
}
},
}
</script>
It's because you're chagning another variable inside a computed property, this.nearExpiry - your use case doesn't need it.
I assume you want to check the expiration date for every product inside v-for.
I would suggest removing isExpiryComing entirely and changing the sortedItems to:
return this.ingredients
.slice()
.sort((a, b) => {
return new Date(a.expiryDate) - new Date(b.expiryDate)
})
.map((ingredient) => {
const now = new Date().getTime()
const expiryDate = new Date(ingredient.expiryDate).getTime()
return { ...ingredient, nearExpiry: now - expiryDate > 30 * 24 * 60 * 60 * 1000 }
})
Might be also a good idea to move const now = new Date().getTime() higher, to data().
A couple of issues I see with this:
isExpiryComing is scoped to your current component (like ingredients and sortedItems), not to the iteration of your v-for (like ingredient and index).
Setting to this.nearExpiry = true is likely what is causing the error. Computed properties should be pure-functions, i.e. they should not affect the state (normally this wouldn't be the case, as this should be scoped to the function with the method signature you used, but this is Vue so good luck with upholding that). If you want to affect state, you should use a watched property. (and since you are just returning this.nearExpiry, why not just make nearExpiry a local variable?).
To fix the error, just make nearExpiry a local variable instead of a vue-instance variable (this.nearExpiry). But, per my first point, that's probably not what you want either. Probably move the computed property to your ingredients-list component.

Why it is hard to use vue-i18n in vue data() (why it is not reactive)

I am using vue-i18n in a vue project. And I found it really confusing when using some data in vue data with i18n. Then if I change locale, that data is not reactive. I tried to return that data from another computed data but anyways it is not reactive because i18n is written in data. *My situation - * I want to show table with dropdown(list of columns with checkbox) above it. When user checks a column it will be showed in table if unchecks it won't. It is working fine until I change locale. After changing locale table columns is not translated but dropdown items is reactively translated and my code won't work anymore. Here is some code to explain better: In my myTable.vue component I use bootstrap-vue table -
template in myTable.vue
<vs-dropdown vs-custom-content vs-trigger-click>
<b-link href.prevent class="card-header-action btn-setting" style="font-size: 1.4em">
<i class="fa fa-th"></i>
</b-link>
<vs-dropdown-menu class="columns-dropdown">
<visible-columns :default-fields="columns" #result="columnListener"></visible-columns>
</vs-dropdown-menu>
</vs-dropdown>
<b-table class="generalTableClass table-responsive" :fields="computedFieldsForTable">custom content goes here</b-table>
script in myTable.vue
data(){
return {
fieldsForTable: [];
}
},
computed: {
computedFieldsForTable () {
return this.fieldsForTable;
},
columns() {
return [
{
key: 'id',
label: this.$t('id'),,
visible: true,
changeable: true
},
{
key: 'fullName',
label: this.$t('full-name'),,
visible: true,
changeable: true
},
{
key: 'email',
label: this.$t('email'),,
visible: true,
changeable: true
}
]
}
},
mounted () {
this.fieldsForTable = this.filterColumns(this.columns);
},
methods: {
filterColumns(columns = []) {
return columns.filter(column => {
if (column.visible) {
return column
}
})
},
columnListener ($event) {
this.fieldsForTable = this.filterColumns($event)
}
}
Can someone give me some advice for this situation ?
*EDIT AFTER SOME DEBUGGING: I think when filtering columns(in computed) and returning it for fieldsForTable inside filterColumns(columns) method, it actually returning array(of objects) with label='Label Name' not label=this.$t('labelName'). So after filtering the new array has nothing to do with vue-i18n. My last chance is reloading the page when locale changes.
Trying modify computedFieldsForTable as follows. You need to reference this.columns in computedFieldsForTable, so that Vue can detect the change of labels in this.columns.
computedFieldsForTable () {
return this.filterColumns(this.columns);
},
EDITED: put your this.columns in data. Then
columnListener ($event) {
this.columns = $event;
}
I hope i didn't misunderstand what you mean.
EDITED (again):
Maybe this is the last chance that I think it can work. Put columns in computed() still and remove computedFieldsForTable. Finally, just leave fieldsForTable and bind it on fields of <b-table>.
watch: {
columns(val) {
this.fieldsForTable = this.filterColumns(val)
}
},
method: {
columnListener ($event) {
this.fieldsForTable = this.filterColumns($event)
}
}
However, I think it is better and easier to reload page whenever local change. Especially when your columns have a more complex data structure.

Access component instance from vue watcher

I'm working on a project, similar as a bill manager, so I want that the subtotal get recalculated every time that quantity or unit value change, I have tried and searched to accomplish this using watcher or computed properties, but I don't find the right approach, cause I need to access the whole scope of the element when another change, like this.
Model structure:
detail
quantity
unit value
subtotal (should be a computed or updated)
So I think I should be able of doing something like this:
Vue.component('item', {
template: '#item',
props: {
item: Object,
},
computed:{
total: function(){
return this.quantity*this.unit_value;
}
},
watch:{
'item.quantity':()=>{
this.subtotal = this.quantity*this.unit_value;
}
}
});
I have several components being read from a list
I merged the approach using watcher and computed in the same code to make it shorter.
The problem is that I haven't found a way to access the hole element from inside itself, anyone could pls explain the right way? thanks
You shouldn't use arrows functions there, use method declarations.
If you want to watch for a property of the item object, you'll have to watch for the item object itself, and additionally use the deep: true flag of the watcher.
Final detail, you are using several properties that are not declared in your data. Declare them, otherwise they will not be reactive, that is, the computed will not recalculate when they change.
See code:
Vue.component('item', {
template: '#item',
props: {
item: Object,
},
data() {
return {
subtotal: null, // added data properties
quantity: null,
unit_value: null
}
},
computed: {
total: function() {
return this.quantity * this.unit_value;
}
},
watch: {
item: { // watching for item now
deep: true, // using deep: true
handler() { // and NOT using arrow functions
this.subtotal = this.quantity * this.unit_value;
}
}
}
});

Vue.js: Binding two input to each other

I'm currently starting on using vue.js, and have encountered a situation.
I wish to bind two inputs for example C = A - B. and B = A - C, where A is constant and a change in B or C will affect the other.
I successfully bind C using v-model and placing it in computed. However, when I tried the same for B, it ran into an infinite loop.
This should be something very simple however, I can't seem to find a solution to it.
Any help is appreciated thank you!
Edit: Code is included below. I wish for use the be able to key into either down_payment or loan_amount. After which it will calculate the other value automatically. However this way seems to make it go into a infinite loop
<input type="number" v-model="down_payment" class="form-control-right" placeholder="Downpayment" value="{{ down_payment }}" number>
<input type="number" v-model="loan_amount" placeholder="Loan Amount" value="{{loan_amount }}" number>
My javascript
new Vue({
el: '#calculator',
data: {
asking_price: 60000,
},
computed: {
loan_amount: function(){
return this.asking_price - this.downpayment;
},
down_payment : function(){
return this.asking_price - this.loan_amount;
},
}
});
You really have two independent variables and one that is computed but needs to be writable and handle the dependencies in its setter.
data: {
asking_price: 60000,
down_payment: 20
},
computed: {
loan_amount: {
get: function() {
return this.asking_price - this.down_payment;
},
set: function(newValue) {
this.down_payment = this.asking_price - newValue;
}
}
}
Fiddle
I'm still learning vue myself, but I'm wondering if it would be better to just define b and c as data properties and then use a watch to recompute one or the other when there's a change (instead of using computeds). Something like this:
var vm = new Vue({
el: '#calculator',
data: {
a: 10,
b: 0,
c: 10
},
watch: {
'b' : function(newVal, oldVal) {
this.c = this.a - newVal;
},
'c' : function(newVal, oldVal) {
this.b = this.a - newVal;
}
}
});