Calculating multiple totals from a v-for loop - vue.js

I have a unique situation where I have a v-for loop of users (Staff) and inside that I have another v-for loop checking the leave a user has accumulated.
so to put it simply
v-for get user
//Print users name
v-for get any leave associated with this user
//Print that days Annual Leave
//Print that days Sick Leave
v-end
//Print total Annual Leave
//Print total Sick Leave
v-end
The leave database content has these values
Type: (Sick Leave, Annual Leave, Bereavement, etc)
Hours: integer
So essentially it will say
Thomas Annual Sick
------------------------------------------
Annual Leave 2 hours 0 Hours
Sick Leave 0 Hours 3 Hours
Annual Leave 4 Hours 0 Hours
-------------------------------------------
Total 6 Hours 3 Hours
John Annual Sick
------------------------------------------
Annual Leave 2 hours 0 Hours
Annual Leave 2 Hours 0 Hours
-------------------------------------------
Total 4 Hours 0 Hours
Now for the code and what I have so far:
<div v-for="user_list in user_list_filtered()">
<div class="user_heading"><h2>{{ user_list.first_name }}</h2></div>
<div class="report_content" v-for="userleave in user_leave_filtered(user_list['.key'])">
<div class="leave_type_content">
{{ userleave.type }}
</div>
<div class="days_taken_content">
//Get Leave
</div>
<div class="lsl_content">
//Get Sick Leave
</div>
</div>
<div class="leave_content">
<div class="total_leave_title">
Total Leave Taken
</div>
<div class="total_hours">
// Print Total Leave
</div>
<div class="total_hours">
//Print Total Sick Leave
</div>
</div>
</div>
So if it is of Type Sick Leave add it to the second column and set the first column to 0 or if !== Sick Leave set first column to value and second column to 0. Then add each side up and print below.
I have tried some things as functions but I get infinite loops so I am kinda stuck as most posts are not as complicated as what I am trying to achieve.
Thanks for the help
Edit:
Additional functions
user_leave_filtered(userPassed) {
var self = this
return this.userLeave.filter(function (i) {
if (i.users_id === userPassed &&
((i.start_time >= self.getUnix(self.firstDate) && i.start_time <= self.getUnix(self.lastDate)) ||
(self.firstDate === null || self.firstDate === '' || self.lastDate === null || self.lastDate === ''))) {
return true
} else {
return false
}
})
},
user_list_filtered() {
var self = this
return this.userList.filter(function (i) {
var passed = false
if (self.userToShow === i['.key'] || self.userToShow === 'All') {
// Track whether to filter out this leave or not
self.userLeave.forEach(function (element) {
if (element.users_id === i['.key']) {
passed = true
} else {}
})
}
return passed
})
},

First rule of thumb, don't call functions in your HTML. Use computed properties instead.
You can get a filtered user list and map it to present the data you need per user.
Anyway, I recommend you to handle the mapping of "user leaves" in the backend, and bring the data as close as you'll use it in the client.
This is an example of how I'd address your case (notice I don't use the same object structure you are probably using, since you didn't provide the full code in your question)
new Vue({
el: "#app",
data: {
userList: [
{ id: 1, firstName: "Jon Doe" },
{ id: 2, firstName: "Jane Doe" }
],
userLeave: [
{ userId: 1, type: "anual", hours: 2 },
{ userId: 1, type: "sick", hours: 3 },
{ userId: 1, type: "anual", hours: 4 },
{ userId: 2, type: "anual", hours: 2 },
{ userId: 2, type: "sick", hours: 3 },
{ userId: 2, type: "anual", hours: 4 }
]
},
computed: {
usersById () {
if (!this.userList.length) return null;
// create a list of users by id and save a few iterations
return this.userList.reduce((list, user) => {
list[user.id] = user;
return list
}, {})
},
filteredUsers () {
if (!this.userLeave.length) return [];
const users = {}
this.userLeave.forEach(leave => {
const user = this.usersById[leave.userId]
if (user) {
if (leave.type === "sick") {
user.totalSick = typeof user.totalSick === "number"
? leave.hours + user.totalSick
: leave.hours;
} else {
user.totalAnual = typeof user.totalAnual === "number"
? leave.hours + user.totalAnual
: leave.hours;
}
if (user.leaves === undefined) user.leaves = []
user.leaves.push(leave)
users[user.id] = user
}
})
return users
}
}
})
.leave_type_content,
.days_taken_content,
.lsl_content,
.total_leave_title,
.total_hours,
.total_hours {
display: inline-block
}
<script src="https://unpkg.com/vue#2.5.17/dist/vue.min.js"></script>
<div id="app">
<div v-for="user in filteredUsers"> <!-- NOTICE THE COMPUTED PROPERTY -->
<div class="user_heading"><h2>{{ user.firstName }}</h2></div>
<div class="report_content" v-for="userleave in user.leaves">
<div class="leave_type_content">
{{ userleave.type }}
</div>
<div class="days_taken_content">
{{ userleave.type === "anual" && userleave.hours || 0 }} hours
</div>
<div class="lsl_content">
{{ userleave.type === "sick" && userleave.hours || 0 }} hours
</div>
</div>
<div class="leave_content">
<div class="total_leave_title">
Total Leave Taken
</div>
<div class="total_hours">
{{ user.totalAnual }}
</div>
<div class="total_hours">
{{ user.totalSick }}
</div>
</div>
</div>
</div>

