Vue.js : Range slider with two handles - vue.js

I want to create a vue js components where it contains a range slider of hours with two handles.
I use vue3 + vite.js
I tried this code to implement the components but when I drag one of handles I have an error
Code :
this is the template :
<template>
<div>
<input type="range" ref="rangeInput" v-model="rangeValue" #input="updateRange"/>
<div class="range-slider">
<div class="handle" :style="{left: leftHandle + '%'}" #mousedown="startHandleDrag(1)">
{{ formatHour(rangeValue[0]) }}
</div>
<div class="handle" :style="{left: rightHandle + '%'}" #mousedown="startHandleDrag(2)">
{{ formatHour(rangeValue[1]) }}
</div>
</div>
</div>
</template>
and this is the script :
<script>
export default {
data() {
return {
rangeValue: [8, 18],
handleDragging: 0
};
},
computed: {
leftHandle() {
return this.rangeValue[0];
},
rightHandle() {
return this.rangeValue[1];
}
},
methods: {
updateRange(event) {
const value = event.target.value;
const range = this.rangeValue;
if (this.handleDragging === 1) {
range[0] = value[0];
} else if (this.handleDragging === 2) {
range[1] = value[1];
} else {
range[0] = value[0];
range[1] = value[1];
}
this.rangeValue = range;
},
startHandleDrag(handle) {
this.handleDragging = handle;
document.addEventListener("mouseup", this.stopHandleDrag);
document.addEventListener("mousemove", this.updateRange);
},
stopHandleDrag() {
this.handleDragging = 0;
document.removeEventListener("mouseup", this.stopHandleDrag);
document.removeEventListener("mousemove", this.updateRange);
},
formatHour(value) {
return value + ":00";
}
}
};
</script>
Error :
any ideas to solve it !!!

In your startHandleDrag() and stopHandleDrag(), you bind updateRange() to the mousemove event:
document.addEventListener("mousemove", this.updateRange);
There are two issues with that:
The target of the mousemove event is the element under the cursor. This can be any element, and unless it happens to be an input, it will not have a value attribute (and if it does, it will not hold an array). If you really want to use the "mousemove" event, use the cursor coordinates like pageX or pageX.
You bind it as a function pointer (addEventListener("mousemove", this.updateRange)), and when called from the listener, this will refer to element.target. To avoid this, either use an arrow function (addEventListener("mousemove", (e) => this.updateRange(e))) or bind this (addEventListener("mousemove", this.updateRange.bind(this))).
I don't fully understand what you want to do with the handles, but my guess is that adding and removing listeners is a workaround, and you actually want to make them draggable? If so, have a look at the drag event. Hope that helps!

Related

Why are checkboxes not reset by v-model?

This is what i have:
Template
<div
v-for="(filter, index) in filtersList"
:key="index"
class="option-block"
>
<label
v-for="value in filter.values"
:key="value.id"
class="option-block__container"
>
{{ value.title }}
<input
type="checkbox"
v-model="filtersValues[filter.name]"
:value="value.value"
>
<span class="option-block__checkmark"></span>
</label>
</div>
And the part of my vue code:
data() {
return {
filtersList: {},
filtersValues: {}
}
},
beforeMount() {
this.loadInitData();
this.initFilters();
},
methods: {
loadInitData() {
const data = JSON.parse(this.$el.getAttribute('data-data'));
this.filtersList = data.filters;
},
initFilters() {
for (let i in this.filtersList) {
if (!this.filtersList.hasOwnProperty(i)) {
continue;
}
this.filtersValues[this.filtersList[i].name] = [];
}
}
}
It works, but when i call initFilters() method again (for reseting) checkboxes are still selected, and i don't know why.
The way you are assigning new, empty arrays to filterValues is not reactive.
If you change your initFilters to assign an entire new value to filterValues, you don't need to worry about using Vue.set(). For example
initFilters() {
this.filtersValues = this.filtersList.reduce((vals, { name }) => ({
...vals,
[ name ]: []
}), {})
}
Demo ~ https://jsfiddle.net/cjx09zwt/
Where did filter.values come from in line 2 of template?
Anyways vue would not be able to track the changes you are making (judging from the visible code)
There are some caveats to vue 2's reactivity. Check here for more info.
TLDR; you will need to declare anything you want to be made reactive in the component's data option upfront.
HTH

