Vue DOM not reactive to computed property - vue.js

I have a button that is set to be disabled if a computed property's valid property is false. If true, the button should be enabled and allow a user to move to the next step in the current flow.
My currentStep computed property is updating perfectly on changes to the current step's inputs, but the button :disabled="currentStep.valid" isn't recognizing the changes that ARE HAPPENING to currentStep.valid.
If I click on the current component (addnewprogram) in vue-devtools to see it's data, the button displays correctly!
Seen here: http://recordit.co/XH6HX7JLhV
However, without clicking on addnewprogram in the devtools, it doesn't function correctly.
Is there an obvservation caveat I'm missing?
The code for this functionality can be found here:
<template>
<section class="newprogram">
<div class="newprogram--content">
<div class="newprogram--stepone" v-if="progression.current === 1">
<div class="content--left">
<a class="link uppercase">use existing program information<i class="fa fa-picture-o"></i></a>
<base-input v-for="input in currentStep.inputs"
:key="input.id"
:data="input"
v-model="input.value"></base-input>
</div>
<div class="content--right">
<!-- images -->
</div>
</div>
<div class="newprogram--steptwo" v-if="progression.current === 2">
<choice-progression :step="1"></choice-progression>
</div>
</div>
</div>
<!-- Consistent among all steps -->
<div class="container--bottomnav">
<div class="bottomnav--left">
<base-btn class="button-fluid"
:data="currentStep.btns[0]"></base-btn>
</div>
<div class="bottomnav--right">
<base-btn :data="currentStep.btns[1]"
:disabled="currentStepValid"></base-btn>
</div>
</div>
<!-- -->
</section>
</template>
<script>
import bottomNav from '../main/bottom-nav.vue';
import baseProgressionBarbell from '../base/base-progression-barbell.vue';
import baseInstruction from '../base/base-instruction.vue';
import baseInput from '../base/base-input.vue';
import baseBtn from '../base/base-btn.vue';
import choiceProgression from '../secondary-flows/choice-progression.vue';
export default {
name: 'addNewProgram',
components: {
bottomNav,
baseProgressionBarbell,
baseInstruction,
baseInput,
baseBtn,
choiceProgression
},
computed: {
progression () {
return this.$store.getters.getProgression('addnewprogram');
},
steps () {
return this.$store.getters.getSteps('addnewprogram');
},
currentStep () {
return this.steps[this.progression.current - 1];
},
currentStepValid () {
return this.currentStep.valid == false ? true : false;
},
stepOneValidation () {
this.steps[0].inputs.forEach(input => {
if (!input.value) {
return this.$set(this.steps[0], 'valid', false);
}
this.$set(this.steps[0], 'valid', true);
});
},
stepTwoChoices() {
return this.$store.getters.getChoices('addnewprogram', 1);
}
}
}
</script>
<style lang="sass" scoped>
#import '../../sass/_variables.sass'
.newprogram
display: flex
flex-direction: column
.container--newprogram
display: flex
flex-direction: column
height: 100%
padding: $s1
.newprogram--header
display: flex
justify-content: space-between
align-items: center
h1
padding: 0
.newprogram--content
display: flex
// justify-content: center
// align-items: center
height: 100%
padding-top: $s2
</style>

You are updating members of a computed item. Computed items aren't editable. You need a data item, or you need to write your changes to the $store and have them refresh from there.

Related

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>

Nuxt - Using router.push inside a component not changing pages correctly