I would create a computed property holding the users you want to display along with their mapped leave and totals. For example
computed: {
usersWithLeave () {
const unixFirstDate = this.firstDate && this.getUnix(this.firstDate)
const unixLastDate = this.lastDate && this.getUnix(this.lastDate)
// first map the leave entries by user for quick access
const leaveByUser = this.userLeave.reduce((map, element) => {
// Filter out by dates
if (
(!unixFirstDate || element.start_time >= unixFirstDate) &&
(!unixLastDate || element.start_time <= unixLastDate)
) {
const elements = map.get(element.users_id) || []
elements.push(element)
map.set(element.users_id, elements)
}
return map
}, new Map())
// now construct a filtered array of users then map to one with leave attached
return this.userList
.filter(({'.key': id}) => [id, 'All'].includes(this.userToShow) && leaveByUser.has(id))
.map(({'.key': id, first_name}) => {
const leave = leaveByUser.get(id)
return {
first_name,
leave, // an array of all leave elements
totals: leave.reduce((totals, element) => {
totals[element.type === 'Sick Leave' ? 'sick' : 'annual'] += element.Hours
return totals
}, { sick: 0, annual: 0 })
}
})
}
}
Whoo, that was more work than expected. This will produce an array of objects that look something like
{
first_name: 'Thomas',
leave: [
{ type: 'Annual Leave', Hours: 2 },
{ type: 'Sick Leave', Hours: 3 },
{ type: 'Annual Leave', Hours: 4 }
],
totals: {
sick: 3,
annual: 6
}
}
Now you can easily use this in your template
<div v-for="user in usersWithLeave">
<div class="user_heading"><h2>{{ user.first_name }}</h2></div>
<div class="report_content" v-for="userleave in user.leave">
<div class="leave_type_content">
{{ userleave.type }}
</div>
<div class="days_taken_content">
{{ userleave.type !== 'Sick Leave' && userleave.Hours || 0 }}
</div>
<div class="lsl_content">
{{ userleave.type === 'Sick Leave' && userleave.Hours || 0 }}
</div>
</div>
<div class="leave_content">
<div class="total_leave_title">
Total Leave Taken
</div>
<div class="total_hours">
{{ userleave.totals.annual }}
</div>
<div class="total_hours">
{{ userleave.totals.sick }}
</div>
</div>
</div>

Related

update DOM only after submit form vue.js

I have a form with two checkboxes. each of them are related to an object to change its props 'is_active' on true or false. on click, DOM updates immediatly : if checked, object appears and if not, disappears. on submit, object is persisted in database.
<form #submit.prevent="submit()">
<div v-for="(checker_website, index) in website.checkers" :key="index">
<input type="checkbox" class="mr-2" :id="index" :true-value="1" :false-value="0" v-model="website.checkers[index].is_active">
<label :for="index">{{checker_website.checker.name}}</label>
</div>
<div class="text-right mt-5">
<button class="save flex font-semibold py-2 px-4 rounded">Enregistrer</button>
</div>
</form>
submit() {
axios.post('/app/store',{websites: this.websites})
},
also, I have a {{total}} prop which is updated after any change on checkbox. it is calculated this way :
computed: {
total() {
let total = 0;
this.websites.forEach(website => {
website.checkers.forEach(checker => {
if (checker.is_active === 1 && checker.checker.status === 1) {
total += checker.checker.price
}
})
})
return total
}
},
and displayed like this :
<span class="p-6">
Total mensuel : {{total}}€/HT
</span>
what I want is to update DOM right after the submit and I find no way to do that. any help will be very appreciated !
One solution is to convert the total computed prop into a data property that gets updated on submit:
export default {
data() {
return {
total: 0,
}
},
computed: {
// total() {⋯} // now a data property
},
methods: {
submit() {
this.total = this.getTotal()
},
getTotal() {
let total = 0
this.websites.forEach(website => {
website.checkers.forEach(checker => {
if (checker.is_active === 1 && checker.checker.status === 1) {
total += checker.checker.price
}
})
})
return total
},
},
}
demo

