Vue.js - emit to update array not working - javascript

I have two components: Parent and Child. The Parent has an array of cars the Child is supposed push objects the cars array. My problem is that my Child component turns cars into an object, instead of pushing an object into the cars array. My Parent component:
<template>
<child v-model="cars"></child>
<ul>
<li v-for="car in cars">
{{ car.model }}
</li>
</ul>
</template>
export default {
data: function() {
return {
cars: []
}
}
}
My Child component:
<template>
<div>
<button type="button" #click="addCar()">Add Car</button>
</div>
</template>
export default {
methods: {
addCar() {
this.$emit("input", { model: "some car model" })
}
}
}
Expected results:
cars gets updated and becomes [{ model: "some car model"}, { model: "some car model"}, etc...]
Actual results:
cars becomes an object {model: "some car model"}
Here is my fiddle:
https://jsfiddle.net/t121ufk5/529/
I assume something is wrong with the way i am using v-model on my child component and/or the way I am emitting is incorrect. Can someone help? Thanks in advance!

Lets discuss, why you not get proper result.Then we discuss other approach to solve this problem.
Firstly we need to understand how v-model works on custom components by default.
When using a text input (including types such as email, number, etc.) or textarea, v-model="varName" is equivalent to :value="varName" #input="e => varName = e.target.value". This means that the value of the input is set to varName after each update to the input varName is updated to the value of the input. A normal select element will act like this too, though a multiple select will be different.
Now we need to understand,
How Does v-model Work On Components?
Since Vue doesn’t know how your component is supposed to work, or if it’s trying to act as a replacement for a certain type of input, it treats all components the same with regards to v-model. It actually works the exact same way as it does for text inputs, except that in the event handler, it doesn’t expect an event object to be passed to it, rather it expects the value to be passed straight to it. So…
<my-custom-component v-model="myProperty" />
…is the same thing as…
<my-custom-component :value="myProperty" #input="val => myProperty = val" />
So when you apply this approach. You have to receive value as a props. and make sure you $emit name is input.
Now you can ask me at this stage,what you do wrong?
Ok, look at like code #input="val => myProperty = val"
when you $emit whit a new value. this newValue will updated our parent value which you wanna update.
Here is your code this.$emit("input", { model: "some car model" }).
You update your parent value with a object. So your Array updated with a Object.
Lets solve the full problem.
Parent Component:
`
<template>
<child v-model="cars"></child>
<ul>
<li v-for="car in cars">
{{ car.model }}
</li>
</ul>
</template>
export default {
data: function() {
return {
cars: []
}
}
}
`
Child Component:
<template>
<div>
<button type="button" #click="addCar()">Add Car</button>
</div>
</template>
export default {
props: ['value']
methods: {
addCar() {
this.$emit("input", this.value.concat({model: "some car model"}))
}
}
}
You can actually solved it several way.
Second Approach,
Parent:
<template>
<child :cars="cars"></child>
<ul>
<li v-for="car in cars">
{{ car.model }}
</li>
</ul>
</template>
export default {
data: function() {
return {
cars: []
}
}
}
Child:
<template>
<div>
<button type="button" #click="addCar">Add Car</button>
</div>
</template>
export default {
props: {
cars: {
type: Array,
default:[]
}
},
methods: {
addCar() {
this.cars.push({ model: "some car model" })
}
}
}
Last Approach:
Parent:
<template>
<child #update="addCar"></child>
<ul>
<li v-for="car in cars">
{{ car.model }}
</li>
</ul>
</template>
export default {
data() {
return {
cars: []
}
}
},
methods: {
addCar() {
this.cars.push({ model: "some car model" })
}
}
}
Child:
<template>
<div>
<button type="button" #click="update">Add Car</button>
</div>
</template>
export default {
methods: {
update() {
this.$emit('update')
}
}
}

It is possible to trigger the event of updating the transmitted value in props
In the parent
<my-component :is-open.sync="isOpen" />
In my-component
this.$emit('update:isOpen', true)

Related

Algolia vue instantsearch dynamically set search query

