I have 4 range inputs. Each of them has min number 0, max number 10.
In Total they can't sum to more than 22.
One way to approach this would be to disable all inputs once they hit 22 and add a reset button. I would find it to be more user-friendly to allow the ranges to be decremented after the max is reached instead of a whole reset.
I tried disabling if it's less or equal 0, but the scroller was still under control.
Check the comments on the
sandbox here if it easier , but the parent class is as below:
<template>
<div class="vote">
<div class="vote__title">Left: <span>{{ hmLeft }}</span> votes</div>
<div class="vote__body">
<div v-for="user in activeInnerPoll" :key="user._id">
<userVoteFor :hmLeft="hmLeft" #cntCount="cntCount" :id="user._id"/>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from "vuex"
import userVoteFor from "#/components/userVoteFor";
export default {
name: "Vote.vue",
components: {
userVoteFor
},
data(){
return {
votes: 22,
objRes: {} // that's where we write what id of a user and how many counts
}
},
computed: {
...mapGetters("polls", ["activeInnerPoll"]), // array of objects {_id : "some_id", cnt: 0}
hmLeft(){ // how much left, counter which tells how many votes left
let sum = 0;
for(let key in this.objRes){
sum += this.objRes[key];
}
return this.votes - sum;
}
},
methods: {
cntCount(id, cnt){ // emit for children, gets id and cnt of input-range and sets to result obj
this.objRes[id] = parseInt(cnt);
}
}
}
</script>
<style scoped lang="scss">
#import "#/assets/vars.scss";
#import "#/assets/base.scss";
.vote{
&__title{
#include center;
margin-top: 15px;
span{
font-size: 20px;
margin: 0 5px;
color: $pink;
}
}
}
</style>
Child class here:
<template>
<div class="vote__component">
<label class="vote__component__label" :for="id">{{ playerNameById( id )}}</label>
<input #input="check($event)" // thought maybe something to do with event ?
:disabled="disable"
class="vote__component__input"
:id="id"
type="range"
min="0"
max="10"
step="1"
v-model="cnt">
<div class="vote__component__res">{{ cnt }}</div>
</div>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "userVoteFor.vue",
props: {
id: {
type: String,
required: true
},
hmLeft: {
type: Number,
required: true
}
},
emits: ["cntCount"],
data() {
return {
cnt: 0,
disable: false,
lastVal: 0
}
},
computed: {
...mapGetters("user", ["playerNameById"]) // gets map object which stores names for user by id
},
methods: {
check(e){
console.log(e);
if(this.hmLeft <= 0) { //HERE IS APART WHERE I THINK SHOULD BE WRITTEN LOGIC if hmLeft <= 0 then ... , else write cnt in resObj and computed var will calc how many votes left
this.lastVal = this.cnt;
this.cnt = this.lastVal;
}
else this.$emit("cntCount", this.id, this.cnt);
}
}
}
</script>
<style scoped lang="scss">
.vote__component{
width: 80%;
margin: 10px auto;
position: relative;
display: flex;
justify-content: right;
padding: 10px 0;
font-size: 15px;
&__input{
margin-left: auto;
width: 60%;
margin-right: 20px;
}
&__res{
position: absolute;
top: 20%;
right: 0;
}
&__label{
}
}
</style>
The way I'd implement this is by using a watch and the get and set method of computed.
The array of values would be updated via a computed. This makes it easy to hook into a v-model and allows us to maintain reactivity with the original array.
The watch is then used to compute the total that is available. Then, for bonus points, we can use the total to adjust the width of the input so the step size remains consistent.
Even though this is using the composition Api, you can implement that using data, watch and computed the classical way
const makeRange = (max, vals, index) => {
const defaultMax = 10;
const num = Vue.computed({
get: () => vals[index],
set: value => vals[index] = Number(value)
});
const total = Vue.computed(() => vals.reduce((a, b) => a + b, 0), vals);
const style = Vue.computed(() => {
return `width: ${(numMax.value * 12 + 20)}px`
})
const numMax = Vue.computed(() => {
return Math.min(defaultMax, (num.value + max - total.value))
}, total);
return {num, numMax, style};
};
const app = Vue.createApp({
setup() {
const vals = Vue.reactive([5, 5, 5])
const max = 22;
const ranges = vals.map((v,i)=>makeRange(max, vals, i));
// helpers for visualising
const total = Vue.computed(() => vals.reduce((a, b) => a + b, 0), vals);
const totalLeft = Vue.computed(() => max - total.value , total.value);
return {ranges, vals, totalLeft, total, max};
}
}).mount('#app');
<script src="https://unpkg.com/vue#3.0.2/dist/vue.global.prod.js"></script>
<div id="app">
<li v-for="range in ranges">
<input
:style="range.style.value"
type="range" min="0"
:max="range.numMax.value"
v-model="range.num.value"
>
value: {{range.num.value}}
max: {{range.numMax.value}}
</li>
<li>{{ vals.join(' + ') }} = {{ total }}</li>
<li>max is {{ max }} , minus total {{total }} is {{ totalLeft }}</li>
</div>
Related
I have an audio file which plays songs. I am currently making a slider that is equal to the songs length and current time. You can't use player.currentTime in the watch component in Vue so how would you go about updating a value in realtime equal to player current time.
I currently have the v-model="player.currentTime" but that only updates when I pause the songs and not real-time.
This is what I have so far
player: new Audio()
this.player.src = this.songs[this.songIndex].src
this.player.play()
Player:
<input
type="range"
name="timeStamp"
ref="time"
v-model.lazy="this.player.currentTime"
step="0.1"
class="w-[100%] hover:cursor-pointer"
/>
You have to listen to the timeupdate event. I made a simple sample code:
Output:
<template>
<div style="border: 1px solid gray; border-radius: 5px; padding: 5px;">
<div>
<button #click="play">Play | Pause</button>
{{ timeLabel }}
</div>
<div>
<input
type="range"
:min="0"
:max="duration"
v-model="currentTime"
#input="updateTime"
>
</div>
</div>
</template>
<script>
export default {
name: 'BaseAudioPlayerTest',
data() {
return {
src: 'Spring-Flowers.mp3',
player: null,
duration: 0,
currentTime: 0,
timeLabel: '00:00:00',
};
},
methods: {
play() {
if (this.player.paused) {
this.player.play();
this.duration = this.player.duration;
} else {
this.player.pause();
}
},
updateTime() {
this.player.currentTime = this.currentTime;
},
timeupdate() {
this.currentTime = this.player.currentTime;
const hr = Math.floor(this.currentTime / 3600);
const min = Math.floor((this.currentTime - (hr * 3600)) / 60);
const sec = Math.floor(this.currentTime - (hr * 3600) - (min * 60));
this.timeLabel = `${hr.toString()
.padStart(2, '0')}:${min.toString()
.padStart(2, '0')}:${sec.toString()
.padStart(2, '0')}`;
},
},
mounted() {
this.player = new Audio(this.src);
this.player.addEventListener('timeupdate', this.timeupdate, false);
},
};
</script>
You can find more info here.
I am trying to add multiple product in the cart. I am using vuejs and django rest framework. My problem is: When I add a product into the cart it added successfully But when I add another product it doesnt add. It adds the same product again and again.
For example:
I have three products named "A" and "B" and "C". I added "A". Then i try to add "C" in the cart. But it stills adds "A" in the cart. I cleared the session and tried again still add the "A" product if I try to add "C" product first. It always add "A" product.
Here is my store/index.js:
import { createStore } from 'vuex'
export default createStore({
state: {
cart: {
items: []
},
isAuthenticated: false,
token: '',
isLoading: false,
},
mutations: {
initializeStore(state){
if(localStorage.getItem('cart')){
state.cart = JSON.parse(localStorage.getItem('cart'))
}
else{
localStorage.setItem('cart', JSON.stringify(state.cart))
}
},
addToCart(state, item) {
let exists = state.cart.items.filter(i => i.product.id === item.product.id)
if (exists.length){
exists[0].quantity = parseInt(exists[0].quantity) + parseInt(item.quantity)
}
else{
state.cart.items.push(item)
}
localStorage.setItem('cart', JSON.stringify(state.cart))
}
},
actions: {
},
modules: {
}
})
Here is my add to cart page and code:
<template>
<br />
<div class="col">
<div class="col1">
<img v-bind:src="product.get_image" alt="">
</div>
<div class="col2">
<div class="product__title">
<p>PRODUCT TITLE</p>
<h2>{{ product.name }}</h2>
<small>{{ product.short_description }}</small>
</div>
<div class="product__price">
<p>Price: $ {{ product.price }}</p>
</div>
<div class="product__button">
<input type="hidden" v-model="quantity" min="1">
<button type="submit" class="button__primary" #click="addToCart">Add To Cart</button>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
import {toast} from 'bulma-toast'
export default {
name: "ProductDetail",
data () {
return {
product: {},
quantity: 1
}
},
mounted() {
this.getProduct()
},
methods: {
getProduct() {
const categorySlug = this.$route.params.category_slug
const productSlug = this.$route.params.product_slug
axios.get(`/api/product-details/${categorySlug}/${productSlug}/`)
.then(response => {
this.product = response.data
})
.catch(error => {
console.log(error)
})
},
addToCart() {
if(isNaN(this.quantity) || this.quantity < 1){
this.quantity = 1
}
const item = {
product: this.product,
quantity: this.quantity
}
this.$store.commit("addToCart", item)
toast({
message: "Product has been added to cart" + item.product.name,
type: "is-success",
pauseOnHover: true,
duration: 2000,
position: "bottom-right",
dismissible: true,
})
}
}
}
</script>
<style scoped>
.col {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 5rem;
height: 50vh;
padding: 60px;
}
.col1 {
height: 520px;
width: 100%;
}
.col1 img{
object-fit: fill;
height: 520px;
width: 100%;
}
.col2 {
display: grid;
grid-auto-rows: 1fr 1fr 1fr 1fr;
background: #fff;
padding: 12px;
}
.product__button button{
height: 40px;
width: 100%;
border: none;
background: #007bc4;
color: #fff;
border-radius: 3px;
}
.product__price p {
color: #007bc4;
font-size: 18px;
}
</style>
Here is the cartitem component:
<template>
<tr class="is-fullwidth">
<td>{{ item.product.name }}</td>
<td>$ {{ item.product.price }}</td>
<td>
<button #click="increment(item)" class="plusButton">+</button>
{{ item.quantity }}
<button #click="decrement(item)" class="minusButton">-</button>
</td>
<td>{{ getTotal(item).toFixed(2) }}</td>
<td><button class="delete"></button></td>
</tr>
</template>
<script>
export default {
name: "Cartitem",
props: {
initialItem: Object
},
data() {
return {
item: this.initialItem
}
},
methods: {
getTotal(item) {
return item.quantity * item.product.price
},
increment(item){
item.quantity += 1
this.updateCart()
},
decrement(item) {
item.quantity -= 1
if(item.quantity === 0){
this.$emit('removeFromCart', item)
}
this.updateCart()
},
updateCart() {
localStorage.setItem('cart', JSON.stringify(this.$store.state.cart))
},
removeFromCart(item) {
this.$emit('removeFromCart', item)
this.updateCart()
}
}
}
</script>
<style scoped>
.plusButton{
border: none;
background: #fff;
font-size: 19px;
}
.minusButton{
border: none;
background: #fff;
font-size: 19px;
}
</style>
I am new in vue js. I am trying to build this projetc so that i can learn. But this issue is eating my brain. I tried to use find function in sotre/index.js. It solved my problem though but if i clear the cookies and try to add product it gives me error.
is there any solution for me?
Thanks in advance.
i`m having a problem with my vue, the problem is im trying to print 2 words, that is 'A.2' and 'B.3', but when im printing it, it just show 'B.3' and 'B.3'. here is my code
this is a simple quiz project, so everytime a user choose option a with true status it should be adding 1 point to the score, i haven`t made that yet.
<template>
<div class="hello">
<h1 v-if="show">hai</h1>
<h1 v-else>hehe</h1>
<p>{{ nama}}</p>
<input type="text" v-model="nama">
<button type="button" v-on:click="hideTitle">Click Me</button>
<h3> 1.Yang Dipakai Di sepatu adalah </h3>
<p>{{ nama}}</p>
<h3 v-for="j in jawaban">
<input type="radio">
{{j}}
</h3>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
},
data : function() {
return{
nama: 'Luthfi',
show: true
},
{
jawaban: 'A.2',
correct: true
},
{
jawaban: 'B.3',
correct: false
},
{
jawaban: 'C.4',
correct: false
}
},
methods: {
hideTitle() {
this.show = !this.show
}
},
mounted: function () {
this.nama = 'Fitra'
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>
i expect there is 4 output from option A to D, but it kept showing me same option
In your code, data() returns only one object that contains
{
nama: 'Luthfi',
show: true
}
You must change this like:
data : function() {
return{
nama: 'Luthfi',
show: true,
jawaban: 'A.22',
correct: true,
jawabann: 'B.3'
}
}
Vue.config.devtools = false
Vue.config.productionTip = false
let modal = Vue.extend({
template: `
<div class="modal">
<p>Balance: {{ balance }}</p>
<input #input="$emit('input', $event.target.value)" :value="value">
<button #click="$emit('input', balance)">ALL</button>
</div>
`,
props: ['balance', 'value']
})
function makeComponent(data) {
return { render(h) { return h(modal, data) } }
}
Vue.component('app', {
template: `
<div>
<p>Balance: {{ balance }}</p>
<p>To withdraw: {{ withdrawAmount }}</p>
<p>Will remain: {{ balance - withdrawAmount }}</p>
<button #click="onClick">Withdraw</button>
<modal-container ref="container"/>
</div>`,
data () {
return {
withdrawAmount: 0,
balance: 123
}
},
methods: {
onClick () {
this.$refs.container.show(makeComponent({
props: {
balance: String(this.balance),
value: String(this.withdrawAmount)
},
on: {
input: (value) => {
this.withdrawAmount = Number(value)
}
}
}))
}
}
})
Vue.component('modal-container', {
template: `
<div>
<component v-if="canShow" :is="modal"/>
</div>
`,
data () {
return { modal: undefined, canShow: false }
},
methods: {
show (modal) {
this.modal = modal
this.canShow = true
},
hide () {
this.canShow = false
this.modal = undefined
}
}
})
new Vue({
el: '#app'
})
.modal {
background-color: gray;
width: 300px;
height: 100px;
margin: 10px;
padding: 10px;
}
* {
font-family: "Source Sans Pro", "Helvetica Neue", Arial, sans-serif;
color: #2c3e50;
line-height: 25px;
font-size: 14px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17-beta.0/vue.js"></script>
<div id="app">
<app></app>
</div>
This is a simplified version of our application using vue-js-modal. The basic Idea is that we pass a component declaration along with required VNodeData to the plugin function and it would append that component to a pre-defined DOM point with proper data bindings.
And our usecase is that:
User clicks 'Withdraw'
Modal pops up with an input field and static field displaying user's balance
User enters the amount he/she wants to withdraw and that amount and resulting balance are displayed in the callee component.
There is a 'ALL' button besides the input field for user to easily enter a number equal to his/her balance
Problem: When user clicks 'ALL', the 'modal' component fires an 'input' event with value of the balance and the callee component receives that event and updates its 'withdrawAmount'. But the 'withdrawAmount' is supposed to be passed back to the 'modal' as 'value' prop and further updates the input field, which doesn't happen.
Following is the code, I define a my data in Vue as below:
{
result: {
name: 'user A',
age: 20,
male: true
},
showDetails: true
}
There are several filter/computed/methods in the view will do the result formatting.
The problem is even I just change the value of the showDetailds, the methods and filters defined for the result will be triggered as well.
May I know any issue in my code ? Or it's the natural of Vue.js (I don't believe.) ?
function countTheCall(key) {
let counter = document.getElementById(key)
if (!counter) {
let container = document.getElementById('statList')
var item = document.createElement('li')
item.innerHTML = `${key}: <span id='${key}'>0</span>`
container.appendChild(item)
counter = document.getElementById(key)
}
counter.innerText = _.parseInt(counter.innerText) + 1
}
var vm = new Vue({
el: '#app',
data() {
return {
result: {
name: 'user A',
age: 20,
male: true
},
showDetails: true
}
},
methods: {
countTheCall: function(key) {
let counter = document.getElementById(key)
counter.innerText = _.parseInt(counter.innerText) + 1
},
trimValue: function(value) {
console.log('Invoke method - trimValue.')
countTheCall('methodCalls of trimValue')
return _.trim(value)
},
getValFromResult: function(key) {
console.log('Invoke method - getValFromResult.')
countTheCall('methodCalls of getValFromResult')
return _.get(this.result, key, 'null')
}
},
computed: {
displayName: function() {
console.log('Invoke computed value - computedVal.')
countTheCall('computedCalls of displayName')
return `${this.result.name} (${this.result.age})`
}
},
filters: {
convertString: function(val) {
countTheCall('filterCalls of convertString')
return _.upperFirst(val)
},
getValFromObject: function(obj, key) {
console.log(`[filter] getValue From Object by ${key}`)
countTheCall('filterCalls of getValFromObject')
return _.get(obj, key, 'null')
},
convertDateString: function(datestr, format = 'MMM DD (ddd), YYYY') {
console.log(`[filter] convertDateString ${datestr}`)
countTheCall('filterCalls of convertDateString')
let m = moment(datestr)
let formattedStr = ''
if (m.isValid()) {
formattedStr = m.format(format)
}
return formattedStr
}
}
})
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
label {
font-style: italic;
color: black;
}
span {
font-weight: bold;
color: darkblue;
}
.callStat {
float: right;
width: 400px;
border: solid darkgrey 2px;
padding: 5px;
margin-right: 40px;
}
<body>
<script type="text/javascript" src="//cdn.bootcss.com/vue/2.2.4/vue.min.js"></script>
<script type="text/javascript" src="//cdn.bootcss.com/moment.js/2.17.1/moment-with-locales.min.js"></script>
<script type="text/javascript" src="//cdn.bootcss.com/lodash.js/4.17.4/lodash.min.js"></script>
<div class="callStat">
<h4>The Stat. of Calls</h4>
<ul id="statList"></ul>
</div>
<div id="app">
<h2>User Profile</h2>
<div>
<label>User Name:</label>
<input type="text" v-model="result.name"></input>
</div>
<div>
<label>Show Details:</label>
<input type="checkbox" id="showDetails" v-model="showDetails"></input>
<label for="showDetails">show detail section?</label>
</div>
<hr/>
<div>
<label>User Name:</label>
<ul>
<li>result.name: <span>{{trimValue(result.name)}}</span></li>
<li>result.unknownFiled: <span>{{result.unknownFiled}}</span></li>
<li>getValFromResult('name'): <span>{{ getValFromResult('name')}} </span></li>
<li>getValFromResult('unknownField'): <span>{{ getValFromResult('unknownField')}} </span></li>
<li>computed Display Name : <span>{{ displayName }} </span></li>
<li>convertString by filter : <span>{{ result.name | convertString }} </span></li>
<li>user birthDay: <span>{{ result.birthDay | convertDateString}}</span></li>
</ul>
</div>
</div>
</body>
This is natural to Vue.
Methods & Filters run on every DOM re-render.
This is why it's best to use Computed whenever possible - these re-run only on change of connected properties.