How to do summation in Vue

I'm quite new to coding (less than 3months old) and I'm currently trying to learn vue.
I'm trying out this simple exercise of doing a basic shopping cart and I want to get the total of all the product amounts. Here is my code:
HTML
<template>
<div class="product" #click="isMilkshown = true">{{ productList[0].name }} $ {{ productList[0].amount }}</div>
<div class="product" #click="isFishshown = true">{{ productList[1].name }} $ {{ productList[1].amount }}</div>
<div class="product" #click="isLettuceshown = true">{{ productList[2].name }} $ {{ productList[2].amount }}</div>
<div class="product" #click="isRiceshown = true">{{ productList[3].name }} $ {{ productList[3].amount }}</div>
<!-- Cart -->
<div class="main-cart">
<div>Cart</div>
<div class="main-cart-list" v-for="product in productList" :key="product">
<div v-if="showProduct(product.name)">{{ product.name }} $ {{ product.amount }}</div>
</div>
<div>Total: 0</div>
</div>
</template>
JS
export default {
data() {
return {
productList: [
{ name: "Milk", amount: 10 },
{ name: "Fish", amount: 20 },
{ name: "Lettuce", amount: 5 },
{ name: "Rice", amount: 2.5 }
],
isMilkshown: false,
isFishshown: false,
isLettuceshown: false,
isRiceshown: false
}
},
methods: {
showProduct(name) {
if (name === "Milk" && this.isMilkshown === false) {
return false
} else if (name === "Fish" && this.isFishshown === false) {
return false
} else if (name === "Lettuce" && this.isLettuceshown === false) {
return false
} else if (name === "Rice" && this.isRiceshown === false) {
return false
} else {
return true
}
}
}
}
I want to replace the "zero" in Total with the sum of all the product amounts when a product is clicked. Hope someone can help me, thanks!
You would use a computed function.
https://v2.vuejs.org/v2/guide/computed.html
In Vue, computed functions watch all the reactive variables referenced within them and re-run to update the returned value when any of those variables change.
Simply create a computed function that loops over each productList item and sums up the amount then returns it.
You can reference this answer to learn how to sum using reduce or for a standard example with a loop Better way to sum a property value in an array
Also, you can use a v-for loop on your
<div class="product" #click="isMilkshown = true">{{ productList[0].name }} $ {{ productList[0].amount }}</div>
component so that you don't have duplicated code:
<div v-for="item in productList" key="item.name" class="product">{{ item.name }} $ {{ item.amount }}</div>
This would create one of each of those elements for each item in your productList variable.
You would then need to re-write the click handler to be dynamic too.
Lastly, you can also convert your big if/else-if chained method into a computed function too so that you watch for changes in that.
To do that, you'd make the computed return an object like this:
{
Milk: true,
Fish: false
...
}
You can put the key as the name so that in your loop you can reference the computed property like this enabledItems[item.name] to get the true/false.

VueJS how to make a button disabled, under different conditions?