I have a search bar on the root component (App.vue). I want to enter a query and on #keyup.enter, it should redirect to the Search component view with the v-text-field input value. Redirection is used by using $router.replace because users might search for a different keyword from within the same route.
The code below work but only ONCE. If I enter a new search term, the URL changed but the results stay the same.
App.vue
<template>
<div>
<header>
<v-text-field #keyup.enter="goToSearchPage($event)"></v-text-field>
</header>
<v-main>
<router-view></router-view>
</v-main>
</div>
</template>
<script>
export default {
methods: {
goToSearchPage($event) {
this.$router.replace({
name: "Search",
query: { q: $event.target.value }
});
}
}
};
</script>
views/Search.vue
<template>
<div>
<ais-instant-search
index-name="dev_brunjar_products"
:search-client="searchClient"
:search-function="searchFunction"
>
<ais-hits>
<ul slot-scope="{ items }">
<li v-for="item in items" :key="item.objectID">
{{ item.name }}
</li>
</ul>
</ais-hits>
</ais-instant-search>
</div>
</template>
<script>
import algoliasearch from "algoliasearch/lite";
export default {
data() {
return {
searchClient: algoliasearch(
process.env.VUE_APP_ALGOLIA_APP_ID,
process.env.VUE_APP_ALGOLIA_SEARCH_KEY
)
};
},
methods: {
// According to Algolia's doc, this should be inside data instead of methods
// https://www.algolia.com/doc/api-reference/widgets/instantsearch/vue/#widget-param-search-function
// But doing so, I wouldn't be able to get this.$route.query.q
searchFunction(helper) {
var query = this.$route.query.q;
if (query) {
helper.setQuery(query).search();
}
}
}
};
</script>
What I've tried
Did a hack-ish way (Test 1) to solve it but didn't work (which I'm glad, because it doesn't feel right). Below was the non-working code addition to the Search component. Created computed & watch property of query which get its data from this.$route.query.q and algoliaHelper data assigned with AlgoliaSearchHelper when the searchFunction first load.
When I typed a new search term, the watcher works and the query indeed changed. Despite that, calling the helper and setting its query with the new term within the watcher did not change the results from Algolia.
Then I used Routing URLs (Test 2) to the ais-instant-search and it still didn't solve the issue. Maybe I'm implementing it wrong? I really tried to understand Algolia's doc and it's just too hard to digest.
views/Search.vue - Test 1 (Failed)
<template>
<div>
<ais-instant-search
index-name="dev_brunjar_products"
:search-client="searchClient"
:search-function="searchFunction"
>
<ais-hits>
<ul slot-scope="{ items }">
<li v-for="item in items" :key="item.objectID">
{{ item.name }}
</li>
</ul>
</ais-hits>
</ais-instant-search>
</div>
</template>
<script>
import algoliasearch from "algoliasearch/lite";
export default {
data() {
return {
searchClient: algoliasearch(
process.env.VUE_APP_ALGOLIA_APP_ID,
process.env.VUE_APP_ALGOLIA_SEARCH_KEY
),
algoliaHelper: null
};
},
computed: {
query() {
return this.$route.query.q;
}
},
watch: {
query(newQuery) {
this.algoliaHelper.setQuery(newQuery).search();
}
},
methods: {
searchFunction(helper) {
if (!this.algoliaHelper) {
this.algoliaHelper = helper;
}
if (this.query) {
helper.setQuery(this.query).search();
}
}
}
};
</script>
views/Search.vue - Test 2 (Failed)
<template>
<div>
<ais-instant-search
index-name="dev_brunjar_products"
:search-client="searchClient"
:search-function="searchFunction"
:routing="routing"
>
<ais-hits>
<ul slot-scope="{ items }">
<li v-for="item in items" :key="item.objectID">
{{ item.name }}
</li>
</ul>
</ais-hits>
</ais-instant-search>
</div>
</template>
<script>
import { history as historyRouter } from "instantsearch.js/es/lib/routers";
import { singleIndex as singleIndexMapping } from "instantsearch.js/es/lib/stateMappings";
import algoliasearch from "algoliasearch/lite";
export default {
data() {
return {
searchClient: algoliasearch(
process.env.VUE_APP_ALGOLIA_APP_ID,
process.env.VUE_APP_ALGOLIA_SEARCH_KEY
),
routing: {
router: historyRouter(),
stateMapping: singleIndexMapping("instant_search")
}
};
},
methods: {
searchFunction(helper) {
if (this.query) {
helper.setQuery(this.query).search();
}
}
}
};
</script>
I would appreciate it if you guys know how to solve this issue.
https://codesandbox.io/s/github/algolia/doc-code-samples/tree/master/Vue%20InstantSearch/routing-vue-router?file=/src/views/Home.vue
This is an example using vue router. I guess this might be what you're looking for.
Please let us know if it works for you.
Hope you where able to solve this since it's been a long time since you asked. But, in order to make it work you have to put searchFunction(helper) inside data() as shown in the docs: https://www.algolia.com/doc/api-reference/widgets/instantsearch/vue/#widget-param-search-function

