Hide element with v-if after a state change - vue.js

I have been crushing my head for 2 days with this, but I think I am not understanding the reactivity thing yet.
Here is the component:
<template>
<div class="tile-content">
<router-link :to="{ name: 'anime', params: { slug: slug } }">
<div class="overlay"></div>
<figure class="image is-16by9">
<img :src="cover || defaultCover">
</figure>
<div class="name">
<h3>{{ name }}</h3>
</div>
<div class="bookmark" v-if="isAuthenticated">
<div v-if="isBookmarked" #click.prevent="unBookmark">
<b-icon icon="star"></b-icon>
</div>
<div v-else #click.prevent="addBookmark">
<b-icon icon="star-outline"></b-icon>
</div>
</div>
</router-link>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from 'vue-property-decorator';
import { mapState } from 'vuex';
import UserModule, { IUserState } from '#/app/account/store';
#Component({
computed: {
...mapState<IUserState, any>('User', {
isAuthenticated: (state: IUserState) => !!state.account.token,
bookmarks: (state: IUserState) => state.bookmarks,
}),
isBookmarked: function() {
this.bookmarks.has(this.slug);
}
}
})
export default class Tile extends Vue {
#Prop() private name!: string;
#Prop() private slug!: string;
#Prop() private cover!: URL;
private isAuthenticated!: boolean;
private isBookmarked!: boolean;
private bookmarks!: Set<string>;
get defaultCover() {
return require('#/assets/default-cover.jpg');
}
private async addBookmark() {
UserModule.AddBookmark(this.slug);
}
private async unBookmark() {
UserModule.RemoveBookmark(this.slug);
}
}
</script>
<style lang="scss" scoped>
</style>
What I want to accomplish is this:
GIVEN User with bookmarks [A, B, C] (each of them is a "slug")
WHEN User clicks star on B
THEN unBookmark will be triggered removing B from state.bookmarks
AND star will change to start-outline
My problem is that bookmarks is a computed variable and slug is a prop, and I can't seem to find a way of comparing them every time the state changes.

Just found that Set type is not reactive in Vue (yet)
https://github.com/vuejs/vue/issues/2410#issuecomment-434990853
It is planned for Vue 3.

You can use a watcher https://v2.vuejs.org/v2/guide/computed.html
Also you might be interested in lifecycle hooks : beforeUpdate, updated .

Related

How to target router-link-active class in element?

I have a component with 2 router links. I want both to be colored when either one is clicked. Problem is that these router links don't share a router-link-active class, so I'm trying to target them via querySelector and apply the color css property manually.
This is the component:
<template>
<div id="nav-link-cms" class="nav-link-cms">
<li>
<router-link class="router-link" :to="{ name: link}">
<span>
<fa class="icon" :icon="icon"></fa>
<span class="label">{{ label }}</span>
<router-link class="plus-link" v-if="plusLink" :to="{ name: plusLink }">
<fa class="icon-plus" :icon="[ 'fas', 'circle-plus' ]"></fa>
</router-link>
</span>
</router-link>
</li>
</div>
</template>
and I can target one router-link class plus-link like so:
<script setup>
import { onMounted } from 'vue';
onMounted(() => {
console.log(document.querySelector('.plus-link'))
})
</script>
This seems to work fine. The browser console outputs:
As you can see both router-link-active and plus-link classes are present in the link element.
Outputting the classList like so:
onMounted(() => {
console.log(document.querySelector('.plus-link').classList)
})
shows this in the console:
DOMTokenList ['plus-link', value: 'plus-link']
0: "router-link-active"
1: "router-link-exact-active"
2: "plus-link"
length: 3
value: "router-link-active router-link-exact-active plus-link"
[[Prototype]]: DOMTokenList
But as soon as I try to see if the list contains a the router-link-active class:
onMounted(() => {
console.log(document.querySelector('.plus-link').classList.contains('router-link-active'))
})
the console shows:
false
Is router-link-active applied even after onMounted? How can I target it?
Have not tried it myself but you should be able to use useLink Docs, from Vue-Router to get if the value for each route (link and plusLink), and then look at the isActive property of both of them then pass it to the template.
Psudo-code:
<template>
<router-link :class="{active: isActive}" :to="link">
<router-link :to="plusLink"></router-link>
</router-link>
</template>
<script setup>
import { RouterLink, RouterView } from "vue-router";
import { ref, watch } from "vue";
import { useLink } from "vue-router";
const isActive = ref(false);
const linkState = useLink({ to: "about" });
const linkPlusState = useLink({ to: "new" });
watch([linkState.isActive + linkPlusState.isActive], () => {
if (linkState.isActive || linkPlusState.isActive) {
isActive.value = true;
} else {
isActive.value = false;
}
});
</script>
Note: As a general thing you should avoid direct DOM manipulation when using any FE framework.

How to change the colour of multiple children components when triggered by a single child component in Vue