Vue.js: #input for <input> not working with v-for

I am creating my own custom <input> Vue component. What I am doing is that the user can never enter the wrong type of input. For that I am using regex.test() at each input.
This is my code for my Vue component for taking an integer element or an integer array:
<template>
<div>
<label>{{ label }}
<template v-if="isArray">
<input
v-model="arr[i - 1]"
#input="filterInput"
:disabled="disableWhen"
v-for="i in arraySize"
:key="i">
</input>
</template>
<template v-else>
<input
v-model="num"
#input="filterInput"
:disabled="disableWhen">
</input>
</template>
</label>
<el-button
type="success"
icon="el-icon-check"
circle
#click="confirm"
:disabled="disableWhen">
</el-button>
</div>
</template>
<script>
export default {
props: {
label: String,
nonNegative: Boolean,
disableWhen: Boolean,
isArray: Boolean,
arraySize: Number
},
data() {
return {
num: '',
arr: []
}
},
methods: {
filterInput() {
if (this.nonNegative) {
if (!/^[0-9]*$/.test(this.num)) {
this.num = '';
}
} else if (!/^(-)?[0-9]*$/.test(this.num)) {
this.num = '';
}
},
confirm() {
if (this.isArray) {
let validArrayInput = true;
for (let i = 0; i < this.arraySize; i++) {
if (!this.validInput(this.arr[i])) {
validArrayInput = false;
}
}
if (validArrayInput) {
this.$emit('confirm', this.arr);
}
} else if (this.validInput(this.num)) {
this.$emit('confirm', this.num);
}
},
validInput(x) {
return (x !== '' && x !== '-' && typeof x !== "undefined");
}
}
}
</script>
The code is working correctly when isArray = false, that is, for integer elements. But the method filterInput is never being called when isArray = true, and there is no restriction for the wrong input. What is the problem?
filterInput is being called fine for both types of input but it only attempts to manipulate num, it doesn't change arr.
Here's my attempt at implementing this:
const MyInput = {
template: `
<div>
<label>{{ label }}
<template v-if="isArray">
<input
v-for="i in arraySize"
v-model="arr[i - 1]"
:disabled="disableWhen"
:key="i"
#input="filterInput"
>
</template>
<template v-else>
<input
v-model="num"
:disabled="disableWhen"
#input="filterInput"
>
</template>
</label>
</div>
`,
props: {
label: String,
nonNegative: Boolean,
disableWhen: Boolean,
isArray: Boolean,
arraySize: Number
},
data() {
return {
arr: []
}
},
computed: {
num: {
get () {
return this.arr[0]
},
set (num) {
this.arr[0] = num
}
}
},
methods: {
filterInput() {
const arr = this.arr
const re = this.nonNegative ? /^\d*$/ : /^-?\d*$/
for (let index = 0; index < arr.length; ++index) {
if (!re.test(arr[index])) {
this.$set(arr, index, '')
}
}
}
}
}
new Vue({
el: '#app',
components: {
MyInput
}
})
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<my-input label="Single"></my-input>
<br>
<my-input label="Multiple" is-array :array-size="3"></my-input>
</div>
A few notes:
I've changed num to be a computed property backed by arr[0]. This simplifies the filtering logic as it only has to consider arr for both types of input. It could be simplified further, e.g. the template doesn't really need to handle two cases, it could treat single-valued just the same as multi-valued but with array-size of 1. Only the value that's emitted (not included in my code) really needs to have different behaviour for the single-valued case. With a little refactoring num could probably be removed altogether.
The implementation is painfully stateful. You're going to run into difficulties if you ever want to pass in values from the outside.
Rather than setting the values to '' I would suggest just stripping out the disallowed characters using replace. I have not made this change in my code, I wanted to retain the behaviour from the original example.
Closing </input> tags are invalid and I have removed them.
There was a lot of duplication in your filterInput method that I've tried to remove. It now checks all the entries in the arr array. There didn't seem to be any need to target the specific input that had changed.
this.$set is used as it's updating an array by index, which otherwise would not be detected by the reactivity system (the standard caveat for manipulating arrays).

