Keeping track of an array of components - vue.js

I have a really simple Vue app:
<div id="app">
<item v-for="item in items" v-bind:title="item.title" v-bind:price="item.price"
#added="updateTotal(item)"></item>
<total v-bind:total="total"></total>
</div>
And a Vue instance:
Vue.component('item',{
'props' : ['title', 'price'],
'template' : "<div class='item'><div>{{ title }} – ${{total}} </div><button class='button' #click='add'>Add</button></div>",
'data' : function(){
return {
quantity : 0
}
},
'computed' : {
total : function(){
return (this.quantity * this.price).toFixed(2);
}
},
methods : {
add : function(){
this.quantity ++;
this.$emit('added');
}
}
});
Vue.component('total', {
'props' : ['total'],
'template' : "<div class='total'>Total: ${{ total }}</div>",
});
var app = new Vue({
'el' : '#app',
'data' : {
'total' : 0,
'items': [
{
'title': 'Item 1',
'price': 21
}, {
'title': 'Item 2',
'price': 7
}
],
},
methods : {
'updateTotal' : function(item){
console.log('updating');
this.total += item.price;
}
}
});
Demo link:
https://codepen.io/EightArmsHQ/pen/rmezQq?editors=1010
And what I'd like to do is update the <total> component as the various items are added to the cart. I have it working at the moment, however it doesn't seem very elegant.
Right now, I add the price of each item to a total. What I'd really like to do is have the total as a computed property, and then every time an item component is changed, loop through them all adding the quantity * price of each. Is there a way I can do this?
One option I have come up with just now is replacing my updateTotal method in the main app to the below:
methods : {
'updateTotal' : function(item){
item.quantity += 1;
}
},
computed : { total : function(){
var t = 0;
for(var i = 0; i < this.items.length; i ++){
t += this.items[i].quantity * this.items[i].price;
}
return t;
}
}
So, beginning to store the quantity of each item inside the Vue app, not the component. But it makes more sense to store the quantity of each item inside its own component... doesn't it? What is the best way of handling this?

