Apply style to an HTML element added dynamically by v-for using Vuejs - vue.js

I'm adding elements to a list dynamically using v-for.
<ol>
<li v-for="light in lights">
<input type="range" min="0" max="255" v-model="light.currentBrightness" v-on:change="setBrightness(light)" />
</li>
</ol>
I want to decorate the slider using rangeslider.
Problem is, when a new element is added after the DOM is initialized, it's not taking the style specified in rangeslider.js. Way to fix this is to call the reinitialize method in rangeslider.js which will redecorate all the slider elements.
I'm not sure how to call the javascript method when the element is added dynamically during the runtime. Does anyone how to do it? To me, it seems like a very common problem but I could not find a solution by Googling.
My issue is same as discussed in github.

If you're new to JavaScript and Vue, you're diving in pretty close to the deep end. The rangeslider isn't just styling (like CSS), it's a widget that replaces the built-in range input.
One basic idea behind Vue is that it controls the DOM and you only modify your model, but there are some carefully controlled exceptions. Components have lifecycle hooks where you are allowed to insert and modify DOM elements owned by the component.
Some instructions for v-model support:
So for a component to work with v-model, it should (these can be
configured in 2.2.0+):
accept a value prop
emit an input event with the new value
So we make a component whose template is a range input element. We give it a value prop. In the mounted hook, we initialize the rangeslider on the input element (made available as el), then set it up to emit input events on change.
new Vue({
el: '#app',
data: {
lights: [{
currentBrightness: 10
},
{
currentBrightness: 30
}
]
},
methods: {
addRange: function() {
this.lights.push({
currentBrightness: 50
});
}
},
components: {
rangeSlider: {
props: ['value', 'min', 'max'],
template: '<input min="{{min}}" max="{{max}}" type=range />',
mounted: function() {
var vm = this
$(this.$el)
.val(this.value)
// init rangeslider
.rangeslider({
polyfill: false
})
// emit event on change.
.on('change', function() {
vm.$emit('input', this.value)
})
}
}
}
});
<link href="//cdnjs.cloudflare.com/ajax/libs/rangeslider.js/2.3.0/rangeslider.css" rel="stylesheet" />
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.2.2/vue.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/rangeslider.js/2.3.0/rangeslider.min.js"></script>
<div id="app">
<ol>
<li v-for="light in lights">
<range-slider v-model="light.currentBrightness" min="0" max="255"></range-slider>
<div>{{light.currentBrightness}}</div>
</li>
</ol>
<button #click="addRange">Add Range</button>
</div>

You can use the below CSS codes to apply some stylings in the html5 range input:
body {
padding: 30px;
}
input[type=range] {
/*removes default webkit styles*/
-webkit-appearance: none;
/*fix for FF unable to apply focus style bug */
border: 1px solid white;
/*required for proper track sizing in FF*/
width: 300px;
}
input[type=range]::-webkit-slider-runnable-track {
width: 300px;
height: 5px;
background: #ddd;
border: none;
border-radius: 3px;
}
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
border: none;
height: 16px;
width: 16px;
border-radius: 50%;
background: goldenrod;
margin-top: -4px;
}
input[type=range]:focus {
outline: none;
}
input[type=range]:focus::-webkit-slider-runnable-track {
background: #ccc;
}
input[type=range]::-moz-range-track {
width: 300px;
height: 5px;
background: #ddd;
border: none;
border-radius: 3px;
}
input[type=range]::-moz-range-thumb {
border: none;
height: 16px;
width: 16px;
border-radius: 50%;
background: goldenrod;
}
/*hide the outline behind the border*/
input[type=range]:-moz-focusring{
outline: 1px solid white;
outline-offset: -1px;
}
input[type=range]::-ms-track {
width: 300px;
height: 5px;
/*remove bg colour from the track, we'll use ms-fill-lower and ms-fill-upper instead */
background: transparent;
/*leave room for the larger thumb to overflow with a transparent border */
border-color: transparent;
border-width: 6px 0;
/*remove default tick marks*/
color: transparent;
}
input[type=range]::-ms-fill-lower {
background: #777;
border-radius: 10px;
}
input[type=range]::-ms-fill-upper {
background: #ddd;
border-radius: 10px;
}
input[type=range]::-ms-thumb {
border: none;
height: 16px;
width: 16px;
border-radius: 50%;
background: goldenrod;
}
input[type=range]:focus::-ms-fill-lower {
background: #888;
}
input[type=range]:focus::-ms-fill-upper {
background: #ccc;
}

