Cannot read property 'length' of undefined" - vue.js

I am getting the error below. The weird part is that I'm positive the data is there because in my vue add on I can see that it successfully grabs the information from the vuex store. My initial guess is that somehow the data is not yet grabbed from the store, at the point that it creates the template?
Vue warn]: Error in render: "TypeError: Cannot read property 'length' of undefined"
The data: 'spaces' is grabbed from the store.
export default {
name: "myspaces",
data() {
return {
filterMaxLength: 3,
selectedSpace: 0,
selectedRoom: 0
}
},
created() {
// Default selected space (first in json)
this.selectedSpace = this.spaces[0].id;
// Default selected room (first in json)
this.selectedRoom = this.spaces[0].rooms[0].id;
},
computed: {
// Get 'spaces' from store.
...mapState([
'spaces'
])
}
Template:
<template>
<div>
<v-flex v-if="spaces.length < filterMaxLength">
<v-btn v-for="space in spaces">
<h4> {{space.name}} </h4>
</v-btn>
</v-flex>
</div>
<template>
The store:
import Vuex from 'vuex'
Vue.use(Vuex);
export default new Vuex.Store({
state: {
spaces:[
{
id:1,
name:'House in Amsterdam',
rooms:[
{
id:1,
name:'Bedroom Otto',
},
{
id:2,
name:'Bedroom Mischa'
}
]
},
{
id:2,
name:'Office in Amsterdam',
rooms:[
{
id:1,
name:'Office 1',
},
{
id:2,
name:'Office 2'
}
]
}
]} });
The vue chrome add on says this information is in the component:

Always before check length, be sure your property its set then check length
<v-flex v-if="spaces && spaces.length < filterMaxLength">
Update ECMAScript 2020
You can use Optional chaining for this purpose too
<v-flex v-if="spaces?.length < filterMaxLength">

You should use Object.keys(spaces).length, such as:
<template>
<div>
<v-flex v-if="typeof spaces !== 'undefined' && typeof spaces === 'object' && Object.keys(spaces).length < filterMaxLength">
<v-btn v-for="space in spaces">
<h4> {{space.name}} </h4>
</v-btn>
</v-flex>
</div>
<template>

just to be sure you have following in your vue
import { mapState } from "vuex";
Else, you can use getters also, eg. :
In your vue file
v-if="this.getSpaces.length !== 0"
In computed fonctions of your vue file
getSpaces() {
return this.$store.getters.getSpaces;
}
In your store
getters: {
getSpaces: state => {
return state.spaces;
}
},

I also had this problem, I imported a sass style sheet with functions(#mixins) and a sass style sheet with variables into one sass style sheet.
I imported them as
(
#import url('./_variables.scss');
#import url('./_func.scss');
)
so i just took out the url() for both import statements and that error went away.

Related

Limiting the two way binding with vuejs / variable values change when modifying another variable

I have a 'client-display' component containing a list of clients that I get in my store via mapGetter. I use 'v-for' over the list to display all of them in vuetify 'v-expansion-panels', thus one client = one panel. In the header of those panels, I have a 'edit-delete' component with the client passed to as a prop. This 'edit-delete' basically just emits 'edit' or 'delete' events when clicked on the corresponding icon with the client for payload. When I click on the edit icon, the edit event is then catched in my 'client-display' so I can assign the client to a variable called 'client' (sorry I know it's confusing a bit). I pass this variable to my dialog as a prop and I use this dialog to edit the client.
So the probleme is : When I edit a client, it does edit properly, but if I click on 'cancel', I find no way to revert what happened in the UI. I tried keeping an object with the old values and reset it on a cancel event, but no matter what happens, even the reference values that I try to keep in the object change, and this is what is the most surprising to me. I tried many things for this, such as initiating a new object and assigning the values manually or using Object.assign(). I tried a lot of different ways to 'unbind' all of this, nothing worked out. I'd like to be able to wait for the changes to be commited in the store before it's visible in the UI, or to be able to have a reference object to reset the values on a 'cancel' event.
Here are the relevant parts of the code (I stripped a lot of stuff to try and make it easier to read, but I think everything needed is there):
Client module for my store
I think this part works fine because I get the clients properly, though maybe something is binded and it should not
const state = {
clients: null,
};
const getters = {
[types.CLIENTS] : state => {
return state.clients;
},
};
const mutations = {
[types.MUTATE_LOAD]: (state, clients) => {
state.clients = clients;
},
};
const actions = {
[types.FETCH]: ({commit}) => {
clientsCollection.get()
.then((querySnapshot) => {
let clients = querySnapshot.docs.map(doc => doc.data());
commit(types.MUTATE_LOAD, clients)
}).catch((e) => {
//...
});
},
}
export default {
state,
getters,
mutations,
...
}
ClientsDisplay component
<template>
<div>
<div>
<v-expansion-panels>
<v-expansion-panel
v-for="c in clientsDisplayed"
:key="c.name"
>
<v-expansion-panel-header>
<div>
<h2>{{ c.name }}</h2>
<edit-delete
:element="c"
#edit="handleEdit"
#delete="handleDelete"
/>
</div>
</v-expansion-panel-header>
<v-expansion-panel-content>
//the client holder displays the client's info
<client-holder
:client="c"
/>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
</div>
<client-add-dialog
v-model="clientPopup"
:client="client"
#cancelEdit="handleCancel"
/>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import * as clientsTypes from '../../../../store/modules/clients/types';
import ClientDialog from './ClientDialog';
import EditDelete from '../../EditDelete';
import ClientHolder from './ClientHolder';
import icons from '../../../../constants/icons';
export default {
name: 'ClientsDisplay',
components: {
ClientHolder,
ClientAddDialog,
EditDelete,
},
data() {
return {
icons,
clientPopup: false,
selectedClient: null,
client: null,
vueInstance: this,
}
},
created() {
this.fetchClients();
},
methods: {
...mapGetters({
'stateClients': clientsTypes.CLIENTS,
}),
...mapActions({
//this loads my clients in my state for the first time if needed
'fetchClients': clientsTypes.FETCH,
}),
handleEdit(client) {
this.client = client;
this.clientPopup = true;
},
handleCancel(payload) {
//payload.uneditedClient, as defined in the dialog, has been applied the changes
},
},
computed: {
isMobile,
clientsDisplayed() {
return this.stateClients();
},
}
}
</script>
EditDelete component
<template>
<div>
<v-icon
#click.stop="$emit('edit', element)"
>edit</v-icon>
<v-icon
#click.stop="$emit('delete', element)"
>delete</v-icon>
</div>
</template>
<script>
export default {
name: 'EditDelete',
props: ['element']
}
</script>
ClientDialog component
Something to note here : the headerTitle stays the same, even though the client name changes.
<template>
<v-dialog
v-model="value"
>
<v-card>
<v-card-title
primary-title
>
{{ headerTitle }}
</v-card-title>
<v-form
ref="form"
>
<v-text-field
label="Client name"
v-model="clientName"
/>
<address-fields
v-model="clientAddress"
/>
</v-form>
<v-card-actions>
<v-btn
#click="handleCancel"
text
>Annuler</v-btn>
<v-btn
text
#click="submit"
>Save</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
import AddressFields from '../../AddressFields';
export default {
name: 'ClientDialog',
props: ['value', 'client'],
components: {
AddressFields,
},
data() {
return {
colors,
clientName: '',
clientAddress: { province: 'QC', country: 'Canada' },
clientNote: '',
uneditedClient: {},
}
},
methods: {
closeDialog() {
this.$emit('input', false);
},
handleCancel() {
this.$emit('cancelEdit', { uneditedClient: this.uneditedClient, editedClient: this.client})
this.closeDialog();
},
},
computed: {
headerTitle() {
return this.client.name
}
},
watch: {
value: function(val) {
// I watch there so I can reset the client whenever I open de dialog
if(val) {
// Here I try to keep an object with the value of this.client before I edit it
// but it doesn't seem to work as I intend
Object.assign(this.uneditedClient, this.client);
this.clientName = this.client.name;
this.clientContacts = this.client.contacts;
this.clientAddress = this.client.address;
this.clientNote = '';
}
}
}
}
</script>
To keep an independent copy of the data, you'll want to perform a deep copy of the object using something like klona. Using Object.assign is a shallow copy and doesn't protect against reference value changes.

Vue mutating a property that is used as "v-model"

Many issue like this but I cannot find my way. I simplified the code to explain my problem...
Just wanted a reusable feedback message to show with the result of the rest api using vuetity andsnackbar widget.
In the parent component:
<Feedback :active="hasFeedback" :msg="feedbackMsg" />
The Feedback component:
<template>
<v-snackbar v-model="active" >
{{ msg }}
<v-icon #click="active = false">mdi-close-thick</v-icon>
</v-snackbar>
</template>
<script>
export default {
components: {},
props: ["active", "msg"]
};
</script>
I tried to add computed property, methods, getter, setter but always got an error.
Just copy your active prop to some prop in a data section of a component and use and change that prop in the data section.
<template>
<v-snackbar v-model="isActive" >
{{ msg }}
<v-icon #click="isActive = false">mdi-close-thick</v-icon>
</v-snackbar>
</template>
<script>
export default {
components: {},
props: {
active: {
type: Boolean,
default: false
}, {
msg: {
type: String
}
},
data () {
return {
isActive: this.active
}
}
};
</script>

Element UI dialog component can open for the first time, but it can't open for the second time

I'm building web app with Vue, Nuxt, and Element UI.
I have a problem with the Element dialog component.
It can open for the first time, but it can't open for the second time.
This is the GIF about my problem.
https://gyazo.com/dfca3db76c75dceddccade632feb808f
This is my code.
index.vue
<template>
<div>
<el-button type="text" #click="handleDialogVisible">click to open the Dialog</el-button>
<modal-first :visible=visible></modal-first>
</div>
</template>
<script>
import ModalFirst from './../components/ModalFirst.vue'
export default {
components: {
'modal-first': ModalFirst
},
data() {
return {
visible: false,
};
},
methods: {
handleDialogVisible() {
this.visible = true;
}
}
}
</script>
ModalFirst.vue
<template>
<el-dialog
title="Tips"
:visible.sync="visible"
width="30%"
>
<span>This is a message</span>
<span slot="footer" class="dialog-footer">
<a>Hello</a>
</span>
</el-dialog>
</template>
<script>
export default {
props: [ 'visible' ]
}
</script>
And I can see a warning message on google chrome console after closing the dialog.
The warning message is below.
webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:620 [Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "visible"
found in
---> <ModalFirst> at components/ModalFirst.vue
<Pages/index.vue> at pages/index.vue
<Nuxt>
<Layouts/default.vue> at layouts/default.vue
<Root>
This is the screenshot of the warning message.
https://gyazo.com/83c5f7c5a8e4d6816c35b3116c80db0d
In vue , using directly to prop value is not allowed . Especially when your child component will update that prop value , in my option if prop will be use
for display only using directly is not a problem .
In your code , .sync will update syncronously update data so I recommend to create local data.
ModalFirst.vue
<el-dialog
title="Tips"
:visible.sync="localVisible"
width="30%"
>
<script>
export default {
props: [ 'visible' ],
data: function () {
return {
localVisible: this.visible // create local data using prop value
}
}
}
</script>
If you need the parent visible property to be updated, you can create your component to leverage v-model:
ModalFirst.vue
<el-dialog
title="Tips"
:visible.sync="localVisible"
width="30%"
>
<script>
export default {
props: [ 'value' ],
data() {
return {
localVisible: null
}
},
created() {
this.localVisible = this.value;
this.$watch('localVisible', (value, oldValue) => {
if(value !== oldValue) { // Optional
this.$emit('input', value); // Required
}
});
}
}
</script>
index.vue
<template>
<div>
<el-button type="text" #click="handleDialogVisible">click to open the Dialog</el-button>
<modal-first v-model="visible"></modal-first>
</div>
</template>
<script>
import ModalFirst from './../components/ModalFirst.vue'
export default {
components: {
'modal-first': ModalFirst
},
data() {
return {
visible: false,
};
},
methods: {
handleDialogVisible() {
this.visible = true;
}
}
}
</script>
v-model is basically a shorthand for :value and #input
https://v2.vuejs.org/v2/guide/forms.html#Basic-Usage
Side-note:
You can also import your component like so:
components: { ModalFirst },
as ModalFirst will be interpreted as modal-first as well by Vue.js

Vue closing component returns avoid mutating a prop directly

I have an component that I want to use on different pages. Well, it is working well till the first toggle. It shows like it used to, but when I click the 'Close' button, it closes, but console outputs :
[Vue warn]: Avoid mutating a prop directly since the value will be
overwritten whenever the parent component re-renders. Instead, use a
data or computed property based on the prop's value. Prop being
mutated: "visible"
found in
---> at src/components/Snackbar.vue
at src/views/Login.vue
And after that it doesn't show up on click
Any way to fix this?
Snackbar.vue
<template>
<v-snackbar v-model.sync="visible" :timeout="5000" bottom>
{{ content }}
<v-btn flat color="primary" #click.native="visible = false">Close</v-btn>
</v-snackbar>
</template>
<script>
export default {
name: 'snackbar',
props: [
'visible',
'content'
]
}
</script>
Login.vue
<template>
<div class="login">
<Snackbar :visible="snackbar.visible" :content="snackbar.content"></Snackbar>
</div>
</template>
<script>
import Snackbar from '#/components/Snackbar.vue'
export default {
components: {
Snackbar
},
data: function() {
return {
email: '',
password: '',
snackbar: {
visible: false,
content: ''
}
}
},
methods: {
login: function() {
if (this.email != '' && this.password != '') {
// Do something
} else {
this.snackbar.content = 'Fields can\'t be empty';
this.snackbar.visible = true;
}
}
}
}
</script>
The console error is being triggered by this:
#click.native="visible = false"
The component is directly mutating the incoming prop. If you want to keep this level of control where the parent component controls the visibility you'll have to do it by having the click event emit an event, which the parent component receives and sets this.snackbar.visible = false thereby triggering a prop change and the child component is hidden.
<Snackbar :visible="snackbar.visible" :content="snackbar.content"
v-on:requestClose="close"></Snackbar>
<v-btn flat color="primary" #click.native="$emit('requestClose')">Close</v-btn>
methods: {
close: function() {
this.snackbar.visible = false;
}
}

Vuex - changing value based on component's current index and passing the value to other components

I have hit a wall and cannot get over it without your help now. I've spent a good few days trying to get my head around mutations and actions but this particular case I have doesn't seem to apply to tutorials online, also the answers on here are for different scenarios to mine. So here goes:
Setup:
- Project is using vuex and store for data and state management
- Currently App.vue has two child components: PizzaGenerator.vue and BaseButtons.vue
I am trying to achieve this:
- When I click on a specific button in BaseButtons.vue I need a centrally managed showBaseIndex to be assigned an index value. This value is then available to the other, PizzaGenerator.vue, component which will reflect the change and show a layer that matches the new value of showBaseIndex.
Please see all the two components and store below.
Can you help me head in the right direction?
PizzaGenerator.vue
<template>
<div class="pizza-generator section" id="screen3" data-anchor="screenThree">
<ul class="pizza-layers">
<!-- Display pizzas -->
<li v-for="(item, index) in getBase" class="pizza-canvas pizza-canvas--base" v-bind:class="item.class" v-if="$store.state.showBaseIndex == index"></li>
<!-- END Display pizzas -->
</ul>
<div class="pizza-items">
<app-base-buttons></app-base-buttons>
</div>
</div>
</template>
<script>
import Base from './pizza-buttons/BaseButtons'
import { mapGetters, mapActions } from 'vuex'
export default {
components: {
appBaseButtons: Base
},
computed: {
getBase(){
return this.$store.state.base
}
},
methods: mapActions([
'baseLayer',
]),
}
</script>
BaseButtons.vue
<div class="pizza-items-selector pizza-items-selector--dough">
<div class="pizza-items-selector__items pizza-items-selector__items--dough">
<div class="sliding-buttons">
<button v-for="(item, index) in getBase" class="sliding-buttons__button dough-button" :key="index" #click="baseLayer = index"> {{ item.name }}</button>
</div>
<button class="buttons-prev-1 prev">prev</button>
<button class="buttons-next-1 next">next</button>
</div>
</div>
<script>
import { mapActions, mapMutations } from 'vuex'
export default {
computed:{
getBase(){
return this.$store.state.base
},
},
methods:{
...mapMutations([
'baseLayer',
]),
baseLayerIndex() {
this.$store.commit('baseLayer');
}
},
}
store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export const store = new Vuex.Store({
state: {
showBaseIndex: 1,
base : [
{ name: 'Dünner Italienisch', class: 'pizza-canvas--base-italienisch', id: 1 },
{ name: 'Dünner Italienisch - Vollkorn', class: 'pizza-canvas--base-italienisch--vollkorn', id: 2 },
{ name: 'Dicker Amerikanisch', class: 'pizza-canvas--base-amerikanisch', id: 3 },
{ name: 'Dicker Amerikanisch / Chili-Käse-Rand', class: 'pizza-canvas--base-amerikanisch--chilli-kaese-rand', id: 4 },
{ name: 'Dicker Amerikanisch / Käse-Rand', class: 'pizza-canvas--base-amerikanisch--kaese-rand', id: 5 }
],
},
getters: {
//
},
mutations: {
baseLayer (state){
state.showBaseIndex
}
},
});
export default store;
Mutations are functions, not simple values. You should check the Vuex guide about mutations, they are quite straightforward.
What you should do is declaring the given mutation in this way, so it will also accept a parameter:
mutations: {
baseLayer (state, id){
state.showBaseIndex = id;
}
},
and commit the mutation properly in the component:
methods:{
...mapMutations([
'baseLayer',
]),
baseLayerIndex(index) { // call this at #click on button, giving the index as parameter
this.$store.commit('baseLayer', index);
}
}
This will set the desired index in the store, and from that on you could get the current base from store using vuex getters like:
getters: {
getSelectedBase(state){
return state.base.find(base=>{return base.id === state.showBaseIndex});
}
},