Binding method to an option in a component in vue.js

I'm pretty new to Vue and are using a litebox component which I want to customize a bit so the lightbox gallery starts on different images depending on which button is pressed. I have succeeded to solve this by using a click event which I'm binding to an option in the litebox component. Since I am new to Vue. I'm just wondering if this is a good way of solving something like this or if there is a better way?
<template>
<div id>
<button type="button" #click="show(); start1();">Show Litebox start 1</button>
<button type="button" #click="show(); start2();">Show Litebox start 2</button>
<vue-litebox v-if="showLitebox" :startAt="start" :items="images" #close="hide"></vue-litebox>
</div>
</template>
<script>
import VueLitebox from "vue-litebox";
export default {
components: { VueLitebox },
data() {
return {
images: [
"https://placekitten.com/400/400",
"https://placekitten.com/400/401",
{
title: "My image title",
src: "https://placekitten.com/400/402"
}
],
showLitebox: false,
start: 0
};
},
methods: {
show() {
this.showLitebox = true;
},
hide() {
this.showLitebox = false;
},
start1() {
this.start = 1
},
start2() {
this.start = 2
}
}
};
</script>
Here is the code on code sandbox:
https://codesandbox.io/s/9ok4y6lopo?fontsize=12
Templates should be as logic-free as possible, besides, there is no need to chain methods this way because you can always pass parameters to methods, like this:
// in your template
<button
type="button"
#click="show(1)"
>
Show Litebox start 1
</button>
// in methods section
show (start = 1) { // defaults to 1
this.show = true;
this.start = start;
}
By the way, it seems like v-show would be a better choice than v-if for vue-litebox component (see documentation).

Is it possible to detect if a change event was triggered by a click on a Vue select element?

