Setting props for components using v-for in Vue JS - javascript

I have a PhoneCard.vue component that I'm trying to pass props to.
<template>
<div class='phone-number-card'>
<div class='number-card-header'>
<h4 class="number-card-header-text">{{ cardData.phone_number }}</h4>
<span class="number-card-subheader">
{{ cardData.username }}
</span>
</div>
</div>
</template>
<script>
export default {
props: ['userData'],
components: {
},
data() {
return {
cardData: {}
}
},
methods: {
setCardData() {
this.cardData = this.userData;
console.log(this.cardData);
}
},
watch: {
userData() {
this.setCardData();
}
}
}
The component receives a property of userData, which is then being set to the cardData property of the component.
I have another Vue.js component that I'm using as a page. On this page I'm making an AJAX call to an api to get a list of numbers and users.
import PhoneCard from './../../global/PhoneCard.vue';
export default {
components: {
'phone-card': PhoneCard
},
data() {
return {
phoneNumbers: [],
}
},
methods: {
fetchActiveNumbers() {
console.log('fetch active num');
axios.get('/api').then(res => {
this.phoneNumbers = res.data;
}).catch(err => {
console.log(err.response.data);
})
}
},
mounted() {
this.fetchActiveNumbers();
}
}
Then once I've set the response data from the ajax call equal to the phoneNumbers property.
After this comes the issue, I try to iterate through each number in the phoneNumber array and bind the value for the current number being iterated through to the Card's component, like so:
<phone-card v-for="number in phoneNumbers" :user-data="number"></phone-card>
However this leads to errors in dev tools such as property username is undefined, error rendering component, cannot read property split of undefined.
I've tried other ways to do this but they all seem to cause the same error. any ideas on how to properly bind props of a component to the current iteration object of a vue-for loop?

Try
export default {
props: ['userData'],
data() {
return {
cardData: this.userData
}
}
}

Answered my own question, after some tinkering.
instead of calling a function to set the data in the watch function, all I had to do was this to get it working.
mounted() {
this.cardData = this.userData;
}
weird, I've used the watch method to listen for changes to the props of components before and it's worked flawlessly but I guess there's something different going on here. Any insight on what's different or why it works like this would be cool!

Related

How to get the props value and use in lifehooks cycle of Vue JS