Selected values in a v-for loop of components

I implemented a component that have a select element inside and it is like following more or less:
<!-- child component -->
<template>
<b-form-select
v-model="selectedProject"
:options="projects"
#change="changedValue">
<template v-slot:first>
<option :value="null" disabled>-- Please select a project --</option>
</template>
</b-form-select>
</template>
<script>
export default {
name: 'AllocationItem',
props: {
projects: {
type: Array,
default: () => [{ value: Number}, { text: String}]
}
},
data() {
return {
selectedProject: null,
}
},
methods: {
changedValue(value) {
this.selectedProject = value;
}
}
}
</script>
I use this component in a parent componenent where it can be possible add other AllocationItem clicking on a button.
To do that i have used an array where i push a new item every time that there is a click on add button (i don't know if it's the right way...)
Follow parent component code:
<!-- parent component -->
<template>
<b-button class="btnAction" #click="addItem()">Add</b-button>
<b-button class="btnAction" #click="sendAllocation()">Send</b-button>
<b-row v-for="allocation in resourceItem.allocations" v-bind:key="allocation.id">
<allocation-item v-bind:projects="projects"></allocation-item>
</b-row>
</template>
<script>
export default {
name: 'Management',
components: {
AllocationItem
},
data() {
return {
allocations: []
}
},
methods: {
addItem() {
this.allocations.push(AllocationItem);
},
sendAllocation() {
this.allocations.forEach((allocation) => {
// I WOULD LIKE TO HAVE ALL SELECTED VALUE HERE!!!
});
},
},
created() {
const dataProjects = this.getProjectsData();
dataProjects.then(response => {
this.projects = response.map((item) => {
return {value: item.id, text: item.name}
})
});
}
</script>
In my application i have another button, send button, that should be read values selected in all child component (allocation-item).
How can i do to have an array with that selected values?
Thank you in advance to all
This depends on the relationship from your 'Send Button'-component with your parent component.
You can either:
Use the $emit() method to propagate data up the component tree to the shared parent component. Then prop it down to the 'Send Button'-component.
Have a single source of truth by using Vuex. This is a store that keeps all your data in a centralised object.
Perhaps you can provide us with more information on the project structure?
First of all, ask yourself if this component is being used anywhere else but here. If you only use it once, build it into the parent component and your problems are solved.
Otherwise I'd go with #laurensvm's approach of using emit or Vuex.
After some researches on google i have founded a solution. I don't know if it the right way but it works fine and it seems clean.
My solution consists to use emit on child component and v-model on parent like show following example.
<!-- child component -->
<template>
<b-form-select
v-model="selectedProject"
:options="projects"
#change="changedValue">
<template v-slot:first>
<option :value="null" disabled>-- Please select a project --
</option>
</template>
</b-form-select>
</template>
<script>
export default {
name: 'AllocationItem',
props: {
projects: {
type: Array,
default: () => [{ value: Number}, { text: String}]
},
value: {
type: Number
}
},
data() {
return {
selectedProject: this.value
}
},
methods: {
changedValue(value) {
this.$emit('input', value);
}
}
}
</script>
And on parent using an array variable on v-model.
Something like this:
<!-- parent component -->
<template>
<b-container>
<b-row>
<b-button class="btnAction" variant="success" #click="addItem(index)">Add</b-button>
<b-button class="btnAction" #click="sendAllocation(index)">Send</b-button>
</b-row>
<b-row v-for="(allocation, index) in resourceItem.allocations" v-bind:key="allocation.id">
<allocation-item v-bind:projects="projects" v-model="allocationItemSelected[index]"></allocation-item>
</b-row>
</b-container>
</template>
See a runnable example clicking on codesandbox link below:
https://codesandbox.io/s/vue-template-4f9xj?autoresize=1&expanddevtools=1&fontsize=14&hidenavigation=1&moduleview=1

Trying to use a single button function to add playlist from an array to another component

Assuming you have an array of an object coming from database (say songs) and it was rendered using the v-for.
i want to create a button with a function to add each object song to another component. having it in mind that i don't know the object keys because i don't know how many objects there are in that array.
Also having it in mind that the button was added to each list using the same v-for
<template>
<div>
<h1>Vue Top 20 Artists</h1>
<ul>
<li class="list" v-for="(artist, x) in artists" :key="x">
<h3>{{artist.name}}</h3>
<button #click="addPlayList()">Add to playlist</button>
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'Test',
props: ["artists"],
methods:{
addPlayList(){
let hey = this.artists
alert(Object.keys(hey))
}
}
}
</script>
What a about useing the artist as argument for your method:
<template>
<div>
<h1>Vue Top 20 Artists</h1>
<ul>
<li class="list" v-for="(artist, x) in artists" :key="x">
<h3>{{artist.name}}</h3>
<button #click="addPlayList(artist)">Add to playlist</button>
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'Test',
props: ["artists"],
methods:{
addPlayList(artist){
alert(artist)
}
}
}
</script>
if you want to transfer the clicked button to the parent element use this.$emit() explained in detail: https://v2.vuejs.org/v2/guide/components-custom-events.html
If the 'other component' is your child component you will most likely pass your list as a prop. If it is your parent, then you could emit it together with a custom event. And you would use Vuex to share your list 'globally'. Though anyway before passing it to somewhere else, first you have to prepare your playlist in your current component. You can add a 'playlist' to your data like: data: () => ({ playlist: [] })
Your function would then push a new artist to the 'playlist'.
You can pass the artist to your function like: #click="addPlayList(artist)"
So in case your 'other component' is your child it could look like this:
<template>
<div>
<h1>Vue Top 20 Artists</h1>
<ul>
<li class="list" v-for="(artist, x) in artists" :key="x">
<h3>{{artist.name}}</h3>
<button #click="addPlayList(artist)">Add to playlist</button>
</li>
</ul>
<some-component :playlist="playlist" />
</div>
</template>
<script>
import 'SomeComponent' form '....'
export default {
name: 'Test',
components: { SomeComponent },
props: ["artists"],
data: () => ({ playlist })
methods:{
addPlayList (artist) {
this.playlist.push(artist)
}
}
}
</script>
If it is a parent component addPlayList function could look like:
addPlayList (artist) {
this.playlist.push(artist)
this.$emit('some-event', this.playlist)
}