I have a button on my website that gives bonuses to the user. I need to make several conditions in 1 button.
If heal_used = 1 or diff < 1, the button must be disabled. I tried to do it like this:
<button v-if="heal_used 1" :disabled="diff < 1" v-else class="btn btn--small btn--purple" #click="takeBonus">Take</button>
But nothing worked. Also, if the button is active and the user can get a bonus, after the bonus you need to make the button inactive. I did it like this:
if (data.type === 'success') {
this.bonus_num = data.bonus_num;
this.heal_used = data.heal_used;
this.$forceUpdate();
}
Is it true? Can you help me please, to make 2 condifitions?
UPDATE
I change code to:
<button class="btn btn--small btn--purple" :disabled="isDisabled" #click="takeBonus">Take</button>
And add:
computed: {
isDisabled() {
return this.heal_used = 1 || this.diff < 10;
},
},
Console.log say me:
console.log(data.heal_used);
console.log(data.diff);
0
17
But button is stil; disabled, what's wrong?
UPDATE takeBonus:
takeBonus() {
this.$root.axios.post('/user/takeBonus', {
value: this.user.cashback
})
.then(res => {
const data = res.data;
if (data.type === 'success') {
this.bonus_num = data.bonus_num;
this.$root.user.balance = data.newBalance;
this.heal_used = data.heal_used;
this.$forceUpdate();
}
this.$root.showNotify(data.type, this.$t(`index.${data.message}`));
})
},
new Vue({
el: '#example',
data: {
heal_used : 4,
diff: 3
},
methods: {
takeBonus1: function () {
this.heal_used=1;
this.diff=0;
},
takeBonus2: function () {
this.heal_used=1;
this.diff=4;
},
takeBonus3: function () {
this.heal_used=2;
this.diff=.1;
},
reset: function () {
this.heal_used=4;
this.diff=3;
}
}
})
<head>
<title>My first Vue app</title>
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.12"></script>
</head>
<body>
<div id="example">
<p>
<span>heal_used: {{ heal_used }}</span>
<span>diff: {{ diff }}</span>
</p>
<button
#click="takeBonus1()"
:disabled="heal_used===1 || diff < 1" >
Take bonus (both)
</button>
<br>
<button
#click="takeBonus2()"
:disabled="heal_used===1 || diff < 1" >
Take bonus (heal_used===1)
</button>
<br/>
<button
#click="takeBonus3()"
:disabled="heal_used===1 || diff < 1" >
Take bonus (diff < 1)
</button>
<br>
<button
#click="reset()">
Reset
</button>
</div>
</body>

Multiple Stopwatch using VUe

