VueJS: Chrome devtools doesn't instantly update an array (in parent component). Why? - vue.js

In the following code, I'm trying to get selected users from a list of given users (users array) via checkbox input (defined in a child component) in an array selectedUsers (defined in parent).
The problem is when I check/select the user, devtools doesn't update on first reaction. I've to navigate away from the selected component in devtools and then comeback to see the updated array.
app.vue (parent)
<template>
<div class="container">
<div class="row">
<div v-for="(user,idx) in users" class="col-xs-12 col-md-6 col-md-offset-3">
<child :user="user" :userIdx="idx" :selectedUsers="selectedUsers" /> <span>{{user.name }}</span>
</div>
</div>
</div>
</template>
<script>
import child from './child.vue'
export default {
data: function() {
return{
users: [
{id: 1, name: 'Allen'},
{id: 2, name: 'Jack'},
{id: 3, name: 'Obama'},
{id: 4, name: 'Donald'},
{id: 5, name: 'Winston'},
selectedUsers: []
}
},
components: {
child
}
}
</script>
child.vue (child)
<template>
<span>
<input type="checkbox" :value="user.name" #change="onSelectedUser">
</span>
</template>
<script>
export default {
props: ['user', 'userIdx', 'selectedUsers'],
data(){
},
methods: {
onSelectedUser() {
var idx = this.userIdx
if(event.target.checked) {
this.selectedUsers[idx] = event.target.value
}
if(event.target.checked == false) {
this.selectedUsers.splice(idx, 1)
}
}
}
}
</script>
<style scoped>
input[type="checkbox"] {
width: 20px;
height: 20px;
margin-right: 10px;
border: 3px solid red;
}
input[type="checkbox"]:checked {
width: 30px;
height: 30px;
}
</style>
Thanks

In this line of code, this.selectedUsers[idx] = event.target.value, I'm directly setting the value of an Array which vue is not able to pickup these direct modification to the array.
Excerpt from the below linked page
When you modify an Array by directly setting an index (e.g. arr[0] = val) or modifying its length property. Similarly, Vue.js cannot pickup these changes.
More info VueJS common gotchas - why DOM isn't updating

Related

Horizontally draggable quasar q-cards using vue.draggable.next

I've seen a few 'solutions' online about this but to no avail for us. We're working with the quasar framework and trying to setup a horizontally wrapped set of that are are draggable (for re-ordering). Below is the basic stub of the code we're working with to get this going. However, we're not able to get the cards to layout horizontally everything is stacked in a list. What are we doing wrong?
<template>
<draggable
v-model="items"
group="items"
direction="horizontal"
item-key="id"
#start="drag = true"
#end="drag = false"
>
<template #item="{ element }">
<q-card class="card">
<q-img src="~assets/temp/bb-auto.png" :alt="`Image ${element.name}`">
<div class="absolute-bottom text-subtitle2 text-center">
Image {{ element.name }}
</div>
</q-img>
</q-card>
</template>
</draggable>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import draggable from 'vuedraggable';
const drag = ref(false);
const items = ref([
{ name: 'item1' },
{ name: 'item2' },
{ name: 'item3' },
{ name: 'item4' },
{ name: 'item5' },
]);
</script>
<style scoped>
.card {
max-height: 200px;
max-width: 200px;
display: flex;
}
</style>
Turns out our overall structure was incorrect and once we sorted that out was able to back this into the draggable. We then had to basically treat our as the row.
<template>
<draggable
v-model="items"
group="items"
class="q-pa-md row items-start q-gutter-md"
direction="horizontal"
item-key="name"
#start="drag = true"
#end="drag = false"
>
<template #item="{ element }">
<q-card class="card">
<q-img src="~assets/temp/bb-auto.png" :alt="`Image ${element.name}`">
<div class="absolute-bottom text-subtitle2 text-center">
Image {{ element.name }}
</div>
</q-img>
</q-card>
</template>
</draggable>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import draggable from 'vuedraggable';
const drag = ref(false);
const items = ref([
{ name: 'item1' },
{ name: 'item2' },
{ name: 'item3' },
{ name: 'item4' },
{ name: 'item5' },
]);
</script>
<style scoped>
.card {
width: 100%;
max-height: 200px;
max-width: 200px;
display: flex;
}
</style>

nuxtjs add and remove class on click on elements

I am new in vue and nuxt and here is my code I need to update
<template>
<div class="dashContent">
<div class="dashContent_item dashContent_item--active">
<p class="dashContent_text">123</p>
</div>
<div class="dashContent_item">
<p class="dashContent_text">456</p>
</div>
<div class="dashContent_item">
<p class="dashContent_text">789</p>
</div>
</div>
</template>
<style lang="scss">
.dashContent {
&_item {
display: flex;
align-items: center;
}
&_text {
color: #8e8f93;
font-size: 14px;
}
}
.dashContent_item--active {
.dashContent_text{
color:#fff;
font-size: 14px;
}
}
</style>
I tried something like this:
<div #click="onClick">
methods: {
onClick () {
document.body.classList.toggle('dashContent_item--active');
},
},
but it changed all elements and I need style change only on element I clicked and remove when click on another
also this code add active class to body not to element I clicked
This is how to get a togglable list of fruits, with a specific class tied to each one of them.
<template>
<section>
<div v-for="(fruit, index) in fruits" :key="fruit.id" #click="toggleEat(index)">
<span :class="{ 'was-eaten': fruit.eaten }">{{ fruit.name }}</span>
</div>
</section>
</template>
<script>
export default {
name: 'ToggleFruits',
data() {
return {
fruits: [
{ id: 1, name: 'banana', eaten: false },
{ id: 2, name: 'apple', eaten: true },
{ id: 3, name: 'watermelon', eaten: false },
],
}
},
methods: {
toggleEat(clickedFruitIndex) {
this.fruits = this.fruits.map((fruit) => ({
...fruit,
eaten: false,
}))
return this.$set(this.fruits, clickedFruitIndex, {
...this.fruits[clickedFruitIndex],
eaten: true,
})
},
},
}
</script>
<style scoped>
.was-eaten {
color: hsl(24, 81.7%, 49.2%);
}
</style>
In Vue2, we need to use this.$set otherwise, the changed element in a specific position of the array will not be detected. More info available in the official documentation.

How to give a dynamically rendered element it's own data value & target it in it's parent component?

I have a BaseMenuItem component that has normal button elements as well as special news elements.
I have added a ticker effect to the news type els and want to stop the ticker on that element when it's clicked. Currently the click event stops the ticker effect on the whole group.
How can I target a single element from that group?
There are two methods, openNews one showing the specific news article that the element is linked to.
And clearItemType that clears the itemType upon recieving the emitted event from the BaseMenuItem component.
I'm just not sure which element to target to change it's itemType.
Does Vuejs have a way to make an unique data value for dynamically generated elements?
If you need anymore information please let me know!
Cheers!
BaseMenuItem
<template>
<q-btn align="left" dense flat class="main-menu-item" v-on="$listeners">
<div class="flex no-wrap items-center full-width">
<iconz v-if="iconz" :name="iconz" type="pop" color="black" class="mr-md" />
<q-icon v-if="menuIcon" :name="menuIcon" class="text-black mr-md" />
<div #click="$emit('stop-ticker')" v-if="itemType === 'news'" class="ellipsis _ticker">
<div class="ellipsis _ticker-item">{{ title }}</div>
</div>
<div v-else>
<div class="ellipsis">{{ title }}</div>
</div>
<slot>
<div class="ml-auto"></div>
<div class="_subtitle mr-md" v-if="subtitle">{{ subtitle }}</div>
<q-icon name="keyboard_arrow_right" class="_right-side" />
<ComingSoon v-if="comingSoonShow" />
</slot>
</div>
</q-btn>
</template>
<style lang="sass" scoped>
// $
.main-menu-item
display: block
font-size: 15px
position: relative
width: 100%
border-bottom: 1px solid #F5F5F5
+py(10px)
._left-side
color: #000000
._subtitle
margin-left: auto
opacity: 0.7
._ticker
position: absolute
font-weight: bold
margin-left: 2em
width: 82%
&-item
display: inline-block
padding-left: 100%
animation: ticker 8s linear infinite
#keyframes ticker
to
transform: translateX(-100%)
</style>
<script>
import { iconz } from 'vue-iconz'
export default {
name: 'MainMenuItem',
components: { iconz },
props: {
comingSoonShow: { type: Boolean, default: false },
title: { type: String, default: 'menu' },
subtitle: { type: String, default: '' },
menuIcon: { type: String, default: '' },
iconz: { type: String, default: '' },
itemType: { type: String, default: '' },
}
}
</script>
MainMenuPage
<template>
<div class="eachMenuGroup" v-if="newsList.length">
<MainMenuItem
v-for="news in newsList"
:key="news.id"
#click="openNews(news)"
:title="news.title"
:itemType="itemType"
:class="{ readLink: readNewsList[news.id] }"
menuIcon="contactless"
#stop-ticker="clearItemType"
></MainMenuItem>
</div>
</template>
<style lang="sass" scoped>
.readLink
font-weight: 500
</style>
<script>
methods: {
openNews(postInfo) {
dbAuthUser().merge({ seenNewsPosts: { [postInfo.id]: true } })
Browser.open({ url: postInfo.url, presentationStyle: 'popover' })
},
clearItemType() {
this.itemType = ''
return
},
</script>
EDIT: I edited since your latest comment. See below
To target the exact element within your v-for that fired the event, you can use $refs by using an index :
<MainMenuItem v-for="(item, index) in items" :ref="`menuItem--${index}`" #stop-ticker="clearItemType(index)" />
In your method:
clearItemType(index){
console.log(this.$refs[`menuItem--${index}`])
// this is the DOM el that fired the event
}
Edited version after your comments:
If you pass the exact same prop to each el of your v-for you wont be able to modify its value just for one child in the parent context.
Instead, keep an array of distinctive values in your parent. Each child will receive one specific prop that you can change accordingly in the parent either by listening to an child emitted event or by passing index as click event as I've done below for simplicity.
See snippet below
Vue.config.productionTip = false;
const Item = {
name: 'Item',
props: ['name', 'isActive'],
template: `<div>I am clicked {{ isActive }}!</div>`,
};
const App = new Vue({
el: '#root',
components: {
Item
},
data: {
items: ["item1", "item2"],
activeItemTypes: []
},
methods: {
toggle(index) {
this.activeItemTypes = this.activeItemTypes.includes(index) ? this.activeItemTypes.filter(i => i !== index) : [...this.activeItemTypes, index]
}
},
template: `
<div>
<Item v-for="(item, i) in items" :name="item" #click.native="toggle(i)" :isActive="activeItemTypes.includes(i)"/>
</div>
`,
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="root"></div>

Stopping an animation with an click event in Vuejs?

I have a MainMenuItem that has a bunch of static titles. There is one variable item that is for news and events and that has a horizontal ticker, scroll animation on it. This animation is happening inside of the MainMenuItem component, but the click event will be happening from the parent. The click event currently needs to stop the animation and change the font weight (can be seen in the readLink class).
Wth Vue I cannot mutate the prop type itself with something like #click="itemType = ''".
So how can I turn this ticker animation off on click?
Basically, all I would need to do is make the MainMenuItem's itemType = '' and make it the same as every other item. Do I need to make a data type? Or a click event that $emit's an event to the child/base component MainMenuItem?
If there is anymore code that is necessary please let me know!
Cheers!
MainMenuItem.vue (base component with the ticker animation )
<template>
<q-btn align="left" dense flat class="main-menu-item" v-on="$listeners">
<div class="flex no-wrap items-center full-width">
<iconz v-if="iconz" :name="iconz" type="pop" color="black" class="mr-md" />
<q-icon :name="menuIcon" />
<div v-if="itemType === 'news'" class="_ticker">
<div class="ellipsis _ticker-item">{{ title }}</div>
</div>
<div v-else>
<div class="ellipsis">{{ title }}</div>
</div>
<q-icon name="keyboard_arrow_right" class="_right-side" />
</div>
</q-btn>
</template>
<style lang="sass" scoped>
// $
.main-menu-item
display: block
font-size: 15px
position: relative
width: 100%
border-bottom: 1px solid #F5F5F5
+py(10px)
._left-side
color: #000000
._ticker
font-weight: bold
margin-left: 2em
width: 83%
white-space: nowrap
overflow: hidden
position: absolute
&-item
display: inline-block
padding-left: 100%
animation: ticker 8s linear infinite
#keyframes ticker
to
transform: translateX(-100%)
</style>
<script>
import { iconz } from 'vue-iconz'
export default {
name: 'MainMenuItem',
components: { iconz },
props: {
comingSoonShow: { type: Boolean, default: false },
title: { type: String, default: 'menu' },
subtitle: { type: String, default: '' },
menuIcon: { type: String, default: '' },
iconz: { type: String, default: '' },
itemType: { type: String, default: '' },
}
}
</script>
MainMenu.vue (just the location I am using the base component)
<template>
<MainMenuItem
v-for="news in newsList"
:key="news.id"
#click="openNews(news)"
:title="news.title"
itemType="news"
:class="{ readLink: readNewsList[news.id] }"
menuIcon="contactless"
></MainMenuItem>
</template>
export default {
name: 'MainMenu',
methods: {
openNews(postInfo) {
dbAuthUser().merge({ seenNewsPosts: { [postInfo.id]: true } })
Browser.open({ url: postInfo.url, presentationStyle: 'popover' })
}
},
computed: {
readNewsList() {
return this.authUserInfo?.seenNewsPosts || {}
},
}
<style lang="sass" scoped>
.readLink
font-weight: 500
</style>
Since it's a prop the simplest way is to emit an event that executes what you need in the parent and also bind a variable not static prop to be able to change it. example in the child :
<button #click="onClickButton">stop animation </button>
export default {
methods: {
onClickButton (event) {
this.$emit('stopAnimation')
}
}
}
And in the parent you listen to it as so:
<MainMenuItem
v-for="news in newsList"
:key="news.id"
#click="openNews(news)"
:title="news.title"
:itemType="itemType"
:class="{ readLink: readNewsList[news.id] }"
menuIcon="contactless"
v-on:stopAnimation="itemType = ''"
></MainMenuItem>
export default {
data() {
return {
itemType: "news",
}
}
}
}

Vue - passing apollo query result from parent to child component

I have this parent component which makes a call to this apollo query:
<template>
<div class="row" style="flex-wrap: nowrap; width:102%">
<BusinessContextTeamPanel :business_contexts="business_contexts"></BusinessContextTeamPanel></BusinessContextTeamDetailPanel>
</div>
</template>
<script>
apollo: {
business_contexts: {
query: gql`query ($businessContextId: uuid!) {
business_contexts(where: {id: {_eq: $businessContextId }}) {
id
businessContextTeams {
id
team {
name
id
}
role
}
}
}`,
variables: function() {
return {
businessContextId: this.currentContextId
}
}
}
},
</script>
My child component( BusinessContextTeamPanel) takes the input props and uses it like this:
<template>
<div v-if="business_contexts.length > 0">
<div v-for="teams in business_contexts" :key="teams.id">
<div v-for="team in teams.businessContextTeams" :key="team.id" style="padding-top: 20px;min-width: 400px">
<div style="height: 25px; color: #83d6c0;">
<div style="width: 7%; font-size: 10px; float: left; padding-top: 8px; cursor: pointer;" class="fa fa-minus-circle" ></div>
<div style="width: 50%; float: left; padding-right: 10px;">
<div class="gov-team-text" #click="editTeam(team)">{{ team.team.name }}</div>
</div>
<div style="width: auto; float: right;">
<b-badge variant="light">{{ team.role }}</b-badge>
</div>
<div style="width: 45%; margin-left: 4%; float: right;">
</div>
</div>
<div style="clear: both; margin-bottom: 15px;"></div>
</div>
</div>
</div>
</template>
The v-if here throws
Cannot read property 'length' of undefined
while the v-for works.
And when I'm trying to use the props in a child method(loadData()) which is called on child component mount hook, I get also undefined
<script>
import Treeselect from '#riophae/vue-treeselect';
import _ from "lodash";
export default {
name: "BusinessContextTeamPanel",
components: { Treeselect },
props: ['business_contexts'],
data() {
return {
itemModel: {},
allRoles: [],
formTitle: "",
filteredTeams: [],
showNoTeamMsg: false
}
},
mounted() {
this.loadData();
},
methods: {
loadData() {
let roles = [];
_.forEach(["Admin", "Contributor", "Owner", "Master", "Viewer", "User"], function (role) {
roles.push({id: role, label: role});
});
console.log(this.business_contexts); // here I get undefined
this.allRoles = roles;
this.showNoTeamMsg = this.business_contexts.length === 0;
}
}
}
</script>
What am I missing?
Either your apollo query isn't working, or it isn't ready in time for the child component's attempted use of it.
For debugging, in the parent component's template add {{ business_contexts }}, just so you can eyeball it. This will confirm whether the query is working.
If the query is working, it could be that your child component is trying to access the data before it's ready. Change your v-if from
<div v-if="business_contexts.length > 0">
to
<div v-if="business_contexts && business_contexts.length > 0">
EDIT: better yet, change the parent component to not try to load the child at all, until the parent has the data:
<BusinessContextTeamPanel v-if="business_contexts" :business_contexts="business_contexts"></BusinessContextTeamPanel></BusinessContextTeamDetailPanel>