Not able to emit an event to a parent component from child component in Vuejs

My parent component takes data which is an array of strings from the api and then passes it to the child component. In the child component I display the data from the parent as a dropdown list and when I select a particular item from the dropdown list I want it to set a particular variable.
I have used $emit and $event as shown in documentation but it is not working.Please look at my code and tell me where I am going wrong.
Parent Component App.vue
<template>
<div id="app">
<nlp-vision-catalog v-bind:cataloglist="catalogs" v-on:listenClick="setcatalogselected($event)" ></nlp-vision-catalog>
</div>
</template>
<script>
import NlpVisionCatalog from './components/NlpVisionCatalog'
import axios from 'axios'
export default {
name: 'App',
components: {
NlpVisionCatalog
},
data (){
return {
catalogs :[],
catalog_selected : ""
}
},
methods:{
fetchcatalogs(){
axios.get("http://localhost:5000/clients")
.then((resp)=>{this.catalogs.push.apply(this.catalogs,
resp.data.response.results.client_name);
}).catch((err)=>{
console.log(err);
})
},
setcatalogselected(catalog){
this.catalog_selected = catalog;
)}
},
created(){
this.fetchcatalogs()
}
}
</script>
<style></style>
My Child Component is NlpVisionCatalog.vue
enter code here
<template>
<div>
<h3>Select Catalog</h3>
<select>
<option v-for="item in cataloglist">
<p v-on:click="emitbackthecatalog(item)"> {{ item }} </p>
</option>
</select>
</div>
</template>
<script>
export default{
name : 'NlpVisionCatalog',
props: ['cataloglist'],
data (){
return {
comp: ""
}
},
methods:{
emitbackthecatalog(catalog_name){
this.$emit('listenClick',catalog_name);
}
}
};
</script>
<style>
</style>
Where exactly I am going wrong?
ps- http://localhost:5000/clients is the api that is running on my system.
The problem is in your child component select element
change your code to like this use onChange function in select element
<h3>Select Catalog</h3>
<select v-model="selected" v-on:change="emitbackthecatalog(selected)">
<option v-for="item in cataloglist" :value="item" :key="item" >
{{ item }}
</option>
</select>
data (){
return {
selected: ""
}
},
methods:{
emitbackthecatalog(catalog_name){
this.$emit('listenclick',catalog_name);
}
}
In your parent component
<nlp-vision-catalog v-bind:cataloglist="catalogs" v-on:listenclick="setcatalogselected($event)" ></nlp-vision-catalog>
check the demo link