Maybe counter-intuitively, the components only need their data as props. The items (as data objects) are defined in the parent; just define quantity there, too. Then use those data items in the components, but make changes via events to the parent.
With an array that includes the quantities, it's easy to create the computed total you want.
Vue.component('item', {
'props': ['item'],
'template': "<div class='item'><div>{{ item.title }} – ${{total}} </div><button class='button' #click='add'>Add</button></div>",
'computed': {
total: function() {
return (this.item.quantity * this.item.price).toFixed(2);
}
},
methods: {
add: function() {
this.$emit('added');
}
}
});
Vue.component('total', {
'props': ['total'],
'template': "<div class='total'>Total: ${{ total }}</div>",
});
var app = new Vue({
'el': '#app',
'data': {
'items': [{
'title': 'Item 1',
'price': 21,
'quantity': 0
}, {
'title': 'Item 2',
'price': 7,
'quantity': 0
}],
},
computed: {
total() {
return this.items.reduce((a, b) => a + (b.price * b.quantity), 0).toFixed(2);
}
},
methods: {
updateTotal(item) {
++item.quantity;
}
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.2.6/vue.min.js"></script>
<div id="app">
<item v-for="item in items" v-bind:item="item" #added="updateTotal(item)"></item>
<total v-bind:total="total"></total>
</div>

Related

How to add a Date Selector to Chartjs in the context of Vue.js

I am new to Vuejs, i have implemented chart using Chartjs, i want to add date selector to my chartjs. I have used 4 buttons, so whenever i click any of the button my chart should change accordingly.
Initially i tried implementing each button with each method, but i don't feel that as an effective way to do so.
> Here is my code: https://jsfiddle.net/95kspb70/3/
I want the chart to change when ever i click any of the button that i have used, i mean i have used 4 buttons, suppose say i clicked 3m, my chart should show previous 3months data only.
Please help me with this, I don't know how exactly i should do it in order to solve this.
You need to filter the data based on the date and rerender the chart for the new data.
Here is the working jsfiddle
I add click event #click="selectPeriod" inside html:
<div id='app'>
<div id="range-selector">
<input type="button" id="1m" #click="selectPeriod" class="period ui-button" value="1m" />
<input type="button" id="3m" #click="selectPeriod" class="period ui-button" value="3m" />
<input type="button" id="6m" #click="selectPeriod" class="period ui-button" value="6m" />
<input type="button" id="all" #click="selectPeriod" class="period ui-button" value="All" />
</div>
<canvas ref='chart' width='800' height='600'></canvas>
</div>
and following js code:
new Vue({
data: () => ({
date: [
1567295500.0,
1567893700.0,
1568615220.0,
1569024000.0,
1569888120.0,
1572456400.0,
1572772560.0,
1574809200.0,
1576706160.0,
1577718000.0,
1578610800.0,
1582650220.0,
1583174000.0,
1584063360.0,
1587427200.0,
1587573420.0,
1588637800.0,
1589587420.0,
1589989800.0
],
challenge: [
0.45,
2.12,
2.55,
3.15,
4.16,
5.56,
6.258,
7.256,
8.364,
9.154,
10.245,
11.654,
12.364,
13.785,
14,
15.32,
16.87,
17.852,
18.254,
19
],
data: []
}),
mounted() {
this.data = this.date.map((date, index) => ({
x: new Date(date * 1000),
y: this.challenge[index]
}))
this.buildGraph(this.data);
},
methods: {
buildGraph(data) {
console.log(data.length);
let ctx = this.$refs.chart.getContext('2d')
let chart = new Chart(ctx, {
type: 'line',
data: {
datasets: [{
data
}]
},
options: {
scales: {
xAxes: [{
type: 'time',
time: {
unit: 'month',
displayFormats: {
month: 'MMM YYYY'
}
}
}],
yAxes: [{
ticks: {
callback: function(value, index, values) {
return value + 'k';
}
}
}]
}
}
})
},
selectPeriod(event) {
let period = event.target.value;
let minValue = new Date(Math.max(...this.date) * 1000);
let axisXMin = new Date(Math.min(...this.date) * 1000);
switch (period) {
case "1m":
minValue.setMonth(minValue.getMonth() - 1);
break;
case "3m":
minValue.setMonth(minValue.getMonth() - 3);
break;
case "6m":
minValue.setMonth(minValue.getMonth() - 6);
break;
case "1y":
minValue.setYear(minValue.getFullYear() - 1);
break;
default:
minValue = axisXMin;
}
let data = this.data.filter(el => {
return el.x >= minValue
});
this.buildGraph(data);
}
}
}).$mount('#app')

I want to substitiute medicines on click of substitute button using Vue.js

Here is my code. I have uploaded some images so that you can get actual scenario of what I am asking.
And I also want to capture the index of each medicine I am substituting
substituteExistMedicine() {
const subMed = payload
const itemInCart = state.orderedMedicines.filter(item => item.id === subMed.activeId)[0];
const selectedSubstituteItem = state.medicineSubstitution[subMed.index];
const updatedSubstituteList = state.medicineSubstitution.map((substitute, i) => {
if (i === subMed.index) {
return {
...itemInCart
};
}
return {
...substitute
};
});
const updatedOrderedMedicines = state.orderedMedicines.map(item => {
if (item.id === subMed.activeId) {
return {
...selectedSubstituteItem
};
}
return {
...item
};
});
console.log('updatedOrderedMedicines', updatedOrderedMedicines)
this.medicineSubstitution = updatedSubstituteList;
},
This is first screen of medicine which has view substitute button
This is Second screen viewing medicine substitute
My actual scenario in which Im not able to substitute
Is a nicer solution available?
Note: First screen shows medicines and when click on VIEW SUBSTITUTE then see next screen of substitute results of that medicine.
When I click on SUBSTITUTE, it should replace medicines or I can say swap. which is not happening in my case.
Now this is my updated code I am able to substitute, but it's not reflecting in DOM; what should I do?
I think I understand what you want to do. I did a little JS Fiddle example of what I think you're trying to do:
Script:
new Vue({
el: "#app",
data: {
medicines: [
{
name: 'greatMedicine',
id: 1,
price: '100',
substitutes: [
{
name: 'notSoGreatButCheaper',
id: 3,
price: '20',
}
]
},
{
name: 'anotherGreatMedicine',
id: 2,
price: '150',
substitutes: [
{
name: 'alsoGreatButCheaper',
id: 4,
price: '30',
}
]
}
],
showSubsMed: '-1'
},
methods: {
substitute: function(med_index,sub_index){
let medicine = this.medicines[med_index];
let substitute = medicine.substitutes[sub_index];
this.medicines.splice(med_index, 1, substitute);
}
}
})
HTML:
<div id="app">
<h2>Medicines:</h2>
<ol>
<li v-for="(med,med_index) in medicines">
{{med.name}} - {{med.price}}€
<span v-if="med.substitutes">
<button #click="showSubsMed === med.id ? showSubsMed = -1 : showSubsMed = med.id" class="btn-primary">
<span v-if="showSubsMed === med.id">hide subs</span>
<span v-else>show subs</span>
</button>
<ol>
<li v-show="showSubsMed === med.id" v-for="(sub,sub_index) in med.substitutes" class="sub">
{{sub.name}} - {{sub.price}}€ <button #click="substitute(med_index,sub_index)" class="btn-primary">Substitute!</button>
</li>
</ol>
</span>
</li>
</ol>
</div>
https://jsfiddle.net/5mt0Loka/1/

ineffective computed loop in vuejs

I've got array as below:
return {
items: [
{ active: false, text: 'text1', textOutput: ''},
{ active: false, text: 'text1', textOutput: ''},
{ active: false, text: 'text1', textOutput: ''},
...
],
}
now, the purpose of this array is to output data to the DOM, using computed property as below.
I want to output only the data where active: true, which is changing onclick in other part of the web, so computed resultDOM is watching for this change and changing only items.active="true". Also I use text-fields to change items.text[i] values.
computed: {
resultDOM () {
for (var i=0; i<this.items.length; i++) {
if (this.items[i].active) {
this.items[i].textOutput = '<li>' + this.items[i].text + '</li>'
console.log('bam')
}
else {
this.items[i].textOutput = ''
console.log('bam else')
}
}
var output=''
for (var i=0; i<this.items.length; i++) {
output = output + this.items[i].textOutput
}
return output
}
}
The problem is that this takes some time to execute and every time I change only one active to true (or items.text value) computed is doing check on every element of the table, so it's very ineffective.
Can I ask you for tips, how should I improve my code to be more efficient?
What is the right way to do it?
edit:
<div v-html="resultDOM"></div>
You can avoid using computed at all if you take advantage of conditional rendering and v-for directive:
const app = new Vue({
el: "#app",
data: {
items: [
{ active: true, text: 'text1',},
{ active: true, text: 'text2',},
{ active: false, text: 'text3',},
],
},
})
<script src="https://unpkg.com/vue"></script>
<div id="app">
<ul>
<li v-for="item in items" v-if="item.active" v-text="item.text"></li>
</ul>
</div>
jsfiddle

How can I make a reusable "CheckAll" checkbox solution in Vue2

I am trying to create a reusable "Check All" solution for displaying a list of objects retrieved from an API.
I really like the get/set methods of computed properties that I use in this example here, https://codepen.io/anon/pen/aLeLOZ but I find that rewriting the same function over and over again and maintaining a seperate checkbox state list is tedious.
index.html
<div id="app">
<input type="checkbox" v-model="selectAll1"> Check All
<div v-for="person in list1">
<input type="checkbox" v-model="checkbox" :value="person.id">
<span>{{ person.name }}</span>
</div>
<hr/>
<input type="checkbox" v-model="selectAll2"> Check All
<div v-for="person in list2">
<input type="checkbox" v-model="checkbox2" :value="person.id">
<span>{{ person.name }}</span>
</div>
</div>
main.js
new Vue({
el: '#app',
data () {
return {
list1: [
{ id: 1, name: 'Jenna1'},
{ id: 2, name: 'Jenna2'},
{ id: 3, name: 'Jenna3'},
{ id: 4, name: 'Jenna4'}
],
list2: [
{ id: 1, name: 'Mary1'},
{ id: 2, name: 'Mary2'},
{ id: 3, name: 'Mary3'},
{ id: 4, name: 'Mary4'}
],
checkbox: [],
checkbox2: []
}
},
computed: {
selectAll1: {
get: function () {
return this.list1 ? this.checkbox.length === this.list1.length : false
},
set: function (value) {
let selected = []
if (value) {
this.list1.forEach(function (bf) {
selected.push(bf.id)
})
}
this.checkbox = selected
}
},
selectAll2: {
get: function () {
return this.list2 ? this.checkbox2.length === this.list2.length : false
},
set: function (value) {
let selected = []
if (value) {
this.list2.forEach(function (bf) {
selected.push(bf.id)
})
}
this.checkbox2 = selected
}
},
}
});
How can I make a resuable selectAll() function that will work in this example that can be included as often as needed?
Is it possible to make a class that can maintain the check box state for each list and still function as a computed property to make use of the v-model directive?
It's not the same at all, but a method based solution would be
methods: {
selectUs: function(){
if (this.checkbox.length <= this.list1.length) {
this.checkbox = Array.from(Array(this.list1.length + 1).keys())
} else {
this.checkbox = []
}
}
}
with #click="selectUs" instead of v-model="selectAll1"
(you could keep the get part of your computed properties to keep track of whether all are selected, and then use if (selectAll1) { etc } in the method)

Vue.js - Reactivity of a select whose value is the selected object and not its id

I'm using vue.js 2.3 and element-ui. I'm facing reactivity issues with a select dropdown whose value is the item picked.
Problems
The select is not automatically filled with the initial object value
Notices
If I change :value="item" to :value="item.name"
and form: {option: {name:'blue', price: ''}} to form: {option:'blue'}
It is working
Questions
Is there a way to make the select dropdown fully reactive when its value is not just a string or a id but rather the whole object that has been selected
https://jsfiddle.net/LeoCoco/aqduobop/
<
div style='margin-bottom:50px;'>
My form object :
{{form}}
</div>
<el-button #click="autoFill">Auto fill</el-button>
<el-select v-model="form.option" placeholder="Select">
<el-option v-for="item in options" :key="item.name" :label="item.name" :value="item">
</el-option>
</el-select>
</div>
var Main = {
data() {
const options = [
{name: 'blue', price: '100$'},{name: 'red', price: '150$'},
]
return {
currentItem: 0,
options,
form: {
option: {name:'', price: ''},
},
testForm: {
option:{name:'red', price: '150$'}
},
}
},
methods: {
autoFill() {
this.form = Object.assign({}, this.testForm); // -> Does not work
}
}
}
var Ctor = Vue.extend(Main)
new Ctor().$mount('#app')
Your issue is that when the selected value is an object, then form.option needs to be the same object in order for it to be selected in the select list.
For example if I change the fiddle code to this, I think it works the way you expect.
var Main = {
data() {
const options = {
'color': [{name: 'blue', price: '100$'},{name: 'red', price: '150$'}],
'engine': [{name: '300hp', price: '700$'},{name: '600hp', price: '2000$'}],
}
let currentCategory = 'color'
return {
currentCategory,
currentItem: 0,
options,
form: {
option: options[currentCategory][0]
},
testForm: {
option:{name:'blue', price: '100$'}
},
}
},
methods: {
autoFill() {
this.form = Object.assign({}, this.testForm); // -> Does not work
}
}
}
var Ctor = Vue.extend(Main)
new Ctor().$mount('#app')
Here is your fiddle updated.
You said your values are coming from an ajax call. That means that when you set form.option you need to set it to one of the objects in options[currentCategory].