Vuetify item-group selected item changes when using firestore data - vue.js

I am trying to display several cards based on Firestore docs, I allow the user to select the card and take some actions with that data. But when a firestore document's data changes, and you already have selected a card, it will rearrange the cards and the 'selected card' will now be different.
1.) select card
2.) firestore data changes(updated live)
3.) the card you selected is now replaced with a new card (new index)
If you change the middle doc's rank to -6, the top card will move down, and be the new selected card
online: [{rank: 1, id: 3030303}, {rank: 2, id: 2020202}, {rank: 3, id: 1010101}]
I looked at a this stackoverflow question but the answer is not working/outdated (as well as others) Manipulating v-list-item-group model with object instead of list index
<template>
<div>
<v-container fluid>
<v-list>
<v-list-item-group v-model="listSelection">
<v-list-item v-for="(item, index) in online" :key="index" :value="item.id">
{{item}}
</v-list-item>
</v-list-item-group>
</v-list>
</v-container>
</div>
</template>
<script>
import db from "#/components/fbInit";
export default {
props: {
value: {}
},
data() {
return {
online: [],
selected: []
}
},
firestore: {
online: db.collection('item').orderBy('rank', 'asc')
},
methods: {
changeSelected(index) {
this.selected = index
this.$emit('change', this.selected);
},
},
computed: {
items: function() {
return this.online
},
listSelection: {
// get: function() { // commented out b/c this line breaks the page
// return this.value.id
// },
set: function(newVal) {
this.$emit('input', this.items.find(item => item.id === newVal));
}
}
},
}
</script>
I would like the selection to be bound to the firestore doc.id (i.e. unchanging doc name versus the always changing doc data)
Any help is greatly appreciated

Related

How do have unique variables for each dynamically created buttons/text fields?

