I have a div in which I am displaying components from array of objects. Above the div I have an input[type=text] which will be filtering the displayed data depending what I will insert. It looks like that:
<div class="filters">
<input type="search" placeholder="Szukaj specjalisty"
:onchange="filterAllSpecialists">
<FilterData/>
</div>
<div class="all-fields">
<SpecialistPreview class="specialist-preview"
v-for="(talk, index) in newTalks"
:key="index"
:name="talk.name"
:speciality="talk.speciality"
:hourly-price="talk.hourlyPrice"
:half-hour-price="talk.halfHourPrice"
:avatar="talk.avatar"
:rating="talk.rating"
:is-available="talk.isAvailable"
>{{ talk }}
</SpecialistPreview>
</div>
I have created a method that takes the value of input and compares with the name from newTalks array:
<script>
export default {
data() {
return {
talks: [
{
name: 'Małgorzata Karwacka-Lewandowska',
speciality: 'Coach',
hourlyPrice: 150,
halfHourPrice: 80,
avatar: 'patrycja.jpg',
rating: 2.5,
isAvailable: true
},
... more objects
],
newTalks: []
}
},
methods: {
filterAllSpecialists(e) {
let input = e.target.value;
if (input !== '') {
this.newTalks = this.talks.filter(el =>
el.name.toLowerCase().includes(input.toLowerCase()))
console.log(this.newTalks);
return this.newTalks;
} else {
return this.newTalks;
}
}
},
mounted() {
this.newTalks = this.talks;
}
}
All I get is Uncaught SyntaxError: Function statements require a function name in the console
Since nobody answered and I have spent more time figuring out what was wrong with my code I found out that the first mistake I did was instead of using # I used : before onchange (I decided that for my purpose I needed #keyup instead of #change). Also there was an easier way of targeting the input value - simply by adding a v-model='allSpecialistInput.
So now in the template it should look like that:
<div class="filters">
<input type="search" placeholder="Szukaj specjalisty" v-model="allSpecialistsInput"
#keyup="filterAllSpecialists">
<FilterData/>
</div>
<div class="all-fields" v-cloak>
<SpecialistPreview class="specialist-preview"
v-for="(coach, index) in newCoaches"
:key="index"
:name="coach.name"
:speciality="coach.speciality"
:hourly-price="coach.hourlyPrice"
:half-hour-price="coach.halfHourPrice"
:avatar="coach.avatar"
:rating="coach.rating"
:is-available="coach.isAvailable"
>{{ coach }}
</SpecialistPreview>
</div>
In the methods all I needed to change was the first line in the filterAllSpecialists function like that:
filterAllSpecialists() {
let input = this.allSpecialistsInput; <--- here
if (input !== '') {
this.newCoaches = this.coaches.filter(el => el.name.includes(input.toLowerCase()))
return this.newCoaches;
} else {
return this.newCoaches;
}
},
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 }
import Vue from 'vue';
export default {
data() {
return {
cities : [
'Bangalore','Chennai','Cochin','Delhi','Kolkata','Mumbai'
],
value: '',
open: false,
current: 0
}
},
props: {
suggestions: {
type: Array,
required: true
},
selection: {
type: String,
required: true,
twoWay: true
}
},
computed: {
matches() {
return this.suggestions.filter((str) => {
return str.indexOf(this.selection) >= 0;
});
},
openSuggestion() {
return this.selection !== "" &&
this.matches.length != 0 &&
this.open === true;
}
},
methods: {
enter() {
this.selection = this.matches[this.current];
this.open = false;
},
up() {
if(this.current > 0)
this.current--;
},
down() {
if(this.current < this.matches.length - 1)
this.current++;
},
isActive(index) {
return index === this.current;
},
change() {
if (this.open == false) {
this.open = true;
this.current = 0;
}
},
suggestionClick(index) {
this.selection = this.matches[index];
this.open = false;
},
}
}
<template>
<div style="position:relative" v-bind:class="{'open':openSuggestion}">
<input class="form-control" type="text" v-model="selection"
#keydown.enter = 'enter'
#keydown.down = 'down'
#keydown.up = 'up'
#input = 'change'
/>
<ul class="dropdown-menu" style="width:100%">
<li
v-for="suggestion in matches"
v-bind:class="{'active': isActive($index)}"
#click="suggestionClick($index)"
>
{{ suggestion }}
</li>
</ul>
</div>
</template>
Getting eslint error [vue/require-v-for-key] Elements in iteration expect to have 'v-bind:key' directives.eslint-plugin-vue.
Tried changing to v-bind:key="suggestion.id" after changing, eslint error is not showing but issue is Autocomplete is not displaying(completely not working).
Can any one correct me if anything is wrong in the code.
When using v-for Vue would like to have a hint on how to identify the items in your list. You don't have to do it, but it is considered best practice and therefore eslint marks it.
To give the hint you add a key attribute with a unique value (id, some text, whatever) to the rendered list item like this:
<li
v-for="suggestion in matches"
v-bind:key="suggestion.id"
>
v-bind:key or :key in short. The value must be of type number | string | boolean | symbol.
See the docs for more info: https://v2.vuejs.org/v2/guide/list.html#Maintaining-State
As you already added the v-bind:key, Now in your code, I believe the issue is with $index, remove that and use the suggestions' index.
Here is the updated code, replace your dropdown-menu and check:
<ul class="dropdown-menu" style="width:100%">
<li
v-for="(suggestion, index) in matches"
v-bind:key="suggestion.id"
v-bind:class="{'active': isActive(index)}"
#click="suggestionClick(index)"
>
{{ suggestion }}
</li>
</ul>
I try this following code with Vue.js 2:
<div id="app">
<div v-for="(item, index) in items">
<div>
<input type="text" v-model="items[index].message">
<input type="text" v-model="items[index].surface">
</div>
</div>
<button #click="addNewfield">Add</button>
</div>
var app = new Vue({
el: '#app',
data: {
item: {
message: 'test',
surface: 45
},
items: [],
},
mounted() {
this.items.push(this.item)
},
methods: {
addNewfield() {
this.items.push(this.item);
}
}
})
The goal is to create news input when user click on Add button. I tried different ways like :
<input type="text" v-model="item.message">
But it doesn't work. If you write in "message" input, all "message" inputs will be updated.
How can I only updated the concerned value ?
Thanks for help !
This is happening because objects in Javascript are stored by reference. This means that every time you push this.item onto the array, it's adding a reference to the exact same object as the last.
You can avoid this by creating a new object each time:
methods: {
addNewfield() {
const obj = {
message: 'test',
surface: 45
}
this.items.push(obj);
}
}
Another option would be to clone the original object each time like:
methods: {
addNewfield() {
const clone = Object.assign({}, this.item);
this.items.push(clone);
}
}
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 have a data structure with nested objects that I want to bind to sub-components, and I'd like these components to edit the data structure directly so that I can save it all from one place. The structure is something like
job = {
id: 1,
uuid: 'a-unique-value',
content_blocks: [
{
id: 5,
uuid: 'some-unique-value',
block_type: 'text',
body: { en: { content: 'Hello' }, fr: { content: 'Bonjour' } }
},
{
id: 9,
uuid: 'some-other-unique-value',
block_type: 'text',
body: { en: { content: 'How are you?' }, fr: { content: 'Comment ça va?' } }
},
]
}
So, I instantiate my sub-components like this
<div v-for="block in job.content_blocks" :key="block.uuid">
<component :data="block" :is="contentTypeToComponentName(block.block_type)" />
</div>
(contentTypeToComponentName goes from text to TextContentBlock, which is the name of the component)
The TextContentBlock goes like this
export default {
props: {
data: {
type: Object,
required: true
}
},
created: function() {
if (!this.data.body) {
this.data.body = {
it: { content: "" },
en: { content: "" }
}
}
}
}
The created() function takes care of adding missing, block-specific data that are unknown to the component adding new content_blocks, for when I want to dynamically add blocks via a special button, which goes like this
addBlock: function(block_type) {
this.job.content_blocks = [...this.job.content_blocks, {
block_type: block_type,
uuid: magic_uuidv4_generator(),
order: this.job.content_blocks.length === 0 ? 1 : _.last(this.job.content_blocks).order + 1
}]
}
The template for TextContentBlock is
<b-tab v-for="l in ['fr', 'en']" :key="`${data.uuid}-${l}`">
<template slot="title">
{{ l.toUpperCase() }} <span class="missing" v-show="!data.body[l] || data.body[l] == ''">(missing)</span>
</template>
<b-form-textarea v-model="data.body[l].content" rows="6" />
<div class="small mt-3">
<code>{{ { block_type: data.block_type, uuid: data.uuid, order: data.order } }}</code>
</div>
</b-tab>
Now, when I load data from the API, I can correctly edit and save the content of these blocks -- which is weird considering that props are supposed to be immutable.
However, when I add new blocks, the textarea above wouldn't let me edit anything. I type stuff into it, and it just deletes it (or, I think, it replaces it with the "previous", or "initial" value). This does not happen when pulling content from the API (say, on page load).
Anyway, this led me to the discovery of immutability, I then created a local copy of the data prop like this
data: function() {
return {
block_data: this.data
}
}
and adjusted every data to be block_data but I get the same behaviour as before.
What exactly am I missing?
As the OP's comments, the root cause should be how to sync textarea value between child and parent component.
The issue the OP met should be caused by parent component always pass same value to the textarea inside the child component, that causes even type in something in the textarea, it still bind the same value which passed from parent component)
As Vue Guide said:
v-model is essentially syntax sugar for updating data on user input
events, plus special care for some edge cases.
The syntax sugar will be like:
the directive=v-model will bind value, then listen input event to make change like v-bind:value="val" v-on:input="val = $event.target.value"
So adjust your codes to like below demo:
for input, textarea HTMLElement, uses v-bind instead of v-model
then uses $emit to popup input event to parent component
In parent component, uses v-model to sync the latest value.
Vue.config.productionTip = false
Vue.component('child', {
template: `<div class="child">
<label>{{value.name}}</label><button #click="changeLabel()">Label +1</button>
<textarea :value="value.body" #input="changeInput($event)"></textarea>
</div>`,
props: ['value'],
methods: {
changeInput: function (ev) {
let newData = Object.assign({}, this.value)
newData.body = ev.target.value
this.$emit('input', newData) //emit whole prop=value object, you can only emit value.body or else based on your design.
// you can comment out `newData.body = ev.target.value`, then you will see the result will be same as the issue you met.
},
changeLabel: function () {
let newData = Object.assign({}, this.value)
newData.name += ' 1'
this.$emit('input', newData)
}
}
});
var vm = new Vue({
el: '#app',
data: () => ({
options: [
{id: 0, name: 'Apple', body: 'Puss in Boots'},
{id: 1, name: 'Banana', body: ''}
]
}),
})
.child {
border: 1px solid green;
}
<script src="https://unpkg.com/vue#2.5.16/dist/vue.js"></script>
<div id="app">
<span> Current: {{options}}</span>
<hr>
<div v-for="(item, index) in options" :key="index">
<child v-model="options[index]"></child>
</div>
</div>