Related

Animate width of div bound to data property in Vue

I have this progress bar div, whichs width is bound to the data property result and changes accordingly. At the moment it still jumps, but I want to animate it. I thought of tracking the old and the new Value and injecting it in the css with css variables or just using a setInterval method, but tracking the 2 values seems to get quite complicated and it seemed like a overkill for me. Does anyone have an easier idea?
<template>
<div class="progress">
<div class="progress-value" :style="{ 'width': result + '%' }">
<h2>{{ result }}%</h2>
</div>
</div>
</template>
<script>
export default {
props: ["result"],
};
</script>
<style scoped>
.progress {
background: #ccc;
border-radius: 100px;
position: relative;
padding: 5px 5px;
margin: 5px 5px;
height: 40px;
width: auto;
}
.progress-value {
animation: load 3s normal forwards;
border-radius: 100px;
background: #fff;
height: 30px;
text-align: center;
}
/* #keyframes load {
0% {
width:
}
100% {
width:
}
} */
</style>
Add css transition like this:
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 1s;
And fix the binding:
<div class="progress-value" :style="'width: ' + result + '%'">
See this example
<template>
<div class="progress">
<div class="progress-value" :style="'width: ' + result + '%'">
<h2>{{ result }}%</h2>
</div>
</div>
</template>
<script>
export default {
data () {
return {
result: 5
}
},
mounted() {
this.increment()
},
methods: {
increment() {
this.result += 10
if (this.result < 95) {
setTimeout(this.increment, 1000)
}
}
}
}
</script>
<style scoped>
.progress {
background: #ccc;
border-radius: 100px;
position: relative;
padding: 5px 5px;
margin: 5px 5px;
height: 40px;
width: auto;
}
.progress-value {
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 1s;
background: #fff;
height: 30px;
text-align: center;
}
</style>

Custom switch component with v-model doesn't work

trying to do my custom Switch component in VueJS following these manuals Using v-model on Components, Adding v-model Support to Custom Vue.js Components but something wrong and it doesn't work.
My SwitchTest component:
<template>
<div>
<input type="checkbox" :id="_uid"
:value="value"
#input="updateData"
class="checkbox">
<label :for="_uid" class="switch"></label>
</div>
</template>
<script>
export default {
props: ["value"],
data() {
return {};
},
methods: {
updateData($event) {
console.log($event.target.value);
this.$emit("input", $event.target.value);
}
}
};
</script>
<style scoped>
.switch {
position: relative;
display: inline-block;
width: 40px;
height: 20px;
background-color: rgba(0, 0, 0, 0.25);
border-radius: 20px;
transition: all 0.3s;
}
.switch::after {
content: "";
position: absolute;
width: 18px;
height: 18px;
border-radius: 50%;
background-color: white;
top: 1px;
left: 1px;
transition: all 0.3s;
}
.checkbox:checked + .switch::after {
left: 20px;
}
.checkbox:checked + .switch {
background-color: #7983ff;
}
.checkbox {
display: none;
}
</style>
And using SwitchTest component:
{{switch1}}
<SwitchTest v-model="switch1"/>
{{switch2}}
<SwitchTest v-model="switch2"/>
You can see the whole MVCE example here: https://codesandbox.io/s/ecstatic-meninsky-zx7w0?file=/src/App.vue:32-131
When I toggle checkbox, its value doesn't change.
Where am I wrong?
Thank you.
Checkboxes are a special case where they have two states (checked / unchecked) which can be translated to two values. Normally, this is simply true / false but since you've bound a single :value, you'll only ever get the same value each time.
Try binding the Boolean value prop to checked and emit a true / false value
<input
type="checkbox"
:checked="value"
:id="_uid"
#input="$emit('input', $event.target.checked)"
class="checkbox"
>

Button doesn't call the function for the first click