I'm searching a way to get the props value through some lifehooks like mounted or updated and trying to save the value with my v-model with some string. But I can't get it.
Though I tried :value on the input element with the props value and some string and I was able to get it, but it seems like I can't access it without v-model, as I researched v-model and :value can't be together.
The purpose is to get the value(with from props and some string) of a input tags.
Parent Component
<invite :user_token="user_token"/>
Child Component
export default {
props: ['user_token'],
data() {
return {
link: ''
}
},
mounted() {
console.log(this.user_token);
this.link = `"http://localhost/johndoe-vue/public/#/invite${this.user_token}"`;
},
updated() {
console.log(this.user_token);
this.link = `"http://localhost/johndoe-vue/public/#/invite${this.user_token}"`;
}
}
Welcome to SO Nigel!
Are you looking for something like this, perhaps?
ParentComponent.vue
<template>
<div id="wrapper">
<invite :userToken="userToken"></invite>
</div>
</div>
<script>
import Invite from "#/Invite.vue";
export default {
components: {
Invite
},
data() {
return {
userToken: "fooBar",
};
}
}
</script>
ChildComponent.vue
<template>
<div id="wrapper">
<p v-if="inviteLink != ''">{{ inviteLink }}</p>
</div>
</template>
export default {
props: {
userToken: {
type: String,
}
},
data() {
return {
inviteLink: ""
}
},
created() {
if(this.userToken != "") {
this.inviteLink == "/your-link-here/"+this.userToken;
}
}
}
Also, you should check out the Vue.js Style Guide. They've marked multi-word component names as essential. Your Invite component should be renamed to BaseInvite or something like that.
Have you tried to $emit this.link
Props is accessible through the $props property of your component. You would reference it like: this.$props.[property name]. $props is called an instance property; there are many of them and they are each accessible this way. See https://v2.vuejs.org/v2/api/#Instance-Properties
Keep in mind that the Vue life cycle methods are somewhat inconsistent. Which instance properties are accessible depends on the method (ie: you can't reference $el in created(...).
https://v2.vuejs.org/v2/guide/instance.html#Lifecycle-Diagram

Vue component doesn't update when state in store is updated

I'm trying to do a simple todo list in Vue but I want to abstract everything out and use a dummy REST API so I can start to get used to production-level projects in Vue and it's all making my head spin. GET, PUT, and POST requests seem to be working, but I can't figure out why the list of todos doesn't update automatically when I do a successful POST request to the back end.
I've got a TodoList component that loops through a todosFiltered() computed property to show the todos. The computed property refers back to the getter todosFiltered in the Vuex store. I also use the created() lifecycle hook here to dispatch an action in the store that makes the initial GET request and then populates an array called todos in the store when the page is first loaded. The getter todosFiltered in the store returns state.todos, so I assumed that when my component re-renders, it would have the new todos array from the state grabbed from todosFiltered, only that's not happening. What am I missing here? Any advice would be greatly appreciated.
TodoList.vue
(I know I'll have to work out a solution for the ids, it's on my list :p)
<template>
<div class="container">
<input v-model="newTodo" type="text" placeholder="What must be done?" class="todo-input" #keyup.enter="addTodo">
<transition-group name="fade" enter-active-class="animated zoomIn" leave-active-class="animated zoomOut">
<todo-item v-for="todo in todosFiltered" :key="todo.id" :checkAll="!anyRemaining" :todo="todo"></todo-item>
</transition-group>
<div class="extra-container">
<todos-filtered></todos-filtered>
</div>
</div>
</template>
<script>
import TodosFiltered from './TodosFiltered'
import TodoItem from './TodoItem'
export default {
name: 'todolist',
components: {
TodosFiltered,
TodoItem
},
data() {
return {
beforeEditCache: '',
newTodo: '',
idForTodo: 10,
}
},
// Methods
methods: {
addTodo() {
if (this.newTodo.trim().length == 0) {
return
}
this.$store.dispatch('addTodo', {
id: this.idForTodo,
title: this.newTodo,
completed: false
})
this.newTodo = ''
this.idForTodo++
}
},
computed: {
todosFiltered() {
return this.$store.getters.todosFiltered
},
},
created() {
this.$store.dispatch('loadTodos')
},
}
</script>
store.js
export const store = new Vuex.Store({
state: {
filter: 'all',
todos: []
},
getters: {
todosFiltered(state) {
if (state.filter == 'all') {
return state.todos
} else if (state.filter == 'active') {
return state.todos.filter(todo => !todo.completed)
} else if (state.filter == 'completed') {
return state.todos.filter(todo => todo.completed)
}
return state.todos
},
showClearCompleted(state) {
return state.todos.filter(todo => todo.completed).length > 0
}
},
mutations: {
addTodo(state, todo) {
state.todos.push(todo)
},
setTodos(state, todos) {
state.todos = todos
},
},
actions: {
loadTodos(context) {
axios.get('http://localhost:3000/todos')
.then(r => r.data)
.then(todos => {
context.commit('setTodos', todos)
})
},
updateTodo(context, todo) {
axios.put('http://localhost:3000/todos/' + todo.id, {
"id": todo.id,
"title": todo.title,
"completed": todo.completed
})
},
addTodo(context, todo) {
axios.post('http://localhost:3000/todos', {
"id": todo.id,
"title": todo.title,
"completed": todo.completed
})
.then(todo => {
context.commit('addTodo', todo)
})
},
}
})
EDIT: Here's what's going on in Vue Dev Tools when I add a todo -- todos in the store's state gets updated immediately, and the todosFiltered computed property in the TodoList component ALSO reflects that -- but the new todo doesn't appear in the list! Strange.
A way to solve this can be to create what I like to call a refresh() method.
Basically, you will have a local list of todos in your data() method, the refresh() method will load all the todos from the store into the local todos list, every time you do an action, such as creating, deleting, or updating, you would call the refresh method to re-load the list for you.
So, in your TodoList.vue:
<template>
<todo-item v-for="todo in todosFiltered" :key="todo.id"></todo-item>
</template>
<script>
export default {
data() {
return {
// Where we store the local list of Todos
// so the component will react when we do something to it
todosFiltered: []
}
},
methods {
refresh() {
// Get the todo list from the store.
// which in turn will trigger a change event so the component
// react to what we did.
this.todosFiltered = this.$store.getters.todosFiltered;
},
addTodo() {
this.$store.dispatch('addTodo').then(() => {
// Refresh the list after adding a new Todo
this.refresh();
})
},
updateTodo() {
this.$store.dispatch('updateTodo').then(() => {
// Refresh the list after updating a Todo
this.refresh();
})
},
deleteTodo() {
this.$store.dispatch('deleteTodo').then(() => {
// Refresh the list after deleting a Todo
this.refresh();
})
}
},
created() {
this.$store.dispatch('loadTodos').then( () => {
// Refresh the list when first loaded after the Todos been saved in Vuex
this.refresh();
})
}
}
</script>
Don't actually delete what you already have and replace it with
this, just apply what's here to your code.
The problem is using a $store.getter on the v-for loop.
Try the following:
Set your computed to
todos() {
return this.$store.todos;
}
Change your v-for loop to use todo in todos
Add a v-if condition to the loop like v-if="filtered(todo)"
Create a new method called filtered (or whatever you prefer), and add your "filteredTodos" logic there, returning true/false as needed
If you need to share this code, you can always use a mixin and share it between your components
Hope this works for you

Vue2: warning: Avoid mutating a prop directly

I'm stuck in the situation where my child component (autocomplete) needs to update a value of its parent (Curve), And the parent needs to update the one of the child (or to completely re-render when a new Curve component is used)
In my app the user can select a Curve in a list of Curve components. My previous code worked correctly except the component autocomplete was not updated when the user selected another Curve in the list (the component didn't update its values with the value of the parent).
This problem is fixed now but I get this warning:
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:
"value"
The description of this warning explain exactly what behavior I expect from my components. Despite this warning, this code works perfectly fine !
Here is the code (parts of it have been removed to simplify)
// curve.vue
<template>
<autocomplete v-model="curve.y"></autocomplete>
</template>
<script>
import Autocomplete from './autocomplete'
export default {
name: 'Curve',
props: {
value: Object
},
computed: {
curve() { return this.value }
},
components: { Autocomplete }
}
</script>
// autocomplete.vue
<template>
<input type="text" v-model="content"/>
</template>
<script>
export default {
name: 'Autocomplete',
props: {
value: {
type: String,
required: true
}
},
computed: {
content: {
get() { return this.value },
set(newValue) { this.value = newValue }
}
}
}
</script>
A lot of people are getting the same warning, I tried some solutions I found but I was not able to make them work in my situation (Using events, changing the type of the props of Autocomplete to be an Object, using an other computed value, ...)
Is there a simple solution to solve this problem ? Should I simply ignore this warning ?
you can try is code, follow the prop -> local data -> $emit local data to prop flow in every component and component wrapper.
ps: $emit('input', ...) is update for the value(in props) bind by v-model
// curve.vue
<template>
<autocomplete v-model="curve.y"></autocomplete>
</template>
<script>
import Autocomplete from './autocomplete'
export default {
name: 'Curve',
props: {
value: Object
},
data() {
return { currentValue: this.value }
}
computed: {
curve() { return this.currentValue }
},
watch: {
'curve.y'(val) {
this.$emit('input', this.currentValue);
}
},
components: { Autocomplete }
}
</script>
// autocomplete.vue
<template>
<input type="text" v-model="content"/>
</template>
<script>
export default {
name: 'Autocomplete',
props: {
value: {
type: String,
required: true
}
},
data() {
return { currentValue: this.value };
},
computed: {
content: {
get() { return this.value },
set(newValue) {
this.currentValue = newValue;
this.$emit('input', this.currentValue);
}
}
}
}
</script>
You can ignore it and everything will work just fine, but it's a bad practice, that's what vue is telling you. It'll be much harder to debug code, when you're not following the single responsibility principle.
Vue suggests you, that only the component who owns the data should be able to modify it.
Not sure why events solution ($emit) does not work in your situation, it throws errors or what?
To get rid of this warning you also can use .sync modifier:
https://v2.vuejs.org/v2/guide/components.html#sync-Modifier

Error in render function: “TypeError: Cannot read property of undefined”, using Vuex and Vue Router

I have been dealing with an issue using Vue, Vuex and Vue-Router. I'm building a flash cards app, fetching all the cards on main app creation, then using a Vuex getter to get each card by its id which is passed as a route parameter.
Relevant bits:
App.vue
export default {
components: {
'app-header': header,
},
data() {
return {
}
},
created() {
this.$store.dispatch('getAllCards');
}
}
The dispatch('getAllCards') is just pulling all the cards from the DB and committing to Vuex store.js.
Now I set up a getter:
getters: {
cardById: (state) => (id) => {
return state.allCards.find((card) => card._id === id);
}
}
Here is Card.vue:
<template>
<div>
<br>
<div v-if="flipped" class="container">
<div class="box">
<pre v-if="card.code"><code class="preserve-ws">{{card.back}}</code></pre>
<p class="preserve-ws center-vertical" v-else>{{card.back}}</p>
</div>
</div>
<div v-else class="container">
<div class="box">
<h1 class="title has-text-centered center-vertical is-2">{{card.front}}</h1>
</div>
</div>
</template>
<script>
export default {
data() {
return {
card: {},
flipped: false,
general_card: false,
code_card: true,
random_card: false
}
},
computed: {
},
methods: {
},
created() {
this.card = this.$store.getters.cardById(this.$route.params.id);
}
}
</script>
I am getting the TypeError referenced in the title. My understanding is that the created() hook happens after data() has been set up, so then I can assign {card} using the getter. Unfortunately this displays nothing...
If I assign card() as a computed property:
computed: {
card() {
return this.$store.getters.cardById(this.$route.params.id);
}
}
The card shows, but I still get that error in console. Any idea why? I looked at this and attempted that solution, but to no avail.
The question don't have all the premise to get a correct answer.
(We don't know the mutations, the state, neither the actions and we have no clues about app-header component)
So we have to admit some hypothesis :
state.allCards is an empty Array when you mount the component
action getAllCards is an async function to retrieve or set state.allCards with a mutation
cardById's getters, return a function, so vuex can't apply reactive on a function. So if the state change, the getter won't be trigger correctly.
To correct this, use computed as you mention here.
But it don't fix the undefined error, that from my point of view, is because the getters return undefined on mount.
getters: {
cardById: (state) => (id) => {
var card = state.allCards.find((card) => card._id === id);
if(card) {
return card;
}
// on undefined return a default value
return {
front:'default value'
};
}
}
You can see the implementation on this jsfiddle with no error on console.
Or you can have a loading state on undefined value for your card component.
If my hypothesis is wrong please provide a jsfiddle to help you.
What you need is a derived state based on store state which is to return a filtered card based on a card id. This id is received to your component via the route params. So its better you use a computed property instead of passing arguments to the store getters
Instead of initializing card in data property make card a computed property like this:
computed:{
card(){
return this.$store.state.allCards.find((card) => card._id === this.$route.params.id);
}
}
Note this
If a component needs derived store state based on its own state(in your case rourte params), it should define a local computed property
I tried everyone else's solutions and they did not work. But I got it to work finally. Here is what worked for me:
I ended up including a top-level:
<div v-if="!card"> Loading card... </div>
<div v-else> Rest of card template </div>
That seems to have silenced the error. Also, the card lives as a computed property:
card() {
return this.$store.getters.cardById(this.$route.params.id);
}
I think it was throw error in this step
getters: {
cardById: (state) => (id) => {
return state.allCards.find((card) => card._id === id);
}
}
in this step, it can not find _id of cars, because allcards was null;
and then you use computed instead, when allcards has been change, it will get again;
you can change code like this
getters: {
cardById: (state) => (id) => {
return state.allCards.find((card) => card && card._id === id);
}
}
$route.params.id must be string, so what is the type of card._id? It seems each card._id is number, I think.

Vuex how to Access state data on component when it's mounted?

I'm trying to create a Quill.js editor instance once component is loaded using mounted() hook. However, I need to set the Quill's content using Quill.setContents() on the same mounted() hook with the data I received from vuex.store.state .
My trouble here is that the component returns empty value for the state data whenever I try to access it, irrespective of being on mounted() or created() hooks. I have tried with getters and computed properties too. Nothing seems to work.
I have included my entry.js file, concatenated all the components to make things simpler for you to help me.
Vue.component('test', {
template:
`
<div>
<ul>
<li v-for="note in this.$store.state.notes">
{{ note.title }}
</li>
</ul>
{{ localnote }}
<div id="testDiv"></div>
</div>
`,
props: ['localnote'],
data() {
return {
localScopeNote: this.localnote,
}
},
created() {
this.$store.dispatch('fetchNotes')
},
mounted() {
// Dispatch action from store
var quill = new Quill('#testDiv', {
theme: 'snow'
});
// quill.setContents(JSON.parse(this.localnote.body));
},
methods: {
setLocalCurrentNote(note) {
console.log(note.title)
return this.note = note;
}
}
});
const store = new Vuex.Store({
state: {
message: "",
notes: [],
currentNote: {}
},
mutations: {
setNotes(state,data) {
state.notes = data;
// state.currentNote = state.notes[1];
},
setCurrentNote(state,note) {
state.currentNote = note;
}
},
actions: {
fetchNotes(context) {
axios.get('http://localhost/centaur/public/api/notes?notebook_id=1')
.then( function(res) {
context.commit('setNotes', res.data);
context.commit('setCurrentNote', res.data[0]);
});
}
},
getters: {
getCurrentNote(state) {
return state.currentNote;
}
}
});
const app = new Vue({
store
}).$mount('#app');
And here is the index.html file where I'm rendering the component:
<div id="app">
<h1>Test</h1>
<test :localnote="$store.state.currentNote"></test>
</div>
Btw, I have tried the props option as last resort. However, it didn't help me in anyway. Sorry if this question is too long. Thank you for taking your time to read this. Have a nice day ;)
I will recommend the following steps to debug the above code:
In the Vue dev tools, check if the states are getting set after the network call
As you are trying to fetch data asynchronously, there can be a chance that data has not arrived when created/mounted hook is called.
Add an updated hook into your component and try to log or access the state and you should be able to see it.
Please provide the results from the above debugging, then I'll be able to add more details.

Categories

Resources