Vue 2 Emit selected data back to parent component - vue.js

Struggling to sort out how to get a selected value from a Typeahead component to pass back to the parent component. I'm allowing the user to search from a variety of data to link a record to a post. Once the user clicks one of the typeahead drop-down records, I pass the item to the sendlink method - I've checked that the data passes ok. When I do the emit using the selected-link event, I'm not getting the data in the parent component.
PostList.vue
<template>
<div>
<div v-if='posts.length === 0' class="header">There are no posts yet!</div>
<form action="#" #submit.prevent="createPost()" class="publisher bt-1 border-fade bg-white" autocomplete="off">
<div class="input-group">
<input v-model="post.content" type="text" name="content" class="form-control publisher-input" placeholder="What's the lastest?" autofocus>
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">Post</button>
</span>
</div>
<span class="publisher-btn file-group">
<i class="fa fa-camera file-browser"></i>
<input type="file">
</span>
</form>
<div #click="doit" v-on:selected-link="onSelectedLink">{{ modellink.name }}</div>
<typeahead
source="/api/typeahead"
placeholder="Link Post to Trip, Business, etc"
filter-key="title"
:start-at="3">
</typeahead>
<post v-for="post in posts"
:key="post.id"
:post="post"
#post-deleted="deletePost($event)">
</post>
</div>
</template>
<script>
var axios = require("axios");
import post from './PostItem.vue';
import typeahead from './Typeahead.vue';
export default {
components: {
post,
typeahead
},
props: ['postableId', 'postableType', 'model'],
data: function() {
return {
modellink: {
"name": "n/a",
"description": "",
"id": null,
"model": "n/a"
},
post: {
id: 1,
content: "",
edited: false,
created_at: new Date().toLocaleString(),
user: {
id: 1,
name: '',
}
},
posts: [
{
id: 1,
content: "",
edited: false,
created_at: new Date().toLocaleString(),
user: {
id: 1,
name: '',
}
}
]
};
},
created() {
this.fetchPostsList();
},
methods: {
onSelectedLink: function (talink) {
alert(JSON.stringify(talink, null, 4));
this.link = talink
},
doit() {
alert(JSON.stringify(this.modellink, null, 4));
},
fetchPostsList() {
if( this.postableId ) {
axios.get('/api/' + this.postableType + '/' + this.postableId + '/posts').then((res) => {
this.posts = res.data;
});
} else {
axios.get('/api/post').then((res) => {
//alert(JSON.stringify(res.data[0], null, 4));
this.posts = res.data;
});
}
},
createPost() {
axios.post('api/post', {content: this.post.content, user_id: Laravel.userId, vessel_id: Laravel.vesselId })
.then((res) => {
this.post.content = '';
// this.post.user_id = Laravel.userId;
// this.task.statuscolor = '#ff0000';
this.edit = false;
this.fetchPostsList();
})
.catch((err) => console.error(err));
},
deletePost(post) {
axios.delete('api/post/' + post.id)
.then((res) => {
this.fetchPostsList()
})
.catch((err) => console.error(err));
},
}
}
</script>
Typeahead.vue
<template>
<div>
<input
v-model="query"
#blur="reset"
type="text"
class="SearchInput"
:placeholder="placeholder">
<transition-group name="fade" tag="ul" class="Results">
<li v-for="item in items" :key="item.id">
<span #click="sendlink(item)">
<strong>{{ item.name }}</strong> - <small>{{ item.model }}</small><br>
<small>{{ item.description }}</small>
</span>
</li>
</transition-group>
<p v-show="isEmpty">Sorry, but we can't find any match for given term :( </p>
</div>
</template>
<script>
var axios = require("axios");
export default {
name: 'Typeahead',
props: {
modellink: {
type: Object,
required: false
},
source: {
type: [String, Array],
required: true
},
filterKey: {
type: String,
required: true
},
startAt: {
type: Number,
default: 3
},
placeholder: {
type: String,
default: ''
}
},
data() {
return {
items: [],
query: '',
taitem: ''
}
},
computed: {
lookup() {
if(this.query.length >= this.startAt) {
axios.get(this.source + '/' + this.query).then((res) => {
this.items = res.data;
return res.data;
});
}
},
isEmpty() {
if( typeof this.lookup === 'undefined' ) {
return false
} else {
return this.lookup.length < 1
}
}
},
methods: {
sendlink: function (taitem) {
this.$emit('selected-link', taitem);
},
reset() {
this.query = ''
}
}
}
</script>