Change class of element on mouseenter/mouseleave event within v-for loop in Vue.js

I'm trying to change the class of a single element within v-for loop on a mouseenter/mouseleave event but I'm confused on how to make it so that only the class on the element that is being hovered changes.
I've tried binding the class to a variable but that obviously causes all the elements in the list to change.
<template>
<ul>
<li class="item" v-for="item in items" #mouseenter="showInfoBar()" #mouseleave="hideInfoBar()">
<span class="infobar" :class="{ show : infoBar }"> </span>
</li>
</ul>
</template>
<script>
export default {
name: 'ItemsList',
props: ['items'],
data() {
return {
infoBar: false
}
},
methods: {
showInfoBar() {
this.infoBar = true
},
hideInfoBar() {
this.infoBar = false
}
}
}
</script>
You could use this instead
<template>
<ul>
<li class="item" v-for="(item, index) in items" :key="index" #mouseenter="infoBar = index" #mouseleave="infoBar = null">
<span class="infobar" :class="{ show : infoBar == index }"> </span>
</li>
</ul>
</template>
Then for your data property,
data() {
return {
infoBar: null
}
},
The problem is that you have infoBar representing the state of all the info bars, when they should be controlled independently.
Turning infoBar into an array, with each element representing the state of the info bar at that respective index should solve your problem.
<template>
<ul>
<li class="item" v-for="(item, index) in items" #mouseenter="showInfoBar(index)" #mouseleave="hideInfoBar(index)">
<span class="infobar" :class="{ show : infoBar[index] }"> </span>
</li>
</ul>
</template>
<script>
export default {
name: 'ItemsList',
props: ['items'],
data() {
return {
infoBar: []
}
},
mounted: function() {
for(var i = 0; i < this.items.length; i++) {
this.infoBar.push(false);
}
},
methods: {
showInfoBar(index) {
this.infoBar[index] = true;
},
hideInfoBar(index) {
this.infoBar[index] = false;
}
}
}
</script>
I think instead of handling this as an array, the better approach should be handling it as separated components, I mean, is better practice to leave the state of each component to each component instead of handling the state of a collection of "components" in an array, I'm basing on React to this idea, you can declare ItemsList and Item as different components, and then for each Item, a separate state, please, tell me if you understand this approach, good luck.
Edition after reading the code
I think also, once you refactor your code, instead of handling the state changing in two different methods, just declare a toggleMethod() and make a logical denial to the current value of the state you are looking at the moment.
This is better:
function toggleState() {
this.someState = !this.someState;
}
Than this
function showState() {
this.someState = true;
}
function hideState() {
this.someState = false;
}
Good luck mate.

Categories