how do you affect multiple (in this case just 2) children components owned by two different parent components when an action is triggered by one of the children components?
For example I have a component, lets call it <component-one/>. Inside this component I have something like below:
<div #mouseover="hover=true" #mouseleave="hover=false" :class="setColour">
<div class="icon-wrapper commercial-layout position-relative">
<u-button icon color="transparent" #click="toggleCommercials">
<u-icon :icon="icon" color="white"/>
</u-button>
<small class="commercial-ind">COMMERCIAL ADS</small>
<div class="commercial-layout commercial-ind">{{hide}}</div>
</div>
</div>
computed: {
setColour () {
if (this.hover) {
return 'bg-danger'
}
else if (this.commercials) {
return 'bg-primary'
}
else if (!this.commercials) {
return 'bg-secondary'
}
},
watch: {
setColour: function(val) {
console.log("val",val)
}
}
But somewhere else in the code base I have two other components, lets call them <component-two/> and <component-three/>. Inside those components I use component-one. When I push on the button from component-two I want the same effect to also be triggered in component-three, and vice versa, but I'm not quite sure how to achieve that.
Currently both component-two and component-three just have component-one. I've tried adding a watch in component-one but it doesn't really do anything other than capturing changes to the setColour computed property. (I naively thought by capturing the change, all places where component-one is used will get updated)
I'm not sure I totally understand your specific component relationships, but in general I recommend using Vuex.
Using Vue 2 and the CLI, I created sample SFCs that use Vuex to store the background color CSS style. Each child is associated with a specific color, and clicking it's button updates the color of all sibling components.
/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
commonBgColor: 'navajowhite'
},
mutations: {
updateBgColor(state, newColor) {
state.commonBgColor = newColor;
}
}
})
Parent.vue
<template>
<div class="parent">
<child initBgColor="aquamarine" instanceName="One" />
<child initBgColor="mediumorchid" instanceName="Two" />
</div>
</template>
<script>
import Child from './Child.vue'
export default {
components: {
Child
}
}
</script>
Child.vue
<template>
<div class="child">
<div class="row">
<div class="col-md-6" :style="currentBgColor">
<span>Sibling Component {{ instanceName }}</span>
<button type="button" class="btn btn-secondary" #click="updateCommonBgColor">Change All Sibling Colors</button>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
initBgColor: {
type: String,
required: true
},
instanceName: {
type: String,
required: true
}
},
data() {
return {
backgroundColor: this.initBgColor
}
},
computed: {
currentBgColor() {
return 'background-color: ' + this.$store.state.commonBgColor;
}
},
methods: {
updateCommonBgColor() {
this.$store.commit('updateBgColor', this.backgroundColor);
}
}
}
</script>
<style scoped>
.child {
margin-top: 0.5rem;
}
span {
font-size: 1.5rem;
padding: 0.5rem;
}
button {
float: right;
}
</style>

Notify parallel Components about changes

i try to notify parallel components (rendered with v-for) about changes.
This is my markup:
<div v-for="item in items" :key="item.ID">
<my-component :item="item"></my-component>
</div>
And now, if for example something in the first instance of "my-component" changes all remaining "my-component"s should be notified about his change. How can i achieve that?
(At the moment i use this.$root.$emit and this.$root.$on)
Thanks in advance for your help.
There are a few ways you can achieve this:
1. Use props
VueJS' data-flow patterns recommend passing props down and bubbling events up. You could use this pattern by having a prop that is passed down that notifies the child components about the change.
Parent
<div v-for="item in items" :key="item.ID">
<my-component :item="item" :last-modified="lastModified"></my-component>
</div>
Child
<script>
export default {
...
watch: {
lastModified (v) {
// Do something
}
}
}
</script>
2. Use Vuex
Another way to achieve this is to use Vuex for global state management. This is the recommended option particularly if you're already using Vuex to manage state.
Parent
<div v-for="item in items" :key="item.ID">
<my-component :item="item"></my-component>
</div>
Child
<script>
import { mapGetters } from 'vuex'
export default {
...
computed: {
mapGetters([
'lastModified'
]),
...
},
watch: {
lastModified (v) {
// Do something
}
}
}
</script>
2. Use $refs
Another simple way to achieve this on a small-scale is to use ref.
Parent
<template>
<div v-for="item in items" :key="item.ID">
<my-component :item="item" :last-modified="lastModified" ref="childComponents"></my-component>
</div>
</template>
<script>
export default {
...
watch: {
lastModified () {
for (const child of this.$refs.childComponents) {
child.onModified()
}
}
}
}
</script>
Child
export default {
...
methods: {
onModified () {
// Do something
}
}
}
4. Use an event bus
This is a very handy way of achieving event propagation, but is not recommended. For this approach, we will create a third component, that is a simple vue instance that will be used for events.
bus.vue
import Vue from 'vue'
export default new Vue()
Parent
<template>
<div v-for="item in items" :key="item.ID">
<my-component :item="item"></my-component>
</div>
</template>
<script>
import Bus from 'bus'
export default {
...
watch: {
lastModified () {
Bus.$emit('modified')
}
}
}
</script>
Child
<script>
import Bus from 'bus'
export default {
...
methods: {
onModified () {
// Do something
}
},
beforeMount () {
Bus.$on('modified', this.onModified)
},
beforeDestroy () {
Bus.$off('modified', this.onModified)
}
}
</script>

