There are very similar questions to the one am asking but the solutions proposed don't my situation.
I am trying to access data from a parent list in a different component using a Modal component in vue.
I have tried passing the prop value in the loop as well as the used component in the parent view but receive no data.
This is the parent template.
<template>
<table class="table table-bordered table-stripped" v-if="users.length>0">
<caption>List of Contacts</caption>
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Name</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody>
<tr v-for="(user, index) in users" :key="user.id">
<td>{{index+1}}</td>
<td>{{user.name}}</td>
<td>
<button type="button" class="btn btn-success btn-sm" #click="initEdit(user)" :euser="user">Edit</button>
</td>
</tr>
</tbody>
</table>
<edit-user v-if="showEditUser" :showEditUser.sync="showEdit"></edit-user>
</template>
<script>
import editUser from '#/components/editUser.vue';
export default {
name: 'listusers',
components: {
'edit-user': editUser,
},
data() {
return {
user: [],
users: [],
euser: {},
showEdit: false,
};
},
methods: {
initEdit() {
this.showEditUser = true;
},
},
};
</script>
And this is the modal component.
<template>
<transition name="modal" role="dialog">
<div class="modal" style="display: block">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Edit Contact</h5>
</div>
<div class="modal-body">
<p>{{euser}}</p>
<p>{{euser.id}}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" #click="closeModal">Close</button>
</div>
</div>
</div>
</div>
</transition>
</template>
<script>
export default {
name: 'editUser',
props: {
euser: {
type: Object,
},
showEdit: {
'default' : false,
}
},
data() {
return {
edit_user: [],
};
},
methods: {
closeModal(){
this.$emit('update:showEdit');
},
},
};
</script>
I have tried passing the prop value in the loop as shown above as well as in the component shown below.
<edit-user v-if="showEditUser" :showEditUser.sync="showEdit" :euser="user"></edit-user>
How can I get a single user from the parent to display in the modal ?
In your parent component you can create a data property called "currUser:null" and on "initUser" method you can do the following:
initEdit(user){
this.currUser=user;
this.showEditUser = true;
}
then your modal component definition will look like the following:
<edit-user v-if="showEditUser" :showEditUser.sync="showEdit" :euser="currUser">
</edit-user>
First, you must pass the euser prop to the <edit-user/> component not the button that will call the edit.
Second, the initEdit() function should look more like this
initEdit(user) {
this.user = user
this.showEditUser = true
}
Third, if you plan on editing the user within the modal you will likely need to create a copy of the user within the child component.
watch: {
showEditUser() {
this.editableUser = JSON.parse(JSON.stringify(this.euser))
}
}
then all of the v-models on the child should point to this.editableUser.
when the user goes to save the edit you can emit a new function that could pass the new version back out to the parent like so
saveEdit() {
this.$emit('save', this.editableUser)
}
you would just need to catch the save in the <edit-user /> component like so
<edit-user v-show="showEditUser" :showEditUSer.sync="showEdit" :euser="user" #save="saveUser" />
//script...data...methods
saveUser(user) {
let ind = this.users.map(u => u.id).indexOf(user.id)
if(ind != -1) {
Object.assign(this.users[ind], user) //put user back where it belongs with new data
}
}
Just passing the user like this:
<edit-user v-if="showEditUser" :showEditUser.sync="showEdit" :euser="user[indexOfUser]"></edit-user>
And change the prop properties to receive an object an not an Array
euser: {
type: Object,
},
Just try to add in your main component data to user just for testing if the 'undefined' problem comes from there. and in the :euser="user[0]"
user: [{
id: 1
}]
Related
I am attempting to do a SPA using Vue.js but unfortunately I know almost nothing about it, I followed a tutorial and got something up and running. This should hopefully be relatively simple!
I'm trying to create a simple page that:
Does a REST API call and pulls some JSON
A list with links of a particular field in the list of results is displayed on the left side of the screen
(I've managed until here)
Now I would like to be able to click on one of the links and see on the right side of the screen the value of another field for the same record.
For instance, suppose my JSON is:
{
"jokes":{
[
"setup":"setup1",
"punchline":"punchline1"
],
[
"setup":"setup2",
"punchline":"punchline2"
],
[
"setup":"setup3",
"punchline":"punchline3"
]
}
}
So in my screen I would see:
setup1
setup2
setup3
So if I click in setup1 I see punchline1, setup2 displays punchline2 and so on.
Here is my code - I'm basically trying to display the punchline in the moduleinfo div. I realise the current solution does not work. I've been searching but can't find any similar examples. Any pointers would be greatly appreciated.
<template>
<div class="home">
<div class="module-list">
<input type="text" v-model.trim="search" placeholder="Search"/>
<div>
<ul>
<li class="modules" v-for="value in modulesList" :key="value.id">
{{ value.setup }}
</li>
</ul>
</div>
</div>
<div class="moduleinfo">
<h2>Module info</h2>
<!-- <p>{{ value.punchline }}</p> -->
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'Home',
data: function(){
return {
jokes: [],
search : ""
}
},
mounted() {
this.getModules();
},
methods: {
getModules() {
var self = this
const options = {
method: 'GET',
url: 'https://dad-jokes.p.rapidapi.com/joke/search',
params: {term: 'car'},
headers: {
'x-rapidapi-key': '...',
'x-rapidapi-host': 'dad-jokes.p.rapidapi.com'
}
};
axios.request(options)
.then(response => {
self.jokes = response.data;
console.log(response.data);
}).catch(function (error) {
console.error(error);
});
}
},
computed: {
modulesList: function () {
var jokes = this.jokes.body;
var search = this.search;
if (search){
jokes = jokes.filter(function(value){
if(value.setup.toLowerCase().includes(search.toLowerCase())) {
return jokes;
}
})
}
return jokes;
}
},
};
</script>
Thanks!
I was building a sample Single File Component in my Vue 2 CLI app, and when I came back to post it, Ryoko had already answered the question with the same approach that I recommend, adding a new property to track showing the punchline.
Since I already built it, I figured that I might as well post my component, which does change the layout, using a table instead of a list, but the functionality works.
<template>
<div class="joke-list">
<div class="row">
<div class="col-md-6">
<table class="table table-bordered">
<thead>
<tr>
<th>SETUP</th>
<th>PUNCHLINE</th>
</tr>
</thead>
<tbody>
<tr v-for="(joke, index) in jokes" :key="index">
<td>
{{ joke.setup }}
</td>
<td>
<span v-if="joke.showPunchline">{{ joke.punchline }}</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
jokes: [
{
setup: "setup1",
punchline: "punchline1"
},
{
setup: "setup2",
punchline: "punchline2"
},
{
setup: "setup3",
punchline: "punchline3"
}
]
}
},
methods: {
getPunchline(index) {
this.jokes[index].showPunchline = true;
},
addPropertyToJokes() {
// New property must be reactive
this.jokes.forEach( joke => this.$set(joke, 'showPunchline', false) );
}
},
mounted() {
this.addPropertyToJokes();
}
}
</script>
You can add a new property inside the data object and then make a new method to set it accordingly when you click the <a> tag. Have a look at the code below, it was a copy of your current solution, edited & simplified to show the addition that I made to make it easier for you to find it.
The select method will insert the object of the clicked joke to the selectedJoke so you can render it below the Module Info.
Because it's defaults to null, and it might be null or undefined, you have to add v-if to the attribute to check wether there is a value or not so you don't get error on the console.
<template>
<div class="home">
<div class="module-list">
<input type="text" v-model.trim="search" placeholder="Search"/>
<div>
<ul>
<li class="modules" v-for="value in modulesList" :key="value.id">
{{ value.setup }}
</li>
</ul>
</div>
</div>
<div class="moduleinfo">
<h2>Module info</h2>
<p v-if="selectedJoke">{{ selectedJoke.punchline }}</p>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'Home',
data: function(){
return {
jokes: [],
search : "",
selectedJoke: null,
}
},
methods: {
select(joke) {
this.selectedJoke = joke;
},
},
};
</script>
I have the following UI:
I will try to explain what is going on here. I have "Add message" button. When I click on the button I have new form with message: title, body, image, language (just multiple select via this plugin). I have clicked twice - I have 2 messages. Simple.
I don't use vue router. Implementation of my routing is with the help of backend. It means that for each route I have new vuex state.
I'm going to keep my messages in vuex, but it's impossible to use v-model for this case.
So, I will show my code.
store:
export const store = new Vuex.Store({
state: {
messages: [],
// more props are here ...
},
mutations: {
setMessages(state, messages) {
state.messages = messages;
},
// more setters are here
},
getters: {
getMessages: state => {
return state.messages;
},
// more getters are here
},
actions: {
updateMessagesAction: function({commit}, value) {
console.log(value)
},
}
});
Messages component:
<template>
<div>
<button class="btn btn-outline-info" #click.prevent="createNewMessage">
<i class="fa fa-language"/> Add message
</button>
<div>
// now it works with local state, but I need to work with vuex
<div v-for="(message, index) in messages">
<button class="btn btm-sm btn-danger" #click="deleteMessage(index, message)"><i class="fa fa-remove"/>
</button>
<b-collapse collapsed :id="`collapse-${index}`">
<form>
<div class="form-group">
<label class="typo__label">Languages</label>
<multiselect
v-model="message.languages"
:options="getLanguagesOptions"
:multiple="true"
:close-on-select="true"
:clear-on-select="false"
:preserve-search="true"
placeholder="Languages"
label="name"
track-by="id"
>
</multiselect>
</div>
<div class="form-group">
<label for="title" class="typo__label">Title</label>
<input type="text" id="title" class="form-control" autocomplete="off" ??? how to bind it to vuex ???? I dont understand :(((>
</div>
<div class="form-group">
<label for="text" class="typo__label">Body</label>
<textarea class="form-control" id="text" ??? how to bind it to vuex ???? I dont understand :(((/>
</div>
<div class="form-group">
<div id="upload-image">
<div v-if="!message.imageSrc">
<h2>Image</h2>
<input type="file" ref="file" #change="onFileChange($event, message)">
</div>
<div v-else>
<img :src="message.imageSrc"/>
<button #click.prevent="removeImage($event, message)">Remove</button>
</div>
</div>
</div>
<hr class="mb-2">
</form>
</b-collapse>
</div>
</div>
</div>
</template>
<script>
// imports
export default {
async created() {
// set languages from servert to vuex
let res = (await axios.post(this.urlForGettingLanguagesFromServerProp)).data;
this.$store.commit('setLanguagesOptions', res);
},
name: "MessagesComponent",
props: {
urlForGettingLanguagesFromServerProp: String,
uploadImageUrl: String,
deleteImageUrl: String,
selectedLanguagesIdsProp: {
type: Array,
default: () => []
},
},
methods: {
...mapMutations(['setLanguagesSelected']),
...mapActions(['updateMessagesAction']),
createNewMessage: function () {
let message = {
languages: [],
languagesIds: [],
title: "",
text: "",
imageSrc: "",
imageDbId: 0
};
this.messages.push(message);
},
deleteMessage: function (index, message) {
this.removeImage("", message);
this.messages.splice(index, 1);
},
onFileChange: async function (e, message) {
// this method add send image on server and save to state db image id ant path
},
removeImage: function (event, message) {
// remove image from server
}
},
computed: {
...mapGetters(['getLanguagesOptions', 'getMessages'])
},
watch: {
messages: {
deep: true,
immediate: true,
handler(val, oldVal) {
let message = JSON.parse(JSON.stringify(val));
this.$store.commit("setMessages", message);
}
}
},
data() {
return {
messages: [],
}
}
}
</script>
I call this component in a parent component. In the parent component I initiate vuex during update operation.
As you can see this component works with local state and synchronize local state with vuex. It's ok for Create operation. I can send messages to vuex, then I can take it in the parent component with other information and send it on the server. But what to do with Update? I have data from the server in the parent component but local state, of course, is empty for the Messages component. How to bind all messages to vuex and have ability to change each message separately? I mean, for example, to change title of the first message and to have it in vuex immediately?
Please, help me improve this component.
I'm using three single-file-components.
ParentVariation.vue
VariationInfo.vue
Childvariation.vue
I'm emitting MarkedFilled event from child component variation-info, and catching that event on ParentVariation. Here's the content of ParentVariation.vue :
<template>
<section class="parentVariation">
<label :for="'key-'+row.id">Key</label>
<select :name="'key-'+row.id" :id="'key-'+row.id" class="select2"></select>
<label :for="'value-'+row.id">Value</label>
<input :name="'value-'+row.id" :id="'value-'+row.id">
<label :for="'quantity-'+row.id">quantity</label>
<input :name="'quantity-'+row.id" :id="'quantity-'+row.id">
<variation-info :filled="row.filled" #markedFilled="row.filled='true'" :key="row.id"></variation-info>
<button #click="addChild" type="button" class="btn btn-link btn-sm btn-fw">
<i class="mdi mdi-table-column-plus-after"></i>
Add Child
</button>
<button #click="popChild" type="button" class="btn btn-link text-danger btn-sm btn-fw">
<i class="mdi mdi-table-column-remove"></i>
Drop Child
</button>
<br>
<div v-if="row.child.length > 0">
<child-variation v-for="child in row.child" :childIndex="child.id" :parentIndex="row.id" :key="child.id"></child-variation>
</div>
</section>
</template>
<script>
export default {
props: [ 'row' ],
methods: {
addChild() {
this.row.child.push({ id:this.row.child.length, filled:'' })
},
popChild() {
this.row.child.pop()
},
}
}
</script>
On the Main Vue-Instance. i have this:
data: function() {
return {
parents: [{ id:0, child: [], filled:'' }]
}
},
And I've initialized ParentVariation like so:
<parent-variation v-for="parent in parents" :row="parent" :key="parent.id"></parent-variation>
i'm trying to achieve this functionality: When MarkedFilled event is called from child component (variation-info). Parent component (parent-variation) will catch that & update filled property for data parent on main vue instance.
But each time this event is called, only first parent element's filled property is changed. I want to change the property of the clicked element.
I've been trying to figure this out for 2 days now, Any help is appreciated.
I just want to understand why only first element is called each time.
You might want to try emitting markedFilled event to parent component:
In ParentVariation.vue
<variation-info :filled="row.filled" #markedFilled="$emit('markedFilled')" :key="row.id">
</variation-info>
and in main
<parent-variation v-for="(parent, index) in parents" :row="parent" :key="parent.id" #markedFilled="changeFilled(index)">
</parent-variation>
export default {
data: function() {
return {
parents: [{ id:0, child: [], filled:'' }]
}
},
methods: {
changeFilled(index) {
this.parents[index].filled = true
this.parents = JSON.parse(JSON.stringify(this.parents))
}
}
}
I have the following HTML code with vue.js bindings. The code shows a list of items in a table.
<table id="test-vue">
<tr v-for="(item, index) in items">
<td>
<input type="text" v-model="item.foreignId" />
</td>
<td>
<div v-if="canApproveItem(item, foreignValues)">
// UI allowing to approve the item
</div>
</td>
</tr>
</table>
The Vue app is defined as:
new Vue({
el: "#test-vue",
data: function() {
return {
items: [
{
foreignId: '4943',
}
],
foreignValues: [
{ id: '2424', canApprove: false },
{ id: '4943', canApprove: true }
],
computed: {
canApproveItem: function(item, foreignValues) {
let foreign = foreignValues(obj => {
return obj.id === item.foreignId;
});
if (foreign) {
return foreign;
} else {
return false;
}
}
}
})
The goal is to show the approve UI only for items which links to a "foreign" whose canApprove property is set to true. The user can edit the foreignId and the user interface should reflect the user-made change of the foreignId values.
If I run the above, the item argument of the canApproveItem function is the Vue object and foreignValues is undefined. I also get a Error in render: "TypeError: canApproveItem is not a function" error in the console.
How to do it properly?
This sounds like a method. You shouldn't pass in foreignValues, as you can access that from the method itself.
<table id="test-vue">
<tr v-for="(item, index) in items">
<td>
<input type="text" v-model="item.foreignId" />
</td>
<td>
<div v-if="canApproveItem(item)">
// UI allowing to approve the item
</div>
</td>
</tr>
</table>
and
new Vue({
// data and stuff here...
methods: {
canApproveItem(item) {
return this.foreignValues.findIndex(obj => obj.id === item.foreignId) !== -1;
}
}
})
I'm new to React and been trying to fix this for hours. I'm trying to get the id of the button which is clicked But this only gets the id around 20% of the time and the rest it returns nulltext. I have no idea what else to do. I have tried different binding methods but haven't been able to make it work.
I simplified the code here and put it below.
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class Popupright extends React.Component {
popupnewshow = (e) => {
let ids = e.target.getAttribute("id") + "text";
console.log(ids)
let elements = document.getElementsByClassName('poprighttext showtext');
while(elements.length > 0){
elements[0].classList.remove('showtext');
};
document.getElementById(ids).classList.toggle("showtext");
};
render() {
return (
<div>
<table className="table-bordered">
<tbody>
<tr className="table-samewidth">
<td className="td-general"><button className="popup" id="xxx" onClick={this.popupnewshow}><div className="popuptitle">xxx</div></button></td>
</tr>
<tr className="table-samewidth">
<td className="td-general"><button className="popup" id="yyy" onClick={this.popupnewshow}><div className="popuptitle">yyy</div></button></td>
</tr>
<tr className="table-samewidth">
<td className="td-general"><button className="popup" id="zzz" onClick={this.popupnewshow}><div className="popuptitle">zzz</div></button></td>
</tr>
</tbody>
</table>
<div id="xxxtext" className="poprighttext">
<p>xxx.</p>
</div>
<div id="yyytext" className="poprighttext">
<p>yyy</p>
</div>
<div id="zzztext" className="poprighttext">
<p>zzz</p>
</div>
</div>
);
}
}
export default Popupright;
Console Image: The buttons should give the id xxxtext, yyytext or zzztext depending on the button clicked but this only works 20% of the time. The rest it returns nulltext and after some clicks it returns again the proper id:
Using e.currentTarget.id should solve your issue.
e.target holds the element that you clicked on, but e.currentTarget will hold the element where you have bind the handler.
When you use e.currentTarget:
<button className="popup" id="xxx" onClick={this.popupnewshow}>
<div className="popuptitle">xxx</div><!-- clicking on here:
e.currentTarget.id is xxx -->
</button>
When you use e.target:
<button className="popup" id="xxx" onClick={this.popupnewshow}>
<div className="popuptitle">xxx</div><!-- clicking on here:
there's no id here (the clicked element id) -->
</button>
In general better to avoid direct DOM manipulation like remove. Also you can get the id directly rather than from the event:
const toggleItem = (arrayOfObjects, objectId) => {
//some implementation of toggle object's vislble prop based on id property in array
}
class Popupright extends React.Component {
state = {
popups: [
{id: 'xxx', text: 'xxxtext', visible: false},
{id: 'yyy', text: 'yyytext', visible: false},
...
]
}
togglePopup = id => {
this.setState(prevState => ({
popups: [...toggleItem(prevState.popups, id)]
})
}
render() {
return (
<table>
...
<td>
<button onClick={() => this.togglePopup('xxx')} />
</td>
...
</table>
<div className="popupsWrap">
{this.state.popups.map(popup => {
if (popup.visible) {
return (
<div className="poprighttext">{popup.text}</div>
)
}
}}
</div>
...