In your PostList.vue, move the v-on:selected-link="onSelectedLink" from the div to typeahead like below. When emitting an event from child to parent, the listener on the parent needs to be on the child component tag for it to work.
<div #click="doit">{{ modellink.name }}</div>
<typeahead
source="/api/typeahead"
placeholder="Link Post to Trip, Business, etc"
filter-key="title"
:start-at="3"
v-on:selected-link="onSelectedLink">
</typeahead>

Related

how to make an good working select input component in vue 3

Im trying to make an pagination with an select field but i dont get it how the v-model and the value properties should be given, for now i have like this ↓...
This is the select/pagination part in that component..
<p v-on:click="back" class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded-l cursor-pointer">Zurück</p>
<p class="bg-gray-300 text-gray-800 font-bold py-2 px-4 "><span>{{siteCount}}/{{siteTotal}}</span></p>
<select :value="modelValue" v-on:change="selectPage">
<option v-for="select in select" :value="select">{{select}}</option>
</select>
<p v-on:click="next" class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded-r cursor-pointer">Weiter</p>
</div>
</template>
<script>
import SearchIcon from "../Assets/SearchIcon";
import NavBar from "./NavBar";
export default {
name: "PaginateSeason",
props: {
data: {
name: String,
description: String,
thumbnail: String,
},
siteCount: Number,
siteTotal: Number,
select: Array,
modelValue: String,
},
components: {
NavBar,
SearchIcon
},
emits: ['next', 'back', 'selectPage'],
methods:{
next() {
this.$emit('next')
},
back() {
this.$emit('back')
},
selectPage() {
this.$emit('selectPage')
}
},
}
</script>
This is the main Component where i fetch anything i need...
<template>
<router-view>
<NavBar :is-disabled="true"></NavBar>
<div v-if="isLoading" class="flex flex-col items-center justify-center mt-9">
<LoaderSpinner></LoaderSpinner>
<p>Daten werden geladen...</p>
</div>
<div v-else>
<PaginateSeason
v-model="count"
:data="seasons"
:site-count="current"
:site-total="siteCountTotal"
:select="select"
#next="paginateNext"
#back="paginateBack"
#selectPage="allCount"
/>
</div>
</router-view>
</template>
<script>
import NavBar from './NavBar'
import PaginateSeason from './PaginateSeason'
import LoaderSpinner from "../Assets/LoaderSpinner";
export default {
name: "AllSeasons",
components: {
PaginateSeason,
NavBar,
LoaderSpinner
},
data(){
return {
seasons: null,
current: 1,
pageSize: 4,
isLoading: true,
siteCountTotal: null,
select: [],
count: '',
}
},
mounted(){
this.list(this.current)
const list = async () => {
await axios.get(`/api/seasons/all`).then(response => {
let meta = response.data.meta
this.siteCountTotal = meta.last_page
for (let i = 1; i <= this.siteCountTotal; i++) {
this.select.push(i)
}
}).catch(error => {
console.log(error)
})
}
list()
},
methods: {
async list(page) {
await axios.get(`/api/seasons/all?page=${page}`).then(response => {
this.seasons = response.data.data
}).catch(error => {
console.log(error)
}).finally(() => {
setTimeout(() => {
this.isLoading = false
},1000)
})
},
paginateNext () {
if (this.seasons.length < this.pageSize) {
this.current = 1
this.list(this.current)
} else {
this.current += 1
this.list(this.current)
}
},
paginateBack () {
if (this.current === 1) {
this.current -= 0
} else {
this.current -= 1
this.list(this.current)
}
},
allCount() {
console.log(this.count)
}
},
IF you can help me with that i would be very gratefull!
The count variable on the AllSeasons component will not be updated, because you pass it as v-model to the PaginateSeason component, but PaginateSeason never emits update:modelValue.
Implement selectPage like this to address this issue:
selectPage(event) {
this.$emit('update:modelValue', event.target.value)
}
You can also keep the selectPage event, or add a wach for the count variable in AllSeasons component to watch for page changes. You may also want to emit the update:modelValue event from next and back functions in the PaginateSeason component.

Vuex / Filter: preserve input value on navigation

Does anyone know how can I select an item from my filteredResults array and display it as the input's value? Furthermore, I need that on page navigation this input value to remain the same if I refresh or navigate back and forth through the pages.
<template>
<div class="search-input relative">
<label class="block">
<span class="text-gray-700 font-bold">{{ label }}</span>
<input
type="search"
class="mt-1 p-2 block min-w-full border-0 focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50 bg-transparent border-b-2 border-blue-500"
autocomplete="off"
:placeholder="placeholder"
v-model.trim="keyword"
#click="resetAirport"
>
</label>
<ul v-if="filteredResults != null" class="mt-2 absolute shadow-md rounded-lg">
<li v-for="result in filteredResults" :key="result.id" class="p-2 bg-white hover:bg-gray-100" #click="selectAirport(result)">
<NuxtLink to="/" class="block">
{{ result.icao }}, {{ result.iata }} {{ result.name }}
</NuxtLink>
</li>
</ul>
<NuxtLink :to="`/airport/LRAR`" class="text-gray-300 mt-2 text-xs text-right">Details</NuxtLink>
</div>
</template>
<script>
import { defineComponent } from '#vue/composition-api'
import { mapGetters, mapActions } from 'vuex'
export default defineComponent({
setup(props) {},
data() {
return {
keyword: '',
}
},
computed: {
...mapGetters({
getAirports: 'getAirports'
}),
filteredResults() {
if (this.keyword.length < 3) return null
return this.getAirports.filter(item => {
if (!item.icao) return false
return item.icao
.toString()
.toLowerCase()
.includes(this.keyword.toLowerCase())
})
},
},
methods: {
selectAirport: function(airport) {
this.addAirport({airport: airport, type: this.type})
this.keyword = `${airport.icao}, ${airport.iata} (${airport.city}) / ${airport.name}`
},
resetAirport: function() {
if (this.keyword != null) {
this.removeAirport(this.type)
this.keyword = ''
}
},
...mapActions([
'addAirport',
'removeAirport'
])
},
props: {
label: String,
placeholder: String,
type: String,
}
})
</script>
Here is my store:
export const state = () => ({
airports: [],
pairing: {
departure: null,
arrival: null
},
loading: false
})
export const mutations = {
SET_AIRPORTS(state, payload) {
state.airports = payload
},
SET_AIRPORT(state, { airport, type }) {
state.pairing[type] = airport
},
CLEAR_AIRPORT(state, type) {
state.pairing[type] = null
}
}
export const actions = {
addAirport({ commit }, { airport, type }) {
commit('SET_AIRPORT', { airport, type })
},
removeAirport({ commit }, type) {
commit('CLEAR_AIRPORT', type)
}
}

radio checked between 2 values

I am modifying a module on Prestashop (Blockwishlist) which uses VueJS and I want to add the available combinations in the customers' wish list in order to be able to select a combination and add it to the cart.
But I'm stuck on a part when loading the page: I get the id_product and the id_product_attribute and I also loop on all the variations of the product.
I am looking for a checked radio button by comparing the values ​​of my loop and the id_product_attribute value that I get back and if it exists in my loop this checked the radio button.
This is what my vuejs code looks like now (with attempts that don't work)
<template>
<div class="wishlist-product">
<a
class="wishlist-product-link"
:href="product.canonical_url"
>
<div class="wishlist-product-image">
<img
v-if="product.default_image"
:src="product.default_image.large.url"
:alt="product.default_image.legend"
:title="product.default_image.legend"
:class="{
'wishlist-product-unavailable': !product.add_to_cart_url
}"
>
<img
v-else
:src="prestashop.urls.no_picture_image.bySize.home_default.url"
>
</div>
<div class="wishlist-product-right">
<p class="wishlist-product-title">{{ product.name }}</p>
<p class="wishlist-product-price">
<span
class="wishlist-product-price-promo"
v-if="product.has_discount"
>
{{ product.regular_price }}
</span>
{{ product.price }}
</p>
<div class="wishlist-product-combinations">
<div class="product-variants">
<div class="clearfix product-variants-item">
<div id="group_1" class="radio">
<div v-for="(attribute, key, index) of product.all_attributes" class="input-container float-xs-left"
:class="{'no_stock_variant' : attribute.quantity <= 0}">
<label>
<input
#change="changeAttribute($event)"
v-model="id_product_attribute"
name="id_product_attribute"
class="input-radio" type="radio"
:data-id-product=attribute.id_product
:data-id-product-attribute=attribute.id_product_attribute
:data-product-attribute=attribute.id_attribute
:value="key"
v-if="checkedId(attribute.id_product_attribute) ? 'checked=checked' : ''"
:title=attribute.attribute_name>
<div class="content">
<span class="radio-label">
{{ attribute.attribute_name }}
<img v-if="attribute.quantity <= 0"
:src="imgEnveloppe"
:class="{hover: isHovering}"
#mouseover="imgEnveloppe = imgRing"
#mouseout="isHovering = false"
:alt="attribute.attribute_name"
>
</span>
</div>
</label>
</div>
</div>
</div>
</div>
</div>
</div>
</a>
<div class="wishlist-product-bottom">
<button
class="btn btn-indies-gold wishlist-button-add"
v-if="!isShare"
#click="removeFromWishlist"
>
{{ deleteText }}
</button>
<button
class="btn btn-indies wishlist-product-addtocart"
:disabled="isDisabled"
#click="
product.add_to_cart_url || product.customizable === '1'
? addToCartAction()
: null
"
>
{{ product.customizable === '1' ? customizeText : addToCart }}
</button>
</div>
<p
class="wishlist-product-availability wishlist-product-availability-responsive"
v-if="product.show_availability"
>
<i
class="material-icons"
v-if="product.availability === 'unavailable'"
>
block
</i>
<i
class="material-icons"
v-if="product.availability === 'last_remaining_items'"
>
warning
</i>
{{ product.availability_message }}
</p>
</div>
</template>
<script>
import EventBus from '#components/EventBus';
import headers from '#constants/headers';
import prestashop from 'prestashop';
import wishlistAddProductToCartUrl from 'wishlistAddProductToCartUrl';
export default {
name: 'Product',
props: {
product: {
type: Object,
required: true,
default: null,
},
deleteText: {
type: String,
required: true,
},
imgEnveloppe: {
type: String,
required: true,
},
imgRing: {
type: String,
required: true,
},
listId: {
type: Number,
required: true,
default: null,
},
isShare: {
type: Boolean,
required: false,
default: false,
},
customizeText: {
type: String,
required: true,
default: 'Customize',
},
quantityText: {
type: String,
required: true,
default: 'Quantity',
},
addToCart: {
type: String,
required: true,
},
status: {
type: Number,
required: false,
default: 0,
},
hasControls: {
type: Boolean,
required: false,
default: true,
},
},
data() {
return {
prestashop,
isHovering: false,
id_product_attribute: null
};
},
computed: {
isDisabled() {
if (this.product.customizable === '1') {
return false;
}
return !this.product.add_to_cart_url;
},
},
methods: {
checkedId (idProductAttribute) {
if (this.product.id_product_attribute === parseInt(idProductAttribute)) {
console.log(idProductAttribute);
return true;
}
return false;
},
changeAttribute(e) {
this.id_product_attribute = e.target.getAttribute('data-id-product-attribute');
},
/**
* Remove the product from the wishlist
*/
async removeFromWishlist() {
EventBus.$emit('showDeleteWishlist', {
detail: {
listId: this.listId,
productId: this.product.id,
productAttributeId: this.product.id_product_attribute,
},
});
},
async addToCartAction() {
if (this.product.add_to_cart_url && this.product.customizable !== '1') {
try {
const datas = new FormData();
datas.append('qty', this.product.wishlist_quantity);
datas.append('id_product', this.product.id_product);
datas.append('id_product_attribute', this.id_product_attribute);
datas.append('id_customization', this.product.id_customization);
const response = await fetch(
`${this.product.add_to_cart_url}&action=update`,
{
method: 'POST',
headers: headers.addToCart,
body: datas,
},
);
const resp = await response.json();
prestashop.emit('updateCart', {
reason: {
idProduct: this.product.id_product,
idProductAttribute: this.id_product_attribute,
idCustomization: this.product.id_customization,
linkAction: 'add-to-cart',
},
resp,
});
/* eslint-disable */
const statResponse = await fetch(
`${wishlistAddProductToCartUrl}&params[idWishlist]=${this.listId}&params[id_product]=${this.product.id_product}&params[id_product_attribute]=${this.id_product_attribute}&params[quantity]=${this.product.wishlist_quantity}`,
{
headers: {
'Content-Type':
'application/x-www-form-urlencoded; charset=UTF-8',
Accept: 'application/json, text/javascript, */*; q=0.01'
}
}
);
/* eslint-enable */
await statResponse.json();
} catch (error) {
prestashop.emit('handleError', {
eventType: 'addProductToCart',
resp: error,
});
}
} else {
window.location.href = this.product.canonical_url;
}
},
},
};
Thank you for help.

Error: Do not mutate vuex store state outside mutation handlers

Scenario
I am using Vuex, to store some data in it, and in my case the ticket details.
Initially, I have a ticket which has an array of discounts, to be empty.
Once I hit the button "Add discount" I mount the component called "testDiscount" which in the mounted hook pushes the first object ({"code": "Foo", "value":"Boo"}) in the discounts array of a specific ticket in the store.
The problem arise when I try to type in the input boxes (changing its state) in this component where I get the error "do not mutate Vuex store state outside mutation handlers.". How could I best handle this?
Test.vue
<template>
<div>
<test-component v-for="(t, key) in tickets" :key="key" :ticket-key="key" :tid="t.id"></test-component>
</div>
</template>
<script>
import TestComponent from "~/components/testComponent.vue";
export default {
layout: "noFooter",
components: {
"test-component": TestComponent,
},
data() {
return {
tickets: this.$store.state.ticketDiscount.tickets,
};
},
mounted() {
if (this.tickets.length == 0) {
this.$store.commit("ticketDiscount/addTicket", {
id:
this.$store.state.ticketDiscount.tickets.length == 0
? 0
: this.$store.state.ticketDiscount.tickets[
this.$store.state.ticketDiscount.tickets.length - 1
].id + 1,
discount: [],
});
}
},
};
</script>
ticketDiscount.js
export const state = () => ({
tickets: []
});
export const mutations = {
addTicket(state, ticket) {
state.tickets.push(ticket);
},
addDiscount(state, property) {
state.tickets.find(ticket => ticket.id == property.id)[property.name].push(property.value);
}
testComponent.vue
<template>
<div>
<h3>Ticket number: {{ticketKey + 1}}</h3>
<button #click="showDiscount = true">Add discount</button>
<test-discount v-model="discount_" v-if="showDiscount" :tid="tid"></test-discount>
</div>
</template>
<script>
import testDiscount from "~/components/test-discount.vue";
export default {
components: {
testDiscount,
},
data() {
return {
showDiscount: false,
tid_: this.tid,
};
},
props: {
tickets: Array,
ticketKey: { type: Number },
tid: { type: Number, default: 0 },
},
methods: {
updateTicket() {
this.$emit("updateTicket", {
id: this.tid_,
value: {
discount: this.discount_,
},
});
},
},
mounted() {
this.$watch(
this.$watch((vm) => (vm.discount_, Date.now()), this.updateTicket)
);
},
computed: {
discount_: {
get() {
return this.$store.state.ticketDiscount.tickets.find(
(ticket) => ticket.id == this.tid
)["discount"];
},
set(value) {
// set discount
},
},
},
};
</script>
testDiscount.vue
<template>
<div class="container">
<div class="title">
<img src="~/assets/svgs/price_tag.svg" />
<span>Discount code</span>
{{ discounts }}
</div>
<div class="discount-container">
<div v-for="(c,idx) in discounts" class="discounts" :key="idx">
<div class="perc-input">
<input style="max-width: 50px;" v-model.number="c.discount" type="number" min="1" max="100" step="1" placeholder="10">
<div>%</div>
</div>
<input class="code-input" v-model="c.code" placeholder="Code">
<img src="~/assets/svgs/bin.svg" title="Delete code" #click="deleteCode(idx)" v-if="discounts.length > 1"/>
</div>
</div>
<span #click="newDiscount" class="add-another">+ Add another discount</span>
</div>
</template>
<script>
export default {
props: {
value: {
type: Array,
},
tid: { type: Number, default: 0 },
},
data() {
return {
discounts: this.value,
}
},
mounted() {
if (this.discounts.length == 0) {
this.newDiscount();
}
},
methods: {
newDiscount() {
this.$store.commit('ticketDiscount/addDiscount',
{
"id": this.tid,
"name": "discount",
"value": { code: null,discount: null }
});
},
deleteCode(index) {
this.discounts.splice(index, 1);
}
},
watch: {
discounts() {
this.$emit('input', this.discounts)
}
},
beforeDestroy() {
this.$emit('input', []);
}
}
</script>
you shouldn't use v-model in this case.
<input style="max-width: 50px;" v-model.number="c.discount" .../>
you could just set the value
<input style="max-width: 50px;" :value="c.discount" #change="handleValueChange" .../>
and then in handleValueChange function to commit the action to update just for that value.

Filter array results on keyup

I have an array of names that I loop over using v-for I'm trying to filter these results when a user starts typing in a search box.
I've added my code below for reference if I do my loop as v-for="entry in entries" then it output the array but doesn't work with the computed and filteredList function
<template>
<div class="container-flex">
<div class="entries">
<div class="entries__header">
<div class="entries__header__title">
<p>Entries</p>
</div>
<div class="entries__header__search">
<input
type="text"
name="Search"
class="input input--search"
placeholder="Search..."
v-model="search">
</div>
</div>
<div class="entries__content">
<ul class="entries__content__list">
<li v-for="entry in filteredList">
{{ entry.name }}
</li>
</ul>
</div>
</div>
</div>
</template>
<script>
import addEntry from '#/components/add-entry.vue'
export default {
name: 'entry-list',
search: '',
components: {
addEntry
},
data: function() {
return {
entries: [
{
name: 'Paul'
},
{
name: 'Barry'
},
{
name: 'Craig'
},
{
name: 'Zoe'
}
]
}
},
computed: {
filteredList() {
return this.entries.filter(entry => {
return entry.name.toLowerCase().includes(this.search.toLowerCase())
})
}
}
}
Try to move the search prop in to data option like this:
export default {
name: 'entry-list',
components: {
addEntry
},
data: function() {
return {
search: '',
entries: [
{
name: 'Paul'
},
{
name: 'Barry'
},
{
name: 'Craig'
},
{
name: 'Zoe'
}
]
}
},
computed: {
filteredList() {
if(this.search === '') return this.entries
return this.entries.filter(entry => {
return entry.name.toLowerCase().includes(this.search.toLowerCase())
})
}
}
}
Also add a check if the search prop is empty to return the full entries list.
Demo fiddle