Why won't vue.js update the DOM?

Working my way learning about Vue. I chose it as the better alternative after looking at React, Angular and Svelte.
I have a simple example that its not working probably because I'm not getting/understanding the reactive behaviour of Vue.
Plain simple App:
<template>
<div id="app">
<app-header></app-header>
<router-view />
<app-footer></app-footer>
</div>
</template>
<script>
import Header from './components/Header.vue'
import Home from './components/Home.vue'
import Footer from './components/Footer.vue'
export default {
components: {
name: 'App',
'app-header': Header,
'app-footer': Footer
}
}
</script>
Where Home.vue and Footer.vue have plain HTML content on the template.
On Header.vue I have:
<template>
<div>
<h1>The Header</h1>
<nav>
<ul>
<li>Curr Player: {{ ethaccount }}</li>
<li>Prop owner: {{ propOwner }}</li>
</ul>
</nav>
<hr />
</div>
</template>
<script>
export default {
data() {
return {
ethaccount: 'N/A',
propOwner: 'N/A'
}
},
methods: {
update() {
var ethaccount = '0xAAAAAA123456789123456789123456789'
console.log('ETH Account: ' + ethaccount)
var propOwner = '0xPPPPPPPPPPP987654321987654321'
console.log('Prop Account: ' + propOwner)
}
},
mounted() {
this.update()
}
}
</script>
But I'm unable to get the header updated and unable to find what I'm doing wrong. Help.
If you need to read a little bit more about the reactivity of the datas in vuejs check this link : https://v2.vuejs.org/v2/guide/reactivity.html
If you need to access/change your data try to do it like that :
this.$data.ethaccount = 'foo';
this.$data.propOwner = 'bar';
For me the problem is taht you re-declare your variable locally by doing :
var ethaccount = "0xAA...";
By doing such you never change the value of the data you're accessing through your template.
Hope it will solve your problem.

Share an object with another Vue component

I know this has been asked several times before, but as a Vue.js beginner I had trouble interpreting some of the other discussions and applying them to my situation. Using this CodeSandbox example, how would one pass the indicated object from "Hello" to "Goodbye" when the corresponding button is pressed? I'm unsure if I should be trying to use props, a global event bus, a plugin, vuex, or simply some sort of global variable.
Edit:
Here is the code for App.vue, Hello.vue and Goodbye.vue (from the previously linked CodeSandbox example).
App.vue
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
<script>
export default {
name: "app"
};
</script>
Hello.vue:
<template>
<div class="hello">
<h1>This is Hello</h1>
<div v-for="(obj, index) in objects" :key="index">
<router-link class="button" :to="{ path: '/goodbye'}">Share obj[{{ index }}] with Goodbye</router-link>
</div>
</div>
</template>
<script>
export default {
name: "hello",
data() {
return {
objects: [0, 1, 2, 3]
};
}
};
</script>
Goodbye.vue:
<template>
<div class="goodbye">
<h1>This is Goodbye</h1>
<p>Obj = "???"</p>
<router-link class="button" :to="{ path: '/hello'}">Hello</router-link>
</div>
</template>
<script>
export default {
name: "goodbye"
};
</script>
Props are used to share data with child components. Since the components never exist at the same time, this is not useful for you. Similarly, events are not very useful to you here. You can send an event on a global bus, but since the other component does not exist yet, it cannot listen for the event.
I am not sure what you would want to do with a plugin in this case. You should never use a global variable, unless you have a very good reason to (e.g. you use Google Analytics, which happens to use a global variable, or you want to expose something within Vue in development mode for debugging purposes). In your case, you likely want to change some global app state, which is exactly what Vuex was made for. Call a Vuex mutator or action either when clicking, or in a router hook such as router.beforeEach to save the information in a structured manner so you can then retrieve it with a mapped getter. Keep in mind that you want to structure your vuex store, so don't use a state variable thingsIWantToShareWithGoodbye, but instead split it up in previousPage, lastClickOffset and numberOfClicks.
For example:
// store/index.js
import Vuex from "vuex";
import Vue from "vue";
Vue.use(Vuex);
const state = {
button: null
};
const getters = {
button(state) {
return state.button;
}
};
const mutations = {
setButton(state, payload) {
state.button = payload;
}
};
export default new Vuex.Store({
state,
getters,
mutations
});
// Hello.vue
<template>
<div class="hello">
<h1>This is Hello</h1>
<div v-for="(obj, index) in objects" :key="index">
<router-link #click.native="setButtonState(obj)" class="button" :to="{ path: '/goodbye'}">Share obj[{{ index }}] with Goodbye</router-link>
</div>
</div>
</template>
<script>
export default {
name: "hello",
data() {
return {
objects: [0, 1, 2, 3]
};
},
methods: {
setButtonState (obj) {
this.$store.commit('setButton', obj)
}
}
};
</script>
// Goodbye.vue
<template>
<div class="goodbye">
<h1>This is Goodbye</h1>
<p>Obj = {{ button }}</p>
<router-link class="button" :to="{ path: '/hello'}">Hello</router-link>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: "goodbye",
computed: {
...mapGetters({
button: 'button'
})
}
};
</script>