Why should I use v-bind for style - vue.js

I just started learning Vue and I was wondering, why should I use v-bind for style and not write it regularly in html/css file

Let's say you need to create a progress bar that is not static. You will then need to update the style attribute width for-example.
To accomplish this, we need to programatically edit the width of the element. We 'cannot' to this in plain css, therefore the :style attribute comes in handy.
Let's create an example:
Codepen
HTML
<div id="vue">
<div class="progress-bar">
<div :style="{'width':progress + '%'}" class="progress" />
</div>
<button #click="fakeProgress">Init fake progress</button>
</div>
Css;
.progress-bar, .progress {
border-radius: 20px;
height: 20px;
}
.progress-bar {
width: 250px;
background-color: gray;
}
.progress {
background-color: blue;
width: 0;
transition: all 1s ease;
}
Javascript
new Vue({
el: '#vue',
data: {
progress: 0
},
methods: {
fakeProgress() {
let progress = setInterval(() => {
if(this.progress == 100) {
clearInterval(progress)
} else {
this.progress += 1;
}
}, 50)
}
}
})
As you see here, we bind the progress data attribute to the width value on the fake progress bar. This is just a simple example, but I hope this makes you see its potential. (You could achieve this same effect using the <progress> tag, but that would ruin the explanation.
EDIT; Also want to point out that you are supposed to write all your css as normal as you point out in your question. However, :style is used in cases that you cannot normally use css for. Like the example above where we need css to change from a variable.

Related

In Vue SFC link click is not triggered first time

I am using Vue 3 to show a set of links for which I am assigning event handlers dynamically(based on link id).
The issues is: The first time when any link is clicked, the corresponding event is not triggered. But subsequently clicks are perfectly working.
The updated code is below:
<script setup>
const makeSizer = ([...sizes]) => {
sizes.map((size) =>{
console.log('size-' + size);
document.getElementById('size-' + size).style.display = "";
document.getElementById('size-' + size).onclick = ((e) =>{
e.preventDefault();
document.body.style.fontSize = e.target.text + 'px';
e.target.style.display = "none";
});
});
};
function zoomIt(){
return {
zoom: makeSizer([12,14,16,18])
}
}
</script>
<template>
<div class="greeting"> {{zoom}}
<p>Some paragraph text</p>
<h1>some heading 1 text</h1>
<h2>some heading 2 text</h2>
<div class="link">
12
</div>
<div class="link">
14
</div>
<div class="link">
16
</div>
<div class="link">
18
</div>
</div>
</template>
<style>
body {
font-family: Helvetica, Arial, sans-serif;
font-size: 12px;
}
h1 {
font-size: 1.5em;
}
h2 {
font-size: 1.2em;
}
.link{
padding:5px; display:inline-table;
}
.greeting {
color: red;
font-weight: bold;
}
.greeting a{
border:2px solid blue;
padding:3px;
color:white;
background-color:blue;
}
#size-12{ font-size:12px;}
#size-14{ font-size:14px;}
#size-16{ font-size:16px;}
#size-18{ font-size:18px;}
</style>
The bad news is, the way you approach it is an anti-pattern in Vue. The good news is, with some small changes you will end up with code that is much more simple to read and maintain!
You are doubling your event listeners by calling onclick() inside makeSizer() and defining click events via #click.
However, let us not just fix the bug by altering the existing code. What we want to do is to get rid of the anti-patern. So instead, we try passing the desired value of 'zoom' to the handler directly and avoid the beforementioned duplications altogether.
// Script
// We define a function that adjusts zoom value using only the value that is being passed to it as an argument
setZoom(size) {*code*}
// Template
<button #click.prevent="setZoomTo(12)">
This is a general idea. I modified your code a bit more to make it more maintainable and added comments where changes were made. I hope this helps.
Script
<script setup>
import { ref } from "vue";
const currentZoom = ref(12); // Let us set default zoom to 12
const zoomOptions = [12, 14, 16, 18]; // We define zoom options as an array to dynamically generate buttons
function setZoomTo(size) {
currentZoom.value = size; // Set current zoom value
document.body.style.fontSize = currentZoom.value + "px"; // Adjust fontSize on body
}
</script>
Template
<div class="links">
<button // We use button tag for semantic correctness
v-for="zoom in zoomOptions" // For every value in zoomOptions a button is created
:key="zoom"
:disabled="zoom === currentZoom" // If zoom value represented by the button is also currentZoom value => add disabled attribute to the button
#click.prevent="setZoomTo(zoom)" // Adjust currentZoom value according to the zoom value represented by the button
>
{{ zoom }} // Button's zoom value
</button>
</div>
Style
.links {
display: flex;
gap: 16px;
}
.links button {
border: 2px solid blue;
padding: 3px;
color: white;
background-color: blue;
cursor: pointer;
}
.links button:disabled {
opacity: 0.7; // For better UX we change button's opacity instead of hiding it
}

Super small Vue button

I'm learning Vue, and even with the simplest examples there is something wrong. For example, buttons. I have a defined component, myButton, responds to clicks, but it doesn't look like it should, is super small and dont have any label. What am I doing wrong?
Part of index.js:
Vue.component('mybutton', {
props: {
buttonLabel: String,
},
template: '<button #click="onClick()" class="btn">{{ buttonLabel }}</button>',
methods: {
onClick(){
console.log('Click');
}
},
})
Part of index.html:
<div id="app">
<mybutton text="From Vue"></mybutton>
<button class="btn">Test</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="index.js"></script>
And CSS:
.btn {
display: inline-block;
background: #000;
color: #fff;
border: none;
padding: 10px,20px;
border-radius: 5px;
text-decoration: none;
font-size: 15px;
font-family: inherit;
}
Your prop is called buttonLabel, while you pass a property called text inside your index.html. Therefore, the button doesn't get any text and then it's rendered without any inner content (and therefore slim, since you didn't give it fixed width and height).
You need to change the part of index.html and replace text with button-label (Vue automatically maps buttonLabel to it, and it is the better option. Using buttonLabel might not work in this case, since you are not using single file components.
Call it like
<mybutton mylabel="hI"></mybutton>
Vue.component('mybutton', {
props: ['mylabel'],
template: '<button>{{ mylabel }}</button>'
})
https://codepen.io/flakerimi/pen/wvgGqVb
https://v2.vuejs.org/v2/guide/components.html

Define a variable used in js and css

For example, I want to define a global color a = '#FFF', and reference it in js and css to make sure that there is only one color named a in the project. then when the value of a changed, a in js and css also changed. is that possible in vue?
Hmm... I was thinking about watchers watch in combination with CSS variables.
Maybe something like this?
Whenever a changes, the CSS variable --a changes aswell.
You can actually type in any color format you want. Hex, rgb, rgba...
let v = new Vue({
el: "#app",
data: {
a: "red"
},
watch: {
a(val){
document.documentElement.style.setProperty("--a", val);
}
}
})
:root {
--a: red;
}
#app {
height: 100px;
width:100%;
background: var(--a);
transition: background 500ms;
}
p {
background: white;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<p>Color: {{a}}</p>
<input v-model="a">
</div>

How to prevent Vue.js hidden element pop-in on page load?

I'm new to Vue.js and I have a (block) element that should be initially hidden on page load. I'm coming from a pure JS mixed with JQuery background so normally I would initially set display:none on the element use JQuery's show/hide methods etc.
I have the showing and hiding working correctly with Vue but a side effect is that the element flashes on the screen briefly on page load until the Vue setup is complete and it knows to hide the element. Setting display:none breaks the show/hide presumably because the elements class prop has higher precedence. Setting opacity:0 also seems to be overriding anything Vue is doing so that breaks the show/hide too. !important on the Vue animation classes does not help either.
The embedded sandbox below might not be the best way to reproduce this, and I suppose it might be system dependent too (speed, memory etc.) but surely this must be a common enough situation with some solution that I've missed.
VUE = new Vue({
el: '#app',
data: {
showFullpageSpinner: false
}
});
setTimeout(function() {
VUE.showFullpageSpinner = true;
setTimeout(function() { VUE.showFullpageSpinner = false; }, 1500);
}, 1500);
.fullpage-spinner-underlay {
position: fixed;
width: 100%;
height: 100%;
left: 0;
top: 0;
background: rgba(0,0,0,0.65);
z-index: 9999;
}
.fullpageSpinner-enter-active, .fullpageSpinner-leave-active {
transition: opacity .25s;
}
.fullpageSpinner-enter, .fullpageSpinner-leave-to {
opacity: 0;
}
.css-spinner {
position: absolute;
display: inline-block;
}
.css-spinner:before {
content: 'Loading...';
position: absolute;
}
.css-spinner:not(:required):before {
content: '';
border-radius: 50%;
border-top: 3px solid #daac35;
border-right: 3px solid transparent;
animation: spinner .7s linear infinite;
-webkit-animation: spinner .7s linear infinite;
}
#keyframes spinner {
to {-ms-transform: rotate(360deg);}
to {transform: rotate(360deg);}
}
#-webkit-keyframes spinner {
to {-webkit-transform: rotate(360deg);}
to {transform: rotate(360deg);}
}
#-moz-keyframes spinner {
to {-moz-transform: rotate(360deg);}
to {transform: rotate(360deg);}
}
.fullpage-loading-spinner {
left: 50%;
top: 45%;
margin-left: -40px;
margin-top: -55px;
}
.fullpage-loading-spinner:BEFORE {
width: 55px;
height: 55px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<transition name="fullpageSpinner">
<div v-if="showFullpageSpinner" class="fullpage-spinner-underlay">
<div class="css-spinner fullpage-loading-spinner"></div>
</div>
</transition>
</div>
Your problem seems to be solvable with the v-cloak directive.
This directive will remain on the element until the associated Vue instance finishes compilation. Combined with CSS rules such as [v-cloak] { display: none }, this directive can be used to hide un-compiled mustache bindings until the Vue instance is ready.
Example:
[v-cloak] {
display: none;
}
<div v-if="showFullpageSpinner" class="fullpage-spinner-underlay" v-cloak>
<div class="css-spinner fullpage-loading-spinner"></div>
</div>

Append child to $slot.default

I have a component that I need display some custom modal on screen. I don't know where I should put this dialog content, so I did something like that:
<template>
<div class="ComponentItself">
<div v-show="false" ref="ModalContent">
Hello!
</div>
<button v-on:click="showModal">Show modal</button>
</div>
</template>
[...]
Note: I could not set the tag name of [ref=ModalContent] to template because the vue reserves this tag to another feature.
My idea is when I click on "show modal" it open creates an instance of another component (v-dialog) that I have created with the [ref=ModalContent] content (it should be compiled to support nested vue components).
import Dialog from './Dialog';
const DialogCtor = Vue.extend(Dialog);
const dialog = new DialogCtor({ propsData: {...} });
dialog['$slots'].default = [ this.$refs['templateNewFolder'].innerHTML ];
{something like document.body.appendChild(dialog.$el)}
This another component have a slot that could receives the HTML content to be displayed inside of that. And it just not works. The modal is displayed, but the slot content is undefined or the HTML content not parsed.
<div class="Dialog">
[...]
<slot></slot>
[...]
</div>
The current result is something like:
What I need:
I need to know if I am on the right way. I have about the component feature, but I could not identify or understand if it is/could resolve my problem;
What I could do to make it work;
Some similar project could help it, but I could not found anyone;
Maybe I could resolve my problem if is possible I just .appendChild() directly to $slot.default, but it is not possible;
It seems to me this might be a case of an XY problem.
What probably happens is that you do not need to manually fill $slot.default, but use your Dialog component a more standard way. Since there is little detail about the latter in your question, that component might also need some refactoring to fit this "standard way".
So a more standard approach would be to directly use your <custom-dialog> component in the template of your parent, instead of using a placeholder (the one you reference as ModalContent) that you have to hide. That way, whatever HTML you pass within that <custom-dialog> will be fed into your Dialog's <slot> (designed beaviour of slot).
That way you also save the hassle of having to manually instantiate your Dialog component.
Then you can toggle your <custom-dialog> visibility (with v-if or v-show) or even manipulate its position in the DOM as you mention in your code; you can access its DOM node as $el: this.$refs.ModalContent.$el when ModalContent is a Vue instance.
You could also factorize the showModal method by delegating it to the Dialog component.
Code example:
Vue.component('modal-dialog', {
template: '#modal-dialog',
data() {
return {
modalShown: false,
};
},
methods: {
showModal() {
this.modalShown = true;
},
hideModal() {
this.modalShown = false;
},
},
});
new Vue({
el: '#app',
methods: {
showModal() {
this.$refs.ModalContent.showModal();
},
},
});
/*
https://sabe.io/tutorials/how-to-create-modal-popup-box
MIT License https://sabe.io/terms#Licensing
*/
.modal {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
opacity: 0;
visibility: hidden;
transform: scale(1.1);
transition: visibility 0s linear 0.25s, opacity 0.25s 0s, transform 0.25s;
}
.modal-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: white;
padding: 1rem 1.5rem;
width: 24rem;
border-radius: 0.5rem;
}
.close-button {
float: right;
width: 1.5rem;
line-height: 1.5rem;
text-align: center;
cursor: pointer;
border-radius: 0.25rem;
background-color: lightgray;
}
.close-button:hover {
background-color: darkgray;
}
.show-modal {
opacity: 1;
visibility: visible;
transform: scale(1.0);
transition: visibility 0s linear 0s, opacity 0.25s 0s, transform 0.25s;
}
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<div id="app">
<modal-dialog ref="ModalContent">
Hello!
</modal-dialog>
<h1>Hello World</h1>
<button v-on:click="showModal">Show modal</button>
</div>
<template id="modal-dialog">
<div class="modal" :class="{'show-modal': modalShown}" #click="hideModal">
<div class="modal-content">
<span class="close-button" ref="closeButton" #click="hideModal">×</span>
<slot></slot>
</div>
</div>
</template>
Now if you really want to fiddle with $slot, #Sphinx's linked answer in the question comments is an acceptable approach. Note that the accepted answer there also favours the standard usage. It seems to me this is also what #Sphinx implies in their 2nd comment.