I'm trying to create buttons and vue element inputs for each item on the page. I'm iterating through the items and rendering them with v-for and so I decided to expand on that and do it for both the rest as well. The problem i'm having is that I need to to bind textInput as well as displayTextbox to each one and i'm not sure how to achieve that.
currently all the input text in the el-inputs are bound to the same variable, and clicking to display the inputs will display them all at once.
<template>
<div class="container">
<div v-for="(item, index) in items" :key="index">
<icon #click="showTextbox"/>
<el-input v-if="displayTextbox" v-model="textInput" />
<el-button v-if="displayTextbox" type="primary" #click="confirm" />
<ItemDisplay :data-id="item.id" />
</div>
</div>
</template>
<script>
import ItemDisplay from '#/components/ItemDisplay';
export default {
name: 'ItemList',
components: {
ItemDisplay,
},
props: {
items: {
type: Array,
required: true,
},
}
data() {
displayTextbox = false,
textInput = '',
},
methods: {
confirm() {
// todo send request here
this.displayTextbox = false;
},
showTextbox() {
this.displayTextbox = true;
}
}
}
</script>
EDIT: with the help of #kissu here's the updated and working version
<template>
<div class="container">
<div v-for="(item, index) in itemDataList" :key="itemDataList.id">
<icon #click="showTextbox(item.id)"/>
<El-Input v-if="item.displayTextbox" v-model="item.textInput" />
<El-Button v-if="item.displayTextbox" type="primary" #click="confirm(item.id)" />
<ItemDisplay :data-id="item.item.uuid" />
</div>
</div>
</template>
<script>
import ItemDisplay from '#/components/ItemDisplay';
export default {
name: 'ItemList',
components: {
ItemDisplay,
},
props: {
items: {
type: Array,
required: true,
},
}
data() {
itemDataList = [],
},
methods: {
confirm(id) {
const selected = this.itemDataList.find(
(item) => item.id === id,
)
selected.displayTextbox = false;
console.log(selected.textInput);
// todo send request here
},
showTextbox(id) {
this.itemDataList.find(
(item) => item.id === id,
).displayTextbox = true;
},
populateItemData() {
this.items.forEach((item, index) => {
this.itemDataList.push({
id: item.uuid + index,
displayTextbox: false,
textInput: '',
item: item,
});
});
}
},
created() {
// items prop is obtained from parent component vuex
// generate itemDataList before DOM is rendered so we can render it correctly
this.populateItemData();
},
}
</script>
[assuming you're using Vue2]
If you want to interact with multiple displayTextbox + textInput state, you will need to have an array of objects with a specific key tied to each one of them like in this example.
As of right now, you do have only 1 state for them all, meaning that as you can see: you can toggle it for all or none only.
You'll need to refactor it with an object as in my above example to allow a case-per-case iteration on each state individually.
PS: :key="index" is not a valid solution, you should never use the index of a v-for as explained here.
PS2: please follow the conventions in terms of component naming in your template.
Also, I'm not sure how deep you were planning to go with your components since we don't know the internals of <ItemDisplay :data-id="item.id" />.
But if you also want to manage the labels for each of your inputs, you can do that with nanoid, that way you will be able to have unique UUIDs for each one of your inputs, quite useful.
Use an array to store the values, like this:
<template>
<div v-for="(item, index) in items" :key="index">
<el-input v-model="textInputs[index]" />
</div>
<template>
<script>
export default {
props: {
items: {
type: Array,
required: true,
},
},
data() {
textInputs: []
}
}
</script>

V-autocomplete not updating on user interaction

I am having 3 multi-select elements on a page and one button. On click on the button, I want whatever the value selected on the third i.e last multi-select element, shall get selected on all top two multi-select elements. On clicking on button, i do see the values are getting updated in vuex.state.vModelValues based on the given key, but the updated values are not reflecting on the first 2 multi-select.
Note: I need to get/set the value of the first 2 multi-select from a JSON object based on the key/value pair. I can use an individual array with v-model with first 2 multi-select element, but i don't want to go with that approach as i may have 100 multi-selects element and can't define 100 array in data section. So want to define a JSON which will hold the key/value for each multi-selects and should be reactive. If the value of one entry change, the key that mapped to multi-select shall get updated immediately.
I am not sure what i am doing wrong here. The value are updated on store but the updated value are not get selected on UI. Please help.
Below is my code:
1. STORE
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({
state() {
return {
vModelValues: {},
};
},
getters: {
getVModelPlaceholder(state) {
return state.vModelValues;
},
getVModelValue(state, key) {
return state.vModelValues[key];
},
},
mutations: {
setVModelValue(state, payload) {
state.vModelValues[payload.key] = payload.value;
},
},
actions: {
setVModelValue(context, payload) {
context.commit("setVModelValue", payload);
},
},
modules: {},
});
APP.VUE
<template>
<v-app>
<v-main>
<HelloWorld />
</v-main>
</v-app>
</template>
<script>
import HelloWorld from "./components/HelloWorld";
export default {
name: "App",
components: {
HelloWorld,
},
data: () => ({}),
mounted: function () {
this.$store.dispatch("setVModelValue", {
key: "listbox_checkbox_ID1",
value: ["Three"],
});
this.$store.dispatch("setVModelValue", {
key: "listbox_checkbox_ID2",
value: ["Two"],
});
},
};
</script>
HelloWorld.vue
<template>
<v-container>
<div id="container_dummy_PID1" style="height: auto; width: 100%">
<v-autocomplete
id="listbox_checkbox_1"
:items="ranges"
v-model="$store.state.vModelValues['listbox_checkbox_ID1']"
hide-details
multiple
dense
return-object
cache-items
>
</v-autocomplete>
<v-autocomplete
id="listbox_checkbox_2"
:items="ranges"
v-model="$store.state.vModelValues['listbox_checkbox_ID2']"
hide-details
multiple
dense
return-object
cache-items
>
</v-autocomplete>
<v-autocomplete
v-model="selectedValues"
:items="ranges"
hide-details
multiple
dense
required
return-object
>
</v-autocomplete>
<v-btn color="primary" #click="applyToAll()">Apply to All</v-btn>
</div>
</v-container>
</template>
<script>
export default {
name: "HelloWorld",
data() {
return {
selectedValues: "",
ranges: ["One", "Two", "Three", "Four", "Five"],
};
},
methods: {
applyToAll() {
this.$store.dispatch("setVModelValue", {
key: "listbox_checkbox_ID1",
value: this.selectedValues,
});
this.$store.dispatch("setVModelValue", {
key: "listbox_checkbox_ID2",
value: this.selectedValues,
});
},
},
};
</script>

(vuetify in nuxt js) autocomplete isnt update relative to items prop

Every input in search i update the items prop but the v-autocomplete become empty
although the data in my component changed
i tried to add the no-filter prop it didnt help i guess something with the reactivity destroyed
i allso tried with computed property as an items but still same result
Every input in search i update the items prop but the v-autocomplete become empty
although the data in my component changed
i tried to add the no-filter prop it didnt help i guess something with the reactivity destroyed
i allso tried with computed property as an items but still same result
<script>
import ProductCartCard from "~/components/cart/ProductCartCard";
export default {
name: "search-app",
components: {
ProductCartCard
},
props: {
items: {
type: Array,
default: () => []
}
},
data() {
return {
loading: false,
filteredItems: [],
search: null,
select: null
};
},
watch: {
search(val) {
if (!val || val.length == 0) {
this.filteredItems.splice(0, this.filteredItems.length);
return;
} else {
val !== this.select && this.querySelections(val);
}
}
},
methods: {
querySelections(v) {
this.loading = true;
// Simulated ajax query
setTimeout(() => {
this.filteredItems.splice(
0,
this.filteredItems.length,
...this.items.filter(i => {
return (i.externalName || "").toLowerCase().includes((v || "").toLowerCase());
})
);
this.loading = false;
}, 500);
}
}
};
</script>
<template>
<div class="search-app-container">
<v-autocomplete
v-model="select"
:loading="loading"
:items="filteredItems"
:search-input.sync="search"
cache-items
flat
hide-no-data
hide-details
label="searchProduct"
prepend-icon="mdi-database-search"
solo-inverted
>
<template v-slot:item="data">
<ProductCartCard :regularProduct="data" />
</template>
</v-autocomplete>
</div>
</template>
One of the caveat of the v-autocomplete as described in the documentation:
When using objects for the items prop, you must associate item-text and item-value with existing properties on your objects. These values are defaulted to text and value and can be changed.
That may fix your issue

How to get item value in v-slot:cell() template of b-table - BootstrapVue

I'm a very new at programming. I'm trying to figure it out how to bind the data to get the link :href work using store, vuex and bootstrap-vue table. I have spent 4 days for this, and now I'm dying. Please help.
books.js(store, vuex)
books: [
{
id: 1,
name: "name 1",
bookTitle: "book1",
thumbnail: '../../assets/img/book01.jpeg',
url: "https://www.google.com",
regDate: '2019-10'
},
{
id: 2,
name: "name2",
bookTitle: "book2",
thumbnail: "book2",
url: "http://www.yahoo.com",
regDate: '2019-10'
},
BookList.vue
<script>
export default {
name: "BookList",
components: {
},
computed: {
fields() {
return this.$store.state.fields
},
books() {
return this.$store.state.books
},
bookUrl() {
return this.$store.state.books.url
}
},
data() {
return {
itemFields: this.$store.state.fields,
items: this.$store.state.books,
//url: this.$store.state.books.url
}
}
};
</script>
<template>
<b-container>
<b-table striped hover :items="items" :fields="itemFields" >
<template v-slot:cell(thumbnail)="items">
<img src="" alt="image">
</template>
<template v-slot:cell(url)="items">
<b-link :href="bookUrl" >link</b-link>
</template>
</b-table>
</b-container>
</template>
The cell slot contains two properties you're generally interested in:
item (the current row, or, to be exact, the current item in items)
value (the cell - or, to be exact, the value of the current column within the item).
Therefore, considering your data, in the case of v-slot:cell(url)="{ value, item }", value is equivalent to item.url
Any of these would work:
<template v-slot:cell(url)="{ value }">
<b-link :href="value">link</b-link>
</template>
<template v-slot:cell(url)="slot">
<b-link :href="slot.value">{{ slot.item.bookTitle }}</b-link>
</template>
<template v-slot:cell(url)="{ item }">
<b-link :href="item.url">{{ item.bookTitle }}</b-link>
</template>
Working example here.
Note your question contains a few minor issues which might prevent your code from working (itemFields is referenced but not defined, not using proper getters, etc...). For details have a look at the working example.
And read the docs!

Vue emmitting component property appears to skip parent

I've been stuck on this for a long time now and I can't find anything remotely similar to this problem online.
Context: Simple restaurant review website.
My application needs to be able to take webcam photos using WebRTC or upload photos from storage. I built a component to do this called AddImageDialog that lets the user select either webcam or upload a stored image by storing the image URL in prop on this component and then emitting it. The parent then handles where to place the image.
One part of my program that requires this is the NewReview component, that allows 5 photos to be uploaded, all using the same AddImageComponent.
There is a page for users to upload a new restaurant They may also leave a review on this page, so the Review component with the AddImageDialog is imported onto this page. This page must also allow the user to upload an official photo for the venue, so I have reused my AddImagedDialog on this page.
The problem... When uploading review photos (on the new restaurant page) whenever uploading via webcam, everything works fine but when uploading a stored image, the image seems to be emmitted but skips the NewReview parent component and jumps right back to the grandparent - NewRestaurant.
AddImageDialog.vue
<template>
<div>
<v-layout row justify-center>
<v-dialog v-model="thisShowDialog" max-width="550px">
<v-card height="350px">
<v-card-title>
Take a new photo or choose an existing one...
</v-card-title>
<v-card-text>
<div>
<!-- Add a new photo option -->
<div class="clickBox left" #click="showCamera = true">
<v-container fill-height>
<v-layout align-center>
<v-flex>
<v-icon size="40pt" class="enlarge">add_a_photo</v-icon>
</v-flex>
</v-layout>
</v-container>
</div>
<!-- Add photo from library -->
<div class="clickBox right" #click="uploadImage">
<v-container fill-height>
<v-flex>
<input type="file" class="form-control" id="file-input" v-on:change="onFileChange">
<v-icon size="40pt" class="enlarge">photo_library</v-icon>
</v-flex>
</v-container>
</div>
</div>
</v-card-text>
</v-card>
</v-dialog>
</v-layout>
<Camera
:showCamera.sync="showCamera"
:photoData.sync="thisPhotoData">
</Camera>
</div>
</template>
<script>
import Camera from '#/components/Camera'
export default {
props: ['showDialog', 'photoData'],
data () {
return {
thisShowDialog: false,
showCamera: false,
thisPhotoData: null
}
},
methods: {
uploadImage () {
var fileUpload = document.getElementById('file-input') // createElement('input', {type: 'file'})
fileUpload.click()
},
onFileChange (event) {
let files = event.target.files || event.dataTransfer.files
if (!files.length) {
return
}
this.createImage(files[0])
this.thisShowDialog = false
this.thisShowDialog = false
},
createImage (file) {
let reader = new FileReader()
reader.onload = e => {
alert('reader loaded')
this.thisPhotoData = e.target.result
}
reader.readAsDataURL(file)
}
},
watch: {
showDialog () {
this.thisShowDialog = this.showDialog
},
thisShowDialog () {
this.$emit('update:showDialog', this.thisShowDialog)
},
showCamera () {
// when hiding the camera, leave this component too.
if (!this.showCamera) {
this.thisShowDialog = false
}
},
thisPhotoData () {
alert('emmiting from add image comp')
this.$emit('update:photoData', this.thisPhotoData)
}
},
components: {
Camera
}
}
</script>
Sorry to post so much code, but I literally have no idea what's causing this problem.
You will notice I have tried to get both webcam and image uploads to work in the same way by assigning to a `photoData' variable and emitting it whenever it's been changed. I'm confused as to why they behave differently.
NewReview.vue
(this is the component being skipped by stored imaged uploads)
<template>
<!-- LEAVE A REVIEW -->
<v-expansion-panel class="my-4" popout>
<v-expansion-panel-content expand-icon="mode_edit">
<div slot="header">Been here before? Leave a review...</div>
<v-card class="px-3">
<v-alert :value="isLoggedIn" type="warning">
You are not logged in. Log in.
</v-alert>
<v-form v-model="validReview">
<!-- Title -->
<v-text-field
name="title"
label="Review Title"
v-model="thisReview.title"
:rules="reviewTitleRules">
</v-text-field>
<!-- Body -->
<v-text-field
name="body"
label="Review Body"
multi-line
v-model="thisReview.reviewBody"
:rules="reviewBodyRules">
</v-text-field>
<v-card-actions>
<!-- Submit -->
<v-btn v-if="canSubmit" :disabled="!validReview" class="primary" #click="submitReview">Submit</v-btn>
<!-- Select Rating -->
<star-rating-input
class="ratingSelectStyle"
:star-size="25"
active-color="#E53935"
v-model="thisReview.reviewRating"
:show-rating="false">
</star-rating-input>
<v-spacer />
<!-- Add and View 5 review images -->
<div v-for="i in 5" :key="i" #click="addImage(i-1)">
<review-image
:photoData="reviewPhotos[i-1]"
:id="i"
></review-image>
</div>
<!-- Add Images Component -->
<AddImageDialog
:showDialog.sync="showCamera"
:photoData.sync="photoData"
></AddImageDialog>
</v-card-actions>
</v-form>
</v-card>
</v-expansion-panel-content>
</v-expansion-panel>
</template>
<script>
import RestaurantService from '../services/RestaurantService'
import ReviewImage from '#/components/ReviewImage'
import StarRatingInput from 'vue-star-rating'
import {mapGetters} from 'vuex'
import AddImageDialog from '#/components/AddImageDialog'
export default {
props: ['review', 'restaurantID'],
data () {
return {
showCamera: false,
photoData: '',
reviewPhotos: ['', '', '', '', ''],
currentReviewPhoto: 0,
thisReview: this.review || {
title: '',
reviewBody: '',
reviewRating: 5,
user: 'Anon'
},
validReview: true,
reviewTitleRules: [
s => !!s || 'Title is required',
s => s.length >= 5 || 'Title must be at least 5 characters'
],
reviewBodyRules: [
s => !!s || 'Body is required',
s => s.length >= 10 || 'Body must be at least 10 characters'
]
}
},
methods: {
addImage (i) {
this.currentReviewPhoto = i
this.showCamera = true
// the rest of this is handled by watching for photodata to be changed by the camera component.
},
setName () {
this.thisReview.user = this.$store.state.isLoggedIn ? this.$store.state.user.name : 'Anon'
},
submitReview: async function () {
try {
await RestaurantService.submitReview(this.restaurantID, this.thisReview).then(res => {
if (res.status === 200) {
this.$store.dispatch('setNoticeText', 'Review Submitted')
}
})
} catch (err) {
if (err.response.status === 404) {
// console.log('404 - Failed to submit')
this.$store.dispatch('setNoticeText', '404 - Failed to submit review')
} else {
this.$store.dispatch('setNoticeText', 'An unexpected problem has occured. [' + err.response.status + ']')
}
}
}
},
computed: {
...mapGetters({isLoaded: 'isLoaded'}),
canSubmit () {
if (this.restaurantID) {
return true
} else {
return false
}
},
isLoggedIn () {
if (this.isLoaded && !this.$store.state.isLoggedIn) {
return true
} else {
return false
}
}
},
watch: {
thisReview: {
handler () {
this.$emit('update:review', this.thisReview)
},
deep: true
},
isLoaded () {
if (this.isLoaded) {
this.setName()
}
},
photoData () {
if (this.photoData !== '') {
this.reviewPhotos[this.currentReviewPhoto] = this.photoData
this.photoData = ''
}
}
},
components: {
StarRatingInput,
ReviewImage,
AddImageDialog
},
mounted () {
this.setName()
}
}
</script>
This works by defining 5 images for a review and an array of 5 strings, then depending on what image was selected, assigning the emmitted image from AddImageDialog to the appropriate array by watching for photo data. As mention, this watcher function will work with the webcam, but is skipped when uploading images.
I'm not even sure if whatever is causing this problem is in this code, but any suggestions would be greatly appreciated :)