The objective: Vue component input-address has to be inside Vue component mail-composer and display a list of addresses only when someone click Address Book button. When someone click one of displayed mails or fill the To field by hand, createdmail.to has to get the value and I have to hide the list of addresses.
Vue component mail-composer. This component receives a list of addresses. (Everything is working here, I think the only part that is not working properly is v-model inside input-address tag)
Vue.component('mail-composer', {
props: ['addressesbook'],
methods: {
send: function(createmail) {
this.$emit('send', createmail);
}
},
template:
`
<div>
<input-address :addresses="addressesbook" v-model="createmail.to"></input-address>
<p><b>Subject: </b><input type="text" v-model="createmail.subject"></input></p>
<p><b>Body: </b><textarea v-model="createmail.body"></textarea></p>
<button #click="send(createmail)">Send</button>
</div>
`,
data(){
return{
createmail:{
to: '',
subject: '',
body: ''
}
}
}
});
The other Vue component is this one, which is in the same file. (I think all problems are here).
I need to display the list of addresses only when someone click Address Book button, and I have to hide it when someone click again the button or one of the emails which are in the list. When someone clicks a mail from list, the createmail.to property from the mail-composer has to get the value of the mail , also if I decide to put the mail by hand it has to occurs the same.
Vue.component('input-address',{
props:["addresses"],
template:
`
<div>
<label><b>To: </b><input type="text"></input><button #click="!(displayAddressBook)">Address Book</button></label>
<ul v-if="displayAddressBook">
<li v-for="address in addresses">
{{address}}
</li>
</ul>
</div>
`,
data(){
return{
displayAddressBook: false
}
}
})
There're some errors in your code:
#click="!(displayAddressBook)" should be #click="displayAddressBook = !displayAddressBook" - the first really does nothing (interesting), the second (suggested) sets the value of displayAddressBook to the opposite it has currently.
the input-address component does not really do anything with the input field (missing v-model)
the changes in the child component (input-address) are not sent back to the parent (added a watcher to do that in the child component)
the parent component (mail-composer) has to handle the values emitted from the child (added the #address-change action handler)
the v-for in your input-address component does not have a key set. Added key by using the index for it (not the best solution, but easy to do).
just put createmail.to: {{ createmail.to }} at the end of MailComposer, so you can see how it changes
Suggestions
always use CamelCase for component names - if you get used to it, then you get less "why is it not working?!" moments
watch for typos: createmail doesn't look good - createEmail or just simply createemail would be better (ok, it doesn't look so nice - maybe you should choose a totally different name for that)
Vue.component('InputAddress', {
props: ["addresses"],
data() {
return {
displayAddressBook: false,
address: null
}
},
template: `
<div>
<label><b>To: </b>
<input
type="text"
v-model="address"
/>
<button
#click="displayAddressBook = !displayAddressBook"
>
Address Book
</button>
</label>
<ul v-if="displayAddressBook">
<li
v-for="(address, i) in addresses"
:key="i"
#click="clickAddressHandler(address)"
>
{{address}}
</li>
</ul>
</div>
`,
watch: {
address(newVal) {
// emitting value to parent on change of the address
// data attribute
this.$emit('address-change', newVal)
}
},
methods: {
clickAddressHandler(address) {
// handling click on an address in the address book
this.address = address
this.displayAddressBook = false
}
}
})
Vue.component('MailComposer', {
props: ['addressesbook'],
data() {
return {
createmail: {
to: '',
subject: '',
body: ''
}
}
},
methods: {
send: function(createmail) {
this.$emit('send', createmail);
},
addressChangeHandler(value) {
this.createmail.to = value
}
},
template: `
<div>
<input-address
:addresses="addressesbook"
v-model="createmail.to"
#address-change="addressChangeHandler"
/>
<p>
<b>Subject: </b>
<input
type="text"
v-model="createmail.subject"
/>
</p>
<p>
<b>Body: </b>
<textarea v-model="createmail.body"></textarea>
</p>
<button #click="send(createmail)">Send</button><br />
createmail.to: {{ createmail.to }}
</div>
`
});
new Vue({
el: "#app",
data: {
addressesbook: [
'abcd#abcd.com',
'fghi#fghi.com'
]
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<mail-composer :addressesbook="addressesbook" />
</div>
Related
I am using vuejs 2.6.14 and am running into the following issue :
Modified data from child component updates data in parent component aswell, without any use of $emit in code.
This is the opposite of the usual "how to update data in child from parent / how to update in parent from child"
Here is my code in greater details:
I have a parent component named Testing.vue, passing a JSON object ("userData") to a child,
GeneralData.vue.
This is what the code looks like for the parent :
<template>
<div id="testing-compo">
<div style="margin-top: 1rem; margin-bottom: 1rem; max-width: 15rem">
<label
class="sr-only"
for="inline-form-input-username"
style="margin-top: 1rem; margin-bottom: 1rem"
>Account settings for :</label
>
<b-form-input
v-model="username"
id="inline-form-input-username"
placeholder="Username"
:state="usernameIsValid"
></b-form-input>
</div>
<b-button class="button" variant="outline-primary"
#click="callFakeUser">
Populate fake user
</b-button>
<GeneralData :userData="user" />
</div>
</template>
<script>
export default {
name: "Testing",
components: {
GeneralData,
},
data() {
return {
user: null,
username: null,
};
},
computed: {
usernameIsValid: function () {
if (this.username != null && this.username.length >= 4) {
return true;
} else if (this.username != null) {
return false;
}
return null;
},
},
methods: {
async callFakeUser() {
userServices.getFakeUser().then((res) => {
this.user = res;
console.log(this.user);
});
},
</script>
A very simple testing component that calls userServices.getFakeUser(), which asynchronously returns a JSON object.
For the child :
<template>
<div id="general-compo">
<!-- AGE -->
<div class="mt-2">
<label for="text-age">Age</label>
<div>
<b-form-input
v-model="userAge"
placeholder="+18 only"
class="w-25 p-1"
type="number"
>
</b-form-input>
</div>
</div>
<!-- LANGUAGES -->
<div class="mt-2">
<label for="lang-list-id">Language(s)</label>
<div
v-for="langKey in userLangsCount"
:key="langKey"
style="display: flex; flex-direction: row"
>
<b-form-input
readonly
:placeholder="userLangs[langKey - 1]"
style="max-width: 50%; margin-top: 0.5rem"
disabled
></b-form-input>
**This form is set to read only, for display purposes only**
<b-button
variant="outline-danger"
#click="removeLang(langKey)"
style="margin-top: 0.5rem; margin-left: 1rem"
>Remove</b-button
>
**This button removes a language from the userLangs array by calling removeLang(langKey)**
</div>
<div style="display: flex; flex-direction: row">
<b-form-input
v-model="userCurrentLang"
list="langlist-id"
placeholder="Add Language"
style="max-width: 50%; margin-top: 0.5rem"
></b-form-input>
<datalist id="langlist-id">
<option>Manual Option</option>
<option v-for="lang in langList" :key="lang.name">
{{ lang.name }}
</option>
</datalist>
<b-button
:disabled="addLangBtnDisabled"
variant="outline-primary"
#click="addLang()"
style="margin-top: 0.5rem; margin-left: 1rem"
>Add</b-button
>
</div>
</div>
</div>
</template>
<script>
import langList from "../assets/langList";
export default {
name: "GeneralData",
components: {},
props: {
userData: Object,
},
data() {
return {
userAge: null,
langList: langList,
userLangs: [],
userCurrentLang: null,
};
},
watch: {
//Updating tabs with fetched values
userData: function () {
this.userLangs = this.userData.general.langs;
this.userAge = this.userData.general.age
},
},
computed: {
**userGeneral is supposed to represent the data equivalent of userData.general, it is therefore computed from the user input, its value is updated each time this.userAge or this.userLangs changes**
userGeneral: function () {
//user data in data() have been filled with userData values
return {
age: this.userAge,
langs: this.userLangs,
};
},
**returns the amount of languages spoken by the user to display them in a v-for loop**
userLangsCount: function () {
if (this.userLangs) {
return this.userLangs.length;
}
return 0;
},
**gets a list of languages name from the original JSON list for display purposes**
langNameList: function () {
let namelist = [];
for (let i = 0; i < this.langList.length; i++) {
namelist.push(langList[i].name);
}
return namelist;
},
**returns true or false depending on whether entered language is in original list**
addLangBtnDisabled: function () {
for (let i = 0; i < this.langList.length; i++) {
if (this.userCurrentLang == langList[i].name) {
return false;
}
}
return true;
},
},
methods: {
addLang() {
this.userLangs.push(this.userCurrentLang);
this.userCurrentLang = null;
},
removeLang(key) {
this.userLangs.splice(key - 1, 1);
},
}
}
</script>
Here is what the data looks in the vuejs dev tool inside the browser after having updated this.user in Testing.vue:
Data in Testing.vue :
user : {
general:{"age":22,"langs":["French"]}
}
Data in GeneralData.vue :
userData : {
general:{"age":22,"langs":["French"]}
}
userAge : 22
userLangs : ["French"]
userGeneral :
{
general:{"age":22,"langs":["French"]}
}
So far so good right?
Well here is where the issues happen, if I change the age field in my form, userAge gets incremented, userGeneral.age gets update, but userData.general.age doesnt. Which is expected as userGeneral.age is computed out of this.userAge, and userData is a prop so it shouldn't be mutated as a good practice (and not method sets userData.general.age = xxx anyways).
HOWEVER, if i hit the Remove button next to French in the language list, this.userLangs gets updated as it should and is now [], this.userGeneral.langs gets updated to [] aswell as it is computed directly from the former. And userData.general.langs ... gets updated to [] aswell which really makes no sense to me.
Worse, in the parent, Testing.vue, user.general.langs is now set to [] aswell.
So somehow, this.userLangs updated the prop this.userData, AND this prop has updated it's original sender user in the parent component, although no $emit of any kind has been involved.
I do not want this to happen as I dont think it's supposed to happen this way and is therefore an hasard, but also because i want to setup a 'save' button later on allowing the user to modify his values all at once.
What i've tried : setting all kinds of .prevent, .stop on the #click element on the Remove / Add buttons, in the method called itself, adding e.preventDefault (modifying addLang and removeLang to send the $event element aswell), none of those attemps have solved anything.
Hopefully I didnt implement the .prevent part correctly, and someone can help me block this reverse-flow annoying issue.
Solution to the problem here is that lang is an array that is passed as a reference thus a mutation is bubbled up to the parent.
Instead of assigning the original array we can just assign a copy of that
userData: function () { this.userLangs = [...this.userData.general.langs]; this.userAge = this.userData.general.age }
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 a Vue.js text-input component like the following:
<template>
<input
type="text"
:id="name"
:name="name"
v-model="inputValue"
>
</template>
<script>
export default {
props: ['name', 'value'],
data: function () {
return {
inputValue: this.value
};
},
watch: {
inputValue: function () {
eventBus.$emit('inputChanged', {
type: 'text',
name: this.name,
value: this.inputValue
});
}
}
};
</script>
And I am using that text-input in another component as follows:
<ul>
<li v-for="row in rows" :key="row.id">
<text-input :name="row.name" :value="row.value">
</text-input>
</li>
</ul>
Then, within the JS of the component using text-input, I have code like the following for removing li rows:
this.rows = this.rows.filter((row, i) => i !== idx);
The filter method is properly removing the row that has an index of idx from the rows array, and in the parent component, I can confirm that the row is indeed gone, however, if I have, for example, two rows, the first with a value of 1 and the second with a value of 2, and then I delete the first row, even though the remaining row has a value of 2, I am still seeing 1 in the text input.
Why? I don't understand why Vue.js is not updating the value of the text input, even though the value of value is clearly changing from 1 to 2, and I can confirm that in the parent component.
Maybe I'm just not understanding how Vue.js and v-model work, but it seems like the value of the text input should update. Any advice/explanation would be greatly appreciated. Thank you.
You cannot mutate values between components like that.
Here is a sample snippet on how to properly pass values back and forth. You will need to use computed setter/getter. Added a button to change the value and reflect it back to the instance. It works for both directions.
<template>
<div>
<input type="text" :id="name" v-model="inputValue" />
<button #click="inputValue='value2'">click</button>
</div>
</template>
<script>
export default {
props: ['name', 'value'],
computed: {
inputValue: {
get() {
return this.value;
},
set(val) {
this.$emit('updated', val);
}
}
}
}
</script>
Notice that the "#updated" event updates back the local variable with the updated value:
<text-input :name="row.name" :value="row.value" #updated="item=>row.value=item"></text-input>
From your code you are trying to listen to changes.. in v-model data..
// Your Vue components
<template>
<input
type="text"
:id="name"
:name="name"
v-model="inputValue"
>
</template>
<script>
export default {
props: ['name', 'value'],
data: function () {
return {
inputValue: ""
};
},
};
</script>
If You really want to listen for changes..
<ul>
<li v-for="row in rows" :key="row.id">
<text-input #keyup="_keyUp" :name="row.name" :value="row.value">
</text-input>
</li>
</ul>
in your component file
<template>...</template>
<script>
export default {
props: ['name', 'value'],
data: function () {
return {
inputValue: ""
};
},
methods : {
_keyUp : () => {// handle events here}
};
</script>
check here for events on input here
To bind value from props..
get the props value, then assign it to 'inputValue' variable
it will reflect in tthe input element
I'm making a component which is a wrapper around a checkbox (I've done similar with inputs of type 'text' and 'number') but I cannot get my passed in value to bind correctly.
My component is:
<template>
<div class="field">
<label :for="name" class="label">
{{ label }}
</label>
<div class="control">
<input :id="name" :name="name" type="checkbox" class="control" :checked="value" v-on="listeners" />
</div>
<p v-show="this.hasErrors" class="help has-text-danger">
<ul>
<li v-for="error in errors" :key="error">{{ error }}</li>
</ul>
</p>
</div>
</template>
<script>
export default {
name: 'check-edit',
props: {
value: {
type: Boolean,
default: false
},
label: {
type: String,
default: ''
},
name: {
type: String,
default: ''
},
errors: {
type: Array,
default: () => []
}
},
mounted () {
},
computed: {
listeners () {
return {
// Pass all component listeners directly to input
...this.$listeners,
// Override input listener to work with v-model
input: event => this.$emit('input', event.target.value)
}
},
hasErrors () {
return this.errors.length > 0
}
},
}
</script>
I've imported it globally; and am invoking it in another view by doing:
<check-edit name="ShowInCalendar" v-model="model.ShowInCalendar" label="Show in calendar?" :errors="this.errors.ShowInCalendar"></check-edit>
My model is in data and the property ShowInCalendar is a boolean and in my test case is true. So when I view the page the box is checked. Using the Vue tools in firefox I can see the model.ShowInCalendar is true, and the box is checked. However, when I click it the box remains checked and the value of ShowInCalendar changes to 'on', then changes thereafter do not change the value of ShowInCalendar.
I found this example here: https://jsfiddle.net/robertkern/oovb8ym7/ and have tried to implement a local data property for it but the result is not working.
The crux of what I'm trying to do is have the initial checkstate of the checkbox be that of ShowInCalendar (or whatever property is bound via v-model on the component) and then have that property be update (to be true or false) when the checkbox is checked.
Can anyone offer me any advice please?
Thank you.
You should not $emit event.target.value, it's the value of the checkbox, it's not a Boolean value. If you want to detect the checkbox is update(to be true or false) or not, You should $emit event.target.checked just like fstep said.
If v-on is the only listener that will be used it might be easier to use v-model as in the checkbox example from the Vue input docs.
However you can use listeners based on Binding-Native-Events-to-Components docs
<template>
<div class="field">
<label :for="name" class="label">
{{ label }}
</label>
<div class="control">
<input :id="name" :name="name" type="checkbox" class="control" checked="value" v-on="listeners" />
</div>
<p v-show="this.hasErrors" class="help has-text-danger">
<ul>
<li v-for="error in errors" :key="error">{{ error }}</li>
</ul>
</p>
</div>
</template>
<script>
export default {
name: 'check-edit',
props: {
value: {
type: Boolean,
default: false
},
label: {
type: String,
default: ''
},
name: {
type: String,
default: ''
},
errors: {
type: Array,
default: () => []
}
},
mounted() {},
computed: {
listeners() {
var vm = this;
// `Object.assign` merges objects together to form a new object
return Object.assign(
{},
// We add all the listeners from the parent
this.$listeners,
// Then we can add custom listeners or override the
// behavior of some listeners.
{
// This ensures that the component works with v-model
input: function(event) {
vm.$emit('input', event.target.checked);
}
}
);
},
hasErrors() {
return this.errors.length > 0;
}
}
};
</script>
Don't change props. Your component, having a v-model, should be emitting input events on change. The parent will handle the actual changing of the value.
I know how to remove a list item from a Vue instance. However, when list items are passed to Vue components, how to remove a list item while keeping the components in sync with the list data?
Here is the use case. Consider an online forum with a Markdown editor. We have a Vue instance whose data are a list of saved comments fetched from a server. These comments are supposed to be written in Markdowns.
To facilitate edits and previews, we also have a list of components. Each component contains an editable input buffer as well as a preview section. The content of the saved comment in the Vue instance is used to initialise the input buffer and to reset it when a user cancels an edit. The preview is a transformation of the content of the input buffer.
Below is a test implementation:
<template id="comment">
<div>
Component:
<textarea v-model="input_buffer" v-if="editing"></textarea>
{{ preview }}
<button type="button" v-on:click="edit" v-if="!editing">edit</button>
<button type="button" v-on:click="remove" v-if="!editing">remove</button>
<button type="button" v-on:click="cancel" v-if="editing">cancel</button>
</div>
</template>
<div id="app">
<ol>
<li v-for="(comment, index) in comments">
<div>Instance: {{comment}}</div>
<comment
v-bind:comment="comment"
v-bind:index="index"
v-on:remove="remove">
</comment>
</li>
</ol>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.8/vue.js"></script>
<script>
let comments = ['111', '222', '333']
Vue.component('comment', {
template: '#comment',
props: ['comment', 'index'],
data: function() {
return {
input_buffer: '',
editing: false,
}
},
mounted: function() { this.cancel() },
computed: {
preview: function() {
// This is supposed to be a transformation of the input buffer,
// but for now, let's simply output the input buffer
return this.input_buffer
},
},
methods: {
edit: function() { this.editing = true },
remove: function() { this.$emit('remove', this.index) },
cancel: function() { this.input_buffer = this.comment; this.editing = false },
//save: function() {}, // submit to server; not implemented yet
},
})
let app = new Vue({
el: '#app',
data: { comments: comments },
methods: {
remove: function(index) { this.comments.splice(index, 1); app.$forceUpdate() },
},
})
</script>
The problem is that, if we remove a comment, the components are not refreshed accordingly. For example, we have 3 comments in the above implementation. if you remove comment 2, the preview of item 3 will still show the content of item 2. It is updated only if we press edit followed by cancel.
I've tried app.$forceUpdate(), but that didn't help.
You just need to add key attribute in the v-for loop like following:
<li v-for="(comment, index) in comments" :key="comment">
See working fiddle: https://fiddle.jshell.net/mimani/zLrLvqke/
Vue tries to optimises rendering, by providing key attribute, it treats those as completely different elements and re-renders those properly.
See the key documentation for more information.
try with:
Vue.component('comment', {
template:
`<div>
{{ comment }}
<button v-on:click="remove"> X </button>
</div>`,
props: ['comment', 'index'],
methods: {
remove: function() {
this.$emit('remove', this.index);
}
},
});
vm = new Vue({
el: '#app',
data: {
comments: ['a','b','c','d','e']
},
methods: {
remove: function(index) {
this.comments.splice(index, 1);
},
},
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.8/vue.min.js"></script>
<div id="app">
<ol>
<li v-for="(comment, index) in comments">
<comment
v-bind:comment="comment"
v-bind:index="index"
v-on:remove="remove">
</comment>
</li>
</ol>
</div>