VUE2js: How to have component re-render after its props change? - vue.js

a Vue newbie here. The thing is is simple:
<template>
<btn :color="color" #click="toggleColor">{{btnmsg}}</btn>
</template>
<script>
import { Btn } from 'chico'
export default = {
name: 'AButton',
componenents: {
Btn
},
data () {
return {
btnmsg: 'Legia pany'
colors: ['blue', 'black', 'green', 'organge', 'teal', 'cyan', 'yellow', 'white'],
color: 'red'
}
},
methods: {
toggleColor () {
this.color = this.colors[Math.floor(Math.random() * Math.floor(this.colors.length))]
}
}
</script>
The 'Btn' from the ChicoFamily goes something like this
<template>
<button :is="tag" :class="[className, {'active': active}]" :type="type" :role="role" ">
<slot></slot>
</button>
</template>
<script>
import classNames from 'classnames';
export default {
props: {
tag: {
type: String,
default: "button"
},
color: {
type: String,
default: "default"
...it takes hellotta props...
},
data () {
return {
className: classNames(
this.floating ? 'btn-floating' : 'btn',
this.outline ? 'btn-outline-' + this.outline : this.flat ? 'btn-flat' : this.transparent ? '' : 'btn-' + this.color,
...classes derived from these props...
)
};
}
};
</script>
Yes, it is a button that, when clicked, should change its color. Clicking it indeed changes a prop passed, but does not, in fact, have the button re-rendered. I am asking this question, because I feel like there is something bigger about Vue2 mechanics that is eluding me.
Why passing a different prop does not re-render this sweet-baby-to-be button? How does one do it properly?
Best, Paco
[edit:] The Btn takes its color from Bootstrap classes deriving from the prop. Can it be that it gets the proper props in, but the className mechanic does not catch up?

Setting a :key on the component is the best way to force Vue to re-render the component. If you require the component to be re-rendered, simply modify the value of the key, and Vue will re-render it.

Your color isn't reactive because you set it as a data and not as a computed.
The way you did it, the className will be set once when the instance will be created.
In ordre to make the className re-evaluate each time you change one of the props in state, you will have to make a computed property out of this :
Btn component :
export default {
props: {
[...]
},
computed: {
className() {
return classNames(
this.floating ? 'btn-floating' : 'btn',
this.outline ? 'btn-outline-' + this.outline : this.flat ? 'btn-flat' : this.transparent ? '' : 'btn-' + this.color);
);
},
},
}

Related

How can I can show a div if an object doesn't have a value outside of the object scope

I have a small Nuxt issue that I can't work out how to get around.
Essentially, I have an object (used for a carousel slider).
<template>
<div
:class="[$style.swiperSlide, 'swiper-slide']"
v-for="slide in slides"
:key="slide.id">
<nuxt-img
:class="[$style.img]"
:alt="slide.alt"
:src="imgSources(slide)"
sizes="sm:100vw"
/>
<div :class="[$style.info, 'info-b']" v-if="slide.info">
{{ slide.info }}
</div>
</div>
<button :class="[$style.infoOpen]"
#click="showTab"
v-if="slideInfoAvailable"
>
Close
</button>
</template>
<script>
export default {
props: {
slides: {
type: Array,
required: true,
default: () => []
}
},
computed: {
slideInfoAvailable() {
return this.slide?.info
}
},
mounted() {
const swiper = new Swiper(".swiper-container", {
. . .
});
},
methods: {
imgSources(slide) {
return `/img${slide.imgPath}.jpg`;
},
};
</script>
All works o.k, the problem is that I have a button outside of this v-for that I need to only be visible if there's slide.info but because this div is outside of the v-for it can't tell if it's available.
Cannot read property 'info' of undefined
The easiest way out of this is to add the button inside of the slider - but I can't for Z-index CSS issues. It has to be outside of the 'slider' div.
Any ideas how I can only show the button if there's slide.info? For some of my slides, there won't be.
<slider
:slides="[
{
imgPath: '/demo',
info: 'Demo info for this slide',
alt: 'Homepage'
},
{
imgPath: '/demo2',
alt: 'Homepage'
},
]"
/>
One way I could do it would be to see if .slide-active .style.info exists. If it doesn't exist then I can hide the button as slide-active is added to the active div by the slider API.
The issue is coming from the fact that you probably have some async fetching and that slides are not available upon initial render. To prevent this, you can use a computed with some optional chaining like this
export default {
computed: {
slideInfoAvailable() {
return this.slide?.info
}
}
}
Then, call it like this
<button :class="[$style.infoOpen]" #click="showTab" v-if="slideInfoAvailable">
You cannot use ?. directly in the template.
You could also do the classic way of
<button :class="[$style.infoOpen]" #click="showTab" v-if="slide && slide.info">
but it does not look as sexy IMO (but you do not need any computed).
And yeah, for this kind of thing, better to handle it with Vue than relying on some hacky dirty CSS tricks!

How to change background color of a div using Vue 3?

I have this following line inside the template:
<div class="card" #click="!state.clicked" :style="state.style">
And inside the script this code:
<script lang="ts">
import { ref, computed, reactive } from "vue";
export default {
name: "Card",
setup() {
const state = reactive({
clicked: false,
style: computed(() => {
backgroundColor: state.clicked ? "red" : "white";
})
});
return {
state
};
}
};
</script>
But my color doesn't change. My clicked flag is toggling correctly, but I just can't applied the background color.
Not sure if this is a reactivity issue or just the way I set the background color.
Any ideas ?
Your computed propertie doesnt return anything and returning nothing means it automatically returns undefined. Wrap it in parenthesis
style: computed(() => ({
backgroundColor: state.clicked ? "red" : "white"
}))

vue property as prop name without value

I want to pass a property that doesn't have any value - only name. I want the name to be dynamic according the prop.
I want to create a component of tool-tip that get the tool-tip side -> top left right or bottom. in v-tooltip there isn't property side, all side is another property without value. I want it to change according the prop (side is a variable - a prop of my component).
<template>
<v-tooltip side>
<template slot="activator">
<slot slot="activator"></slot>
</template>
<span>{{text}}</span>
</v-tooltip>
</template>
<script>
export default {
props: {
text: {
type: String,
required: true,
},
side: {
default: 'top',
},
},
}
</script>
I can't find a way to use prop as the name of the property
There are two things that you need to realise. In Vue, calling a component with <my-component has-this-prop /> it the same as calling a component with <my-component :has-this-prop="true" />. The boolean basically toggles the prop.
You can also dynamically change which props are bound to a component by using the v-bind syntax without a prop name, as outlined in the documentation.
This means that if you have a prop called side in your parent component that always contains the correct side, you can create a computed property that contains all the props you want to pass, then use v-bind on your v-tooltip to pass the props:
<template>
<v-tooltip v-bind="tooltipProps">
<!-- Something -->
</v-tooltip>
</template>
<script>
const validSides = [
'top',
'top left',
'top center',
'top right',
// you get the idea
];
export default {
props: {
side: {
type: String,
default: 'top',
validator(prop) {
return validSides.contains(prop);
}
}
},
computed: {
tooltipProps() {
if (!validSides.contains(this.side)) {
return {};
}
const propsNames = this.side.split(' ');
const props = {};
for (const propName of propsNames) {
props[propName] = true;
}
return props;
}
}
}
</script>

Vue.js Trying to use props in style

I am trying to use some props in a component of mine that show different v-icons and sizes depending on the values passed to it.
However when i try use one of the props to set the icon size with styles the icon doesnt change at all
Heres the code:
<template>
<div v-if="this.name === 'someName'">
<v-icon :style="style" >some-icon-name</v-icon>
</div>
<div v-else>
<v-icon :style="style" >some-other-icon-name</v-icon>
</div>
</template>
export default {
computed: {
style () {
return 'size: ' + this.iconSize + ';'
}
},
props: {
iconSize: {
type: Number,
required: false
},
name : {
type: String,
required: false
},
},
data () {
return {
//
}
}
}
</script>
When using the component i simply use v-bind to pass the props:
<appIcons v-bind:iconSize="90" v-name="someName" />
You seem to be confusing two things. v-icon has a prop called size whereas style is a Vue mechanism for setting custom CSS styles. Either could be used in this case. You cannot set a style of size, that's meaningless as size is not a CSS property.
I think what you want is this:
<v-icon :size="iconSize" >some-icon-name</v-icon>
That's using the size prop of v-icon rather than a custom style.
You could in theory do it using a style if you set the font-size. e.g.
style () {
return 'font-size: ' + this.iconSize + 'px'
}
Or perhaps using an object instead:
style () {
return {
fontSize: this.iconSize + 'px'
}
}

Vue component computed not reacting

I have 2 components OperatorsList and OperatorButton.
The OperatorsList contains of course my buttons and I simply want, when I click one button, to update some data :
I emit select with the operator.id
This event is captured by OperatorList component, who calls setSelectedOperator in the store
First problem here, in Vue tools, I can see the store updated in real time on Vuex tab, but on the Components tab, the operator computed object is not updated until I click antoher node in the tree : I don't know if it's a display issue in Vue tools or a real data update issue.
However, when it's done, I have another computed property on Vue root element called selectedOperator that should return... the selected operator : its value stays always null, I can't figure out why.
Finally, on the button, I have a v-bind:class that should update when the operator.selected property is true : it never does, even though I can see the property set to true.
I just start using Vue, I'm pretty sure I do something wrong, but what ?
I got the same problems before I used Vuex, using props.
Here is my OperatorList code :
<template>
<div>
<div class="conthdr">Operator</div>
<div>
<operator-button v-for="operator in operators" :op="operator.id"
:key="operator.id" #select="selectOp"></operator-button>
</div>
</div>
</template>
<script>
import OperatorButton from './OperatorButton';
export default {
name: 'operators-list',
components : {
'operator-button': OperatorButton
},
computed : {
operators() { return this.$store.getters.operators },
selected() {
this.operators.forEach(op =>{
if (op.selected) return op;
});
return null;
},
},
methods : {
selectOp(arg) {
this.$store.commit('setSelectedOperator', arg);
}
},
}
</script>
OperatorButton code is
<template>
<span>
<button type="button" v-bind:class="{ sel: operator.selected }"
#click="$emit('select', {'id':operator.id})">
{{ operateur.name }}
</button>
</span>
</template>
<script>
export default {
name: 'operator-button',
props : ['op'],
computed : {
operator() {
return this.$store.getters.operateurById(this.op);
}
},
}
</script>
<style scoped>
.sel{
background-color : yellow;
}
</style>
and finally my app.js look like that :
window.Vue = require('vue');
import Vuex from 'vuex';
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';
const store = new Vuex.Store({
state: {
periods : [],
},
mutations: {
setInitialData (state, payload) {
state.periods = payload;
},
setSelectedOperator(state, payload) {
this.getters.operateurs.forEach( op => {
op.selected = (op.id==payload.id)
})
},
},
getters : {
operators : (state) => {
if (Array.isArray(state.periods))
{
let ops = state.periods
.map( item => {
return item.operators
}).flat();
ops.forEach(op => {
// op.selected=false; //replaced after Radu Diță answer by next line :
if (ops.selected === undefined) op.selected=false;
})
return ops;
}
},
operatorById : (state, getters) => (id) => {
return getters.operators.find(operator => operator.id==id);
},
}
});
import Chrono from './components/Chrono.vue';
var app = new Vue({
el: '#app',
store,
components : { Chrono },
mounted () {
this.$store.commit('setInitialData',
JSON.parse(this.$el.attributes.initialdata.value));
},
computed: {
...mapState(['periods']),
...mapGetters(['operators', 'operatorById']),
selectedOperator(){
this.$store.getters.operators.forEach(op =>{
if (op.selected) return op;
});
return null;
}
},
});
Your getter in vuex for operators is always setting selected to false.
operators : (state) => {
if (Array.isArray(state.periods))
{
let ops = state.periods
.map( item => {
return item.operators
}).flat();
ops.forEach(op => {
op.selected=false;
})
return ops;
}
}
I'm guessing you do this for initialisation, but that's a bad place to put it, as you'll never get a selected operator from that getter. Just move it to the proper mutations. setInitialData seems like the right place.
Finally I found where my problems came from :
The $el.attributes.initialdata.value came from an API and the operator objects it contained didn't have a selected property, so I added it after data was set and it was not reactive.
I just added this property on server side before converting to JSON and sending to Vue, removed the code pointed by Radu Diță since it was now useless, and it works.