I am a newbie to Vue.
I am working on multiple stopwatch using Vue.
I'm stuck, becuase my component is updating the values on all instances of the components, instead of just in one.
This is what I tried:
<div id="app">
<user-name></user-name>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.2/vue.js"></script>
<script type='text/x-template' id="test-template">
<div>
<div class="column" v-for="(item, index) in items" :key="index">
<div class="ui segment">
<h3 class="ui blue header">{{parentTitle}}</h3>
<h2 class="ui greenheader">{{item.name}}</h2>
<div class="column">
<p class="ui huge header">
{{ hours }} :
{{ minutes | zeroPad }} :
{{ seconds | zeroPad }} :
{{ milliSeconds | zeroPad(3) }}</p>
<button class="ui secondary button" #click="startTimer"
:disabled="isRunning">START</button>
<button class="ui button" #click="pushTime" :disabled="!isRunning">LAP</button>
<button class="ui button" #click="stopTimer" :disabled="!isRunning">STOP</button>
<button class="ui basic button" #click="clearAll">CLEAR</button><br><br>
<ul class="ui bulleted list" v-if="times.length">
<li class="item" v-for="item in times">
{{ item.hours }} :
{{ item.minutes | zeroPad }} :
{{ item.seconds | zeroPad }} :
{{ item.milliSeconds | zeroPad(3) }}
</li>
</ul>
<br><br>
</div>
</div>
</div>
</div>
</script>
<script>
Vue.component('user-name', {
data() {
return {
parentTitle: "Employee Names",
test: "welcome",
times: [],
animateFrame: 0,
nowTime: 0,
diffTime: 0,
startTime: 0,
isRunning: false,
items: [{
id: 1,
name: 'Employee 1'
},
{
id: 2,
name: 'Employee 2'
}
],
count: 0
}
},
template: '#test-template',
methods: {
// 現在時刻から引数に渡した数値を startTime に代入
setSubtractStartTime: function (time) {
var time = typeof time !== 'undefined' ? time : 0;
this.startTime = Math.floor(performance.now() - time);
},
// タイマーをスタートさせる
startTimer: function () {
// loop()内で this の値が変更されるので退避
var vm = this;
//console.log(this);
//alert(timer0.innerText);
vm.setSubtractStartTime(vm.diffTime);
// ループ処理
(function loop() {
vm.nowTime = Math.floor(performance.now());
vm.diffTime = vm.nowTime - vm.startTime;
vm.animateFrame = requestAnimationFrame(loop);
}());
vm.isRunning = true;
//alert(innerText);
},
// タイマーを停止させる
stopTimer: function () {
this.isRunning = false;
cancelAnimationFrame(this.animateFrame);
},
// 計測中の時間を配列に追加
pushTime: function () {
this.times.push({
hours: this.hours,
minutes: this.minutes,
seconds: this.seconds,
milliSeconds: this.milliSeconds
});
},
// 初期化
clearAll: function () {
this.startTime = 0;
this.nowTime = 0;
this.diffTime = 0;
this.times = [];
this.stopTimer();
this.animateFrame = 0;
}
},
computed: {
// 時間を計算
hours: function () {
return Math.floor(this.diffTime / 1000 / 60 / 60);
},
// 分数を計算 (60分になったら0分に戻る)
minutes: function () {
return Math.floor(this.diffTime / 1000 / 60) % 60;
},
// 秒数を計算 (60秒になったら0秒に戻る)
seconds: function () {
return Math.floor(this.diffTime / 1000) % 60;
},
// ミリ数を計算 (1000ミリ秒になったら0ミリ秒に戻る)
milliSeconds: function () {
return Math.floor(this.diffTime % 1000);
}
},
filters: {
// ゼロ埋めフィルタ 引数に桁数を入力する
// ※ String.prototype.padStart() は IEじゃ使えない
zeroPad: function (value, num) {
var num = typeof num !== 'undefined' ? num : 2;
return value.toString().padStart(num, "0");
}
}
});
new Vue({
el: "#app",
});
</script>
Here is a working JSFiddle sample here
Any help highly appreciated.
Here is a solution to your problem jsfiddle
In your code you are mixing the data form your vue instance, with the data of your component.
Instead of having 1 component you can add your component multiple times with the v-for
This is your vue instance + data:
new Vue({
el: "#app",
data() {
return {
people: [{
id: 1,
name: 'Employee 1'
},
{
id: 2,
name: 'Employee 2'
}
]
}
}
});
The solution is to pass the data of the person to a component via props (here called item), and render this component as many time as needed in the array. This way, each component is a independent "instance".
<user-name v-for="(person, index) in people" :key="person.id" :item="person"></user-name>
Vue.component('user-name', {
props:['item'],
.....

How to add new records in a table with temp values in vuejs

I have a table with records. There is an option for inline editing when you click on the field. There is an option to add new rows.
The problem is that when I click Add New 2 times and there are 2 empty rows in the table with inputs and start typing, field values for both rows are changed.
It's because I use v-model="temp.name" and isCreateMode and in my case there are several rows with this temp model, but I'm not sure how to deal with this.
temp is necessary because users can cancel editing the field. I use the same input fields for create and edit.
//in component
// other code here
computed: {
...mapState([
'editModeField',
'editedUser',
'editMode',
'createMode',
'temp',
'users'
]),
...mapGetters([
'filteredUsers'
]),
isEditable (field, user, index) {
if (this.isCreateMode(user)) {
return this.users[index] === user
}
return this.editedUser === user && this.editModeField ===
field
},
isEditMode (field, user) {
return this.editMode && this.editedUser === user &&
this.editModeField === field
},
isCreateMode (user) {
return this.createMode && !user.id
},
addUser (user, index) {
if (!user) {
user = {
name: '',
car: ''
}
this.toggleCreateMode(true)
this.createUser(user)
return
}
// this makes a request to the endpoint
this.storeUser(user, index)
},
//actions.js
createUser ({ state, commit }, user) {
commit('CREATE_USER', user)
commit('SET_TEMP_OBJECT', { name: '', car: null })
},
//mutations
CREATE_USER (state, user) {
state.users.push(user)
},
SET_TEMP_OBJECT (state, user) {
state.temp = user
},
<table>
<tr v-for="user, index in filteredUsers">
<td>{{ index + 1 }}</td>
<td>
<input
v-if="isEditable('name', user, index)"
v-model="temp.name"
v-focus="!isCreateMode(user)" />
<div v-if="isEditMode('name', user)"
#click="updateField('name', user)"></div>
<span v-if="isEditMode('name', user)"
#click="cancelUpdate('name', user)"></span>
<span
v-if="isShowMode('name', user)"
#click="editField('name', user)">{{ user.name }}</span>
</td>
<td>
<a href="javascript:void(0)"
#click="addUser()">
Add user</a>
</td>
// other columns
</table>
How about preventing the user from adding a new input unless he submits the existing one:
i can't really see how your code works since you didn't provide the temp / state of your store data but the suggested solution should be something like this :
new Vue({
el: "#app",
data: {
users: ['foo', 'bar'],
input: '',
counter: 0
},
methods: {
addUser(user) {
if (this.counter == 0) {
this.counter++
} else if (this.counter == 1 && user) {
this.counter--
this.input = ''
this.users.push(user)
} else {
alert('please fill the exisiting input')
}
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<input v-for="n in counter" type="text" v-model="input">
<button #click="addUser(input)">addUser</button>
<ul>
<li v-for="user in users">{{user}}</li>
</ul>
</div>