I have a checkbox and a button that when it's checked, then, the button suppose to submit, but when it's unchecked, the button is disable.
However, my button keep disabling and I can't submit, the reason because I add: v-on:click.prevent="checkboxJsInput"
The reason I use "prevent" is because I have disable and enable button, and "prevent" is to disable the button from submit when it's disable.
If I don't use "prevent", the button would submit even it's disable and the strange thing it's enable for the first time, but after you "check" for the second and so on, it's working.
My question, how to make the button works?
so when the button is grey, it's supposed to disable, when it's blue, it's supposed to enable to submit.
Is there a way to do this in vue way rather than using native javascript in vue?
checkboxJsInput: function() {
const checkBox = document.querySelector(".guestlistCheckboxJs");
const buttonFormJs = document.querySelector(".buttonCheckboxJs");
if (checkBox != null) {
checkBox.addEventListener("click", function() {
if (this.checked) {
buttonFormJs.disabled = false;
buttonFormJs.classList.remove("disabled");
} else {
buttonFormJs.disabled = true;
buttonFormJs.classList.add("disabled");
}
});
}
}
}
.buttonCheckboxJs.disabled {
background-color: #d6d6d6;
}
.guestlist-form-wrapper-checkbox {
display: flex;
margin: 20px 0 30px 0;
}
.guestlist-text-checkbox {
color: #ffffff;
margin: 5px 10px;
}
.guestlist-input-checkbox[type="checkbox"] {
display: none;
width: 45px;
height: 45px;
background: #d6d6d6;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05),
inset 0 -15px 10px -12px rgba(0, 0, 0, 0.05);
}
.guestlist-span-checkbox {
visibility: visible;
content: "";
display: block;
width: 25px;
height: 25px;
font-size: 20px;
text-align: center;
color: #ffffff;
background-color: #4990e2;
display: flex;
justify-content: center;
align-items: center;
border-radius: 3px;
}
.guestlist-input-checkbox[type="checkbox"]:checked
+ label
.guestlist-span-checkbox::before {
content: "✔";
}
.guestlist-form-wrapper-textarea {
width: 100%;
margin-top: 25px;
}
.guestlist-textarea {
background-color: transparent;
text-indent: 5px;
height: 50px;
width: 100%;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
color: #ffffff;
}
.button-style {
background-color: #4990e2;
border: 0;
color: #ffffff;
font-weight: bold;
font-size: 18px;
padding: 15px 20px;
border-radius: 5px;
width: 600px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div class="guestlist-form-wrapper-checkbox">
<input
class="guestlist-input-checkbox guestlistCheckboxJs"
v-on:click="checkboxJsInput"
type="checkbox"
value="1"
name="OptIn"
id="OptIn"
>
<label class="guestlist-label-checkbox" for="OptIn">
<span class="guestlist-span-checkbox"></span>
</label>
</div>
<button
v-on:click.prevent="checkboxJsInput"
class="button-style buttonCheckboxJs disabled"
>Check Availability</button>
First of all I don't understand why you are using an event listner inside an event listner for that checkbox click.
Secondly, you can use v-model.
In your data, add property check like this:
data() {
return {
check: false
}
}
Then in your html use like this:
<div class="guestlist-form-wrapper-checkbox">
<input
class="guestlist-input-checkbox guestlistCheckboxJs"
v-model="check"
type="checkbox"
value="1"
name="OptIn"
id="OptIn"
>
<label class="guestlist-label-checkbox" for="OptIn">
<span class="guestlist-span-checkbox"></span>
</label>
</div>
<button
:disabled="check"
class="button-style buttonCheckboxJs disabled"
>Check Availability</button>
I just use a check property which is a boolean. Then you can run your function on click of that button.
Consider using v-model for this.
https://v2.vuejs.org/v2/guide/forms.html#v-model-with-Components
Your checkbox can have its v-model bound to a data property, and the disabled property on the button can be bound to the same. It'd look like this:
<template>
<input type="checkbox" v-model="enableSubmit" />
<button #click="mySubmitFunction()" :disabled="!enableSubmit">
</template>
<script>
export default {
data: function() {
return {
enableSubmit: false
}
},
methods: {
mySubmitFunction() {
//...
}
}
}
</script>

How to get VueJS transitioning Divs beside eachother?

When using Vue transitions with a slide left/right animation, how can I get the Divs beside eachother?
Take a look at this pen;
https://codepen.io/anon/pen/jeBBaB
HTML
<div class="heading">
<h1>Transition demo</h1>
<h4>Why this no work?</h4>
</div>
<div class="container" id="app">
<transition :enter-active-class="enterAnimation" :leave-active-class="leaveAnimation" mode="">
<div key="one" v-if="currentStep == 1">
This is Step One
<button class="btn btn-primary" #click="currentStep = 2; previousStep=1">Next</button>
</div>
<div key="two" v-else>
This is Step Two
<button class="btn btn-primary" #click="currentStep = 1; previousStep=2">Back</button>
</div>
</transition>
</div>
CSS
$purple: #5c4084;
body {
background-color: $purple;
padding: 50px;
}
.container {
padding: 40px 80px 15px 80px;
background-color: #fff;
border-radius: 8px;
max-width: 800px;
}
.heading {
text-align: center;
h1 {
background: -webkit-linear-gradient(#fff, #999);
-webkit-text-fill-color: transparent;
-webkit-background-clip: text;
text-align: center;
margin: 0 0 5px 0;
font-weight: 900;
font-size: 4rem;
color: #fff;
}
h4 {
color: lighten(#5c3d86,30%);
text-align: center;
margin: 0 0 35px 0;
font-weight: 400;
font-size: 24px;
}
}
.btn{
outline: none !important;
}
.btn.btn-primary {
background-color: $purple;
border-color: $purple;
outline: none;
&:hover {
background-color: darken($purple, 10%);
border-color: darken($purple, 10%);
}
&:active, &:focus {
background-color: lighten($purple, 5%);
border-color: lighten($purple, 5%);
}
& .fa {
padding-right: 4px;
}
}
.form-group {
margin-bottom: 25px;
}
JS
new Vue({
el: '#app',
data: {
currentStep: 1,
previousStep: 0
},
computed:{
enterAnimation() {
if (this.currentStep < this.previousStep) {
return "animated slower fadeInLeft";
} else {
return "animated slower fadeInRight";
}
},
leaveAnimation() {
if (this.currentStep > this.previousStep) {
return "animated slower fadeOutLeft";
} else {
return "animated slower fadeOutRight";
}
}
}
});
When using no mode, the "entering" div appears on the line below the "leaving" div until then end, then it pops upwards.
I can use mode="out-in" but then there's a noticable gap between the Divs. I'd like to just have one sliding in, right next to the one sliding out. Any way to achieve this?
You could make use of absolute positioning on the div elements, you need to adjust your css a bit though.
But as a starting point, change your .container rules to this (adding position: relative;):
.container {
padding: 40px 80px 15px 80px;
background-color: #fff;
border-radius: 8px;
max-width: 800px;
position:relative;
}
and add this as a new rule below it:
.container div {position:absolute;top:0;left:0;}
The flexbox way:
Change your container rules to this:
.container {
padding: 40px 80px 15px 80px;
background-color: #fff;
border-radius: 8px;
max-width: 800px;
display:flex;
}
After this you can use a css translate rule to position the content. You can see a working example here:
Vue transitions

Stop event listener from listing to children elements in Vue

I'm creating a modal in Vue, which I want to to be able to close whenever the user clicks outside of the inner modal container. The problem is when I add an event listener on click to the parent element all children elements also trigger that event listener when clicked.
I created a simple demo below to demonstrate my problem. If the user clicks on the black portion of the parent element the modal should close but the containing white space of the child element shouldn't be able to trigger the close function.
new Vue({
el: '#modal',
data: {
active: true,
},
methods: {
closeModal() {
this.active = false
}
}
})
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
justify-content: center;
align-items: center;
background-color: black;
}
.modal.active {
display: flex;
}
.modal-content {
width: 200px;
height: 200px;
padding: 1rem;
background-color: white;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
<div id="modal" class="modal" :class="{active: active}" v-on:click="closeModal">
<div class="modal-content">This child element shouldn't be able to close the modal on click.</div>
</div>
You can try
v-on:click.self=...
Should only trigger if the target element is itself.
Hope that helps
I endorse kimy82's answer. Use click.self. Snippet updated.
new Vue({
el: '#modal',
data: {
active: true,
},
methods: {
closeModal(event) {
this.active = false
}
}
})
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
justify-content: center;
align-items: center;
background-color: black;
}
.modal.active {
display: flex;
}
.modal-content {
width: 200px;
height: 200px;
padding: 1rem;
background-color: white;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
<div id="modal" class="modal" :class="{active: active}" v-on:click.self="closeModal">
<div class="modal-content">This child element shouldn't be able to close the modal on click.</div>
</div>
There are event modifiers for the click event handler
https://v2.vuejs.org/v2/guide/events.html#Event-Modifiers
<!-- modifiers can be chained -->
<a v-on:click.stop.prevent="doThat"></a>