index.vue -
<template>
<div>
<Header />
<div class="container">
<SearchForm />
</div>
</div>
</template>
<script>
const Cookie = process.client ? require('js-cookie') : undefined
export default {
data() {
return {
form: {
email: '',
password: ''
},
show: true
}
},
methods: {
logout() {
// Code will also be required to invalidate the JWT Cookie on external API
Cookie.remove('auth')
this.$store.commit('setAuth', {
auth: null,
user_type: null
})
}
}
}
</script>
<style>
.container {
/* margin: 0 auto; */
/* min-height: 100vh; */
display: flex;
justify-content: center;
align-items: center;
text-align: center;
}
</style>
jobs.vue -
<template>
<div>
<Header />
<SearchForm />
<b-container class="main_container">
<b-row>
<h1> Results for "{{q}}"</h1>
</b-row>
<b-row>
<ul id="array-rendering">
<li v-for="item in results" :key="item.job_id">
{{ item.job_title }}
{{ item.job_city }}
{{ item.job_state }}
{{ item.job_work_remote }}
</li>
</ul>
</b-row>
</b-container>
</div>
</template>
<script>
const Cookie = process.client ? require('js-cookie') : undefined
export default {
// middleware: 'notAuthenticated',
watchQuery: ['q'],
data() {
return {
q: null,
results: []
}
},
async fetch() {
this.q = this.$route.query.q
this.results = await this.$axios.$get('/api/job/search', {
params: {
keyword: this.q,
}
})
},
methods: {
}
}
</script>
<style>
.container {
align-items: center;
text-align: center;
}
</style>
SearchForm.vue component -
<template>
<div id='searchFormDiv'>
<b-form inline #submit.prevent="onSubmit">
<label class="sr-only" for="inline-form-input-name"> keyword</label>
<b-form-input v-model="form.keyword" id="inline-form-input-name" class="mb-2 mr-sm-2 mb-sm-0" placeholder="Job title or keyword" size="lg"></b-form-input>
<label class="sr-only" for="inline-form-input-username">location</label>
<b-input-group class="mb-2 mr-sm-2 mb-sm-0">
<b-form-input v-model="form.location" id="inline-form-input-username" size="lg" placeholder="City, state or zip"></b-form-input>
</b-input-group>
<b-button type="submit" variant="primary">Find Jobs</b-button>
</b-form>
</div>
</template>
<script>
import {
BIconSearch,
BIconGeoAlt
} from 'bootstrap-vue'
export default {
data() {
return {
form: {
keyword: '',
location: ''
}
}
},
created () {
this.form.keyword = this.$route.query.q
},
methods: {
onSubmit() {
this.$router.push({
path: 'jobs',
query: {
q: this.form.keyword
}
});
}
},
components: {
BIconSearch,
BIconGeoAlt
},
}
</script>
<style>
#searchFormDiv {
margin-top: 50px
}
</style>
The route for "http://localhost:3000/" returns the index.vue page.
In this vue page, I have a component with a search form. Once you complete these form and hit the seach button, it will re-direct to a results page
if this.form.keyword = "Data", the next URL will be "http://localhost:3000/jobs?q=Data" and it will be using the jobs.vue page.
The issue I'm running into is the CSS is not being loaded from the jobs.vue page. It's still coming from the index.vue page for some reason. If I refresh the page, then the CSS from jobs.vue is loading. I need the CSS to load from jobs.vue on the initial redirect. All of the query data is working as expected so thats a plus.
However, the following CSS is being applied from index.vue for some reason instead of the CSS from the jobs.vue page -
display: flex;
justify-content: center;
Does anyone know whats going on here? This app is SSR and not SPA.
You have to scope your css from the index.vue page to the other pages with the scoped directive (see docs https://vue-loader.vuejs.org/guide/scoped-css.html)
<style scoped>
/* local styles */
</style>
<style>
/* global styles */
</style>
You can add your global CSS in your layouts/default.vue file.
This solved the issue -
methods: {
onSubmit() {
window.location = 'http://localhost:3000/jobs?q=' + this.form.keyword;
}
},

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>

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

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

Hide div when clicking the main div, but not when clicking child divs, hrefs, spans etc

So, when you click the blue content, I want to hide it, but not if you click a child element, like a h2, a, p, etc. Is this possible?
Please see the fiddle
html
<html>
<head></head>
<body>
<div id="app">
<div v-if="modal" id="content" #click="toggle">
<h3>
Name
</h3>
<span>
Like this (don't close if clicked)
</span>
</div>
<button #click="toggle">Toggle</button>
</div>
</body>
</html>
.js
new Vue({
el: '#app',
data: {
modal: true
},
methods: {
toggle: function( ){
this.modal = !this.modal
}
}
});
.css
a {
color: white
}
#content {
width: 300px;
height: 300px;
background: blue;
color: white;
}
Fiddle: http://jsfiddle.net/awu752mr/6/
I believe that is what the v-on:click.self modifier does. See here in the docs.
As #LShapz answered, the modifier=.self should be the recommended approach (Vue Style).
Another way is compare $event.target === $event.currentTarget (not recommended for your use case, but you may use these two properties for some other situations in future).
Vue.config.productionTip = false
new Vue({
el: '#app',
data: {
modal: true
},
methods: {
toggle: function(ev){
if(ev.target === ev.currentTarget) this.modal = !this.modal
}
}
});
a {
color: white
}
#content {
width: 300px;
height: 300px;
background: blue;
color: white;
}
<script src="https://unpkg.com/vue#2.5.16/dist/vue.js"></script>
<div id="app">
<div v-if="modal" id="content" #click="toggle($event)">
<h3>
Name
</h3>
<span>
Like this
</span>
</div>
<button #click="toggle">Toggle</button>
</div>