I have a <select>-element that has a data property bound to it using v-model in Vue.
Sometimes I want to change that value dynamically. I also have an event-listener attached to this element which is triggered on the change-event. See code example:
<template>
<div class="mySelector">
<select id="testSelect" v-model="mySelectModel"
#change="onChange($event)">
<template v-for="(item, index) in someList">
<option :class="['btn', 'btn-default', 'removing-button']" :value="index">{{item.name}}</option>
</template>
</select>
</div>
</template>
<script>
export default {
data() {
return {
mySelectModel: null
}
},
props: {
},
methods: {
customChange: function() {
this.mySelectModel = ... // some value we from somewhere else that is set dynamically on some condiftion
},
onChange: function (event) {
if (!event) return;
// DO SOMETHING THAT WE ONLY WANT TO DO ON A REAL CLICK
}
},
}
</script>
The problem I have is that when I change the data value mySelectModel dynamically, like in the customChange-method, the change event is also called, triggering the method onChange. I only want to do stuff in that method if it was really triggered by a real click, not when it was changed dynamically.
I can not find a way to distinguish between those cases when the change-event is triggered by a click or when it is just changed for some other reason. Any suggestions?
See vue-js-selected-doesnt-triggering-change-event-select-option, it appears that select does not trigger #change when v-model is updated by JS (only when the selected value is changed by user).
A directive can add the functionality
Vue.directive('binding-change', {
update: function (el, binding, vnode) {
const model = vnode.data.directives.find(d => d.name === 'model')
if (model) {
binding.value(model.value)
}
}
})
use like
<select id="testSelect"
v-binding-change="onChange"
v-model="mySelectModel"
#change="onChange($event)">
Not sure about the parameter to onChange - I'll give it a test.
Similar to this suggested solution, you can make a settable computed that you v-model in your widget:
The get function simply returns the data item
The set function does whatever you want a change in the widget to do, in addition to setting the data item
Other code can change the data item directly and will not execute the set code of the computed.
new Vue({
el: '#app',
data: {
values: ['one','two','three'],
selectedItem: 'two'
},
computed: {
wrappedSelectedItem: {
get() { return this.selectedItem; },
set(value) {
console.log("Changed in widget");
this.selectedItem = value;
}
}
},
methods: {
changeToThree() {
console.log("Stealth change!");
this.selectedItem = 'three';
}
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<div id="app">
<select v-model="wrappedSelectedItem">
<option v-for="value in values" :value="value">{{value}}</option>
</select>
<button #click="changeToThree">Set to three</button>
</div>

Passing data from a component to the parent viewmodel in Vue.js 2.x

I'm just starting with Vue.js and I'm puzzled as of how complicated it is to pass data from a component to the parent viewmodel/component.
My usecase is as follows:
I have a custom rangeslider component (https://www.npmjs.com/package/vue-range-slider). I use 3 of these in a wrapping component Typetest.vue:
<template>
<div class="typetest">
<div :class="'slider1-val-' + slider1RoundedValue">
<range-slider
class="slider"
min="1"
max="3"
step="0.01"
v-model="sliderValue1">
</range-slider>
</div>
<div :class="'slider2-val-' + slider2RoundedValue">
<range-slider
class="slider"
min="1"
max="3"
step="0.01"
v-model="sliderValue2">
</range-slider>
</div>
<div :class="'slider3-val-' + slider3RoundedValue">
<range-slider
class="slider"
min="1"
max="3"
step="0.01"
v-model="sliderValue3">
</range-slider>
<p>{{ slider3Texts[slider3RoundedValue] }}</p>
</div>
<p>Your Choice: {{ sliderSum }}</p>
<!-- this value needs to be passed to the parent viewmodel whenever it changes -->
</div>
</template>
<script>
import RangeSlider from 'vue-range-slider'
import 'vue-range-slider/dist/vue-range-slider.css'
export default {
name: 'typetest',
data () {
return {
sliderValue1: 2,
sliderValue2: 2,
sliderValue3: 2,
}
},
computed: {
slider1RoundedValue: function() {
return this.calcSliderValue(this.sliderValue1).toString();
},
slider2RoundedValue: function() {
return (this.calcSliderValue(this.sliderValue2)*10).toString();
},
slider3RoundedValue: function() {
return (this.calcSliderValue(this.sliderValue3)*100).toString();
},
sliderSum: function() {
return this.slider1RoundedValue*1 + this.slider2RoundedValue*1 + this.slider3RoundedValue*1
},
},
methods: {
hello: function() { console.log(this.sliderSum); },
calcSliderValue: function(val) {
switch (true) {
case val < 1.67:
return 1
break
case val < 2.34:
return 2
break
default:
return 3
}
}
},
components: {
RangeSlider
}
}
</script>
Now, whenever the computed property sliderSum mutates, I need to pass the result to the parent viewmodel. Several other components then need to update according to the parent viewmodel's sliderSum property, which at all times needs to be in sync with the component's computed property. It reflects the state of the three range sliders in one value. Depending on this sum I can decide in the parent which texts and images to show.
How would I achieve this?
Okay, I solved it doing the following:
1) I added a data property sliderSum on the parent viewmodel,
initially putting an empty string.
2) I added a method to the parent viewmodel that would update the sliderSum property when called, with the argument passed.
data() {
return {
sliderSum: ""
}
},
methods: {
updateSliderValue: function(newVal) {
this.sliderSum = newVal;
}
}
3) In my parent viewmodel's template, I pass the updateSliderValue method to be called whenever an event sliderValueHasMutated is emitted:
<typetest #sliderValueHasMutated="updateSliderValue"></typetest>
4) Inside the Typetest.vue component, I changed the (indepent, though identically named) computed property sliderSum as follows:
sliderSum: function () {
var sum = this.slider1RoundedValue * 1 + this.slider2RoundedValue * 1 + this.slider3RoundedValue * 1
this.$emit('sliderValueHasMutated', sum)
return sum
},
So now, whenever the computed property changes, it emits a sliderValueHasMutated event that triggers the parent's updateValue() method.
Done!