Vue.js store data as dictionary using v-model - vue.js

I have
<v-switch
v-model="Books" v-for="option in options"
:value="option.id" :label="option.display_name"
:key="option.id"
:disabled="disabled"
color="primary" dense hoverable>
</v-switch>
and
export default{
name: Books
data: () => ({
Books: []
})
}
I want to store my data as objects where option.id is the key, and inside you have id and presence. If the switch wasn't selected that presence is null
"97":{"id":"97","presence":"1"},
"98":{"id":"98","presence":"1"},
"99":{"id":"99","presence":null},
How would I do that ?

You can do that with Array.prototype.reduce.
const arrayToObject = (array, keyField) =>
array.reduce((obj, item) => {
obj[item[keyField]] = item
return obj
}, {})
const convertedBooks = arrayToObject(books, "id")
This is a common problem, and there a lot of articles that guide you on how to convert an array to a dictionary, for example here is one

Related

v-rating not displaying the value stored in the firestore database

need a bit of help here. I want to create a contractor's performance rating where we can rate the contractor using vuetify v-rating and save it into firebase firestore.
I succeeded to update rating and save it in firestore but whenever I refresh the page it shows an empty rating slots like this rating slot is empty after refresh, not showing the rating that i've just key-in even though it is stored in firestore.
it does show up if I want it to display as a number, like this: it shows the rating value if display it as a number.
How to display it in the form of star rating itself?
<template>
<div>
<v-data-table
dense
:headers="headers"
:items="subwork"
:loading="loading"
class="elevation-1"
style="margin: 3%;">
<template v-slot:[`item.rating`]="{ item }">
<v-rating
color="yellow"
half-increments
#input="giveRating(item, $event)"
item-value="rating"></v-rating>
</template>
<template v-slot:no-data>
<v-btn color="primary" #click="initialize"> Reset </v-btn>
</template>
</v-data-table>
</div>
</template>
To load the document & add the rating to firestore
methods: {
initialize() {
this.loading = true
this.subwork = []
firestore
.collection('registersubwork')
.get()
.then((querySnapshot) => {
querySnapshot.forEach((doc) => {
// doc.data() is never undefined for query doc snapshots
this.subwork.push({ ...doc.data(), id: doc.id, })
this.loading = false
})
console.log(this.subwork)
})
},
giveRating(item, value) {
this.editedIndex = this.subwork.indexOf(item);
this.editedItem = Object.assign({}, item);
console.log(item, value)
if (this.editedIndex > -1) {
Object.assign(this.subwork[this.editedIndex], this.editedItem);
} else {
this.subwork.push(this.editedItem);
}
var data = {
rating: this.editedItem.rating,
};
firestore
.collection("registersubwork")
.doc(this.editedItem.id)
.set({ rating: value }, { merge: true })// .update(data)
.then(() => {
console.log("Rating updated succesfully!");
})
.catch(e => {
console.log(e);
});
},
},
Thanks!
You're using item-value="rating". as far as i know it's value="5" and for binding it should be :value="item.rating" try changing this. if you don't have rating with each item make sure to check that.

how to select a default value in q-select from quasar?

I need my q-select to select the "name" depending on the previously assigned "id".
Currently the input shows me the "id" in number and not the name to which it belongs.
<q-select
class="text-uppercase"
v-model="model"
outlined
dense
use-input
input-debounce="0"
label="Marcas"
:options="options"
option-label="name"
option-value="id"
emit-value
map-options
#filter="filterFn"
#update:model-value="test()"
>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey"> No results </q-item-section>
</q-item>
</template>
</q-select>
Example I would like the name that has the id: 12 to be shown loaded in the q-select.
const model = ref(12);
const options = ref([]);
const filterFn = (val, update) => {
if (val === "") {
update(() => {
options.value = tableData.value;
});
return;
}
update(() => {
const needle = val.toLowerCase();
options.value = tableData.value.filter((v) =>
v.name.toLowerCase().includes(needle)
);
});
};
I'm running into the same issue and couldn't find a proper way to set default value of object type.
I ended up use find() to look for the default value in the options and assign it on page created event.
created() {
this.model = this.options.find(
(o) => o.value == this.model
);
}

Searching data by foreign key and display it in bootstrap-vue table

Basing on this How to render custom data in Bootstrap-Vue Table component? I created this code:
assignDocuments(id) {
const documents = this.$store.state.documentList.filter(e => e.user.idUser === id);
console.log(documents);
let i;
for(i=0; i < documents.length;i++) {
return documents[i] ? `${documents[i].filename}` : 'loading...';
}
}
but it doesn't work like I need... I need to display the names(filename) of all objects(in this case I have 2 objects in documents array) in array but now only name of first object is displayed in b-table.
EDIT:
B-table code:
<b-table ref="table" small striped hover :items="$store.state.userList" :fields="fields" head-variant="dark">
<template v-slot:cell(indexNumber)="row">
{{ row.item.indexNumber}}
</template>
<template v-slot:cell(name)="row">
{{ row.item.name}}
</template>
<template v-slot:cell(surname)="row">
{{ row.item.surname}}
</template>
<template v-slot:cell(specialization.specName)="row">
{{ row.item.specialization.specName}}
</template>
<template v-slot:cell(yearbook)="row">
{{ row.item.yearBook.startYear }}<b>/</b>{{ row.item.yearBook.endYear }}
</template>
<template v-slot:cell(details)="row">
<b-button size="sm" #click="row.toggleDetails" class="mr-2">
{{row.detailsShowing ? 'Ukryj' : 'Pokaż'}} Szczegóły
</b-button>
</template>
</b-table>
Fields:
fields:[
{
key: 'indexNumber',
label: 'Numer indeksu'
},
{
key: 'name',
label: 'Imię'
},
{
key: 'surname',
label: 'Nazwisko',
},
{
key: 'specialization.specName',
label: 'Kierunek',
},
{
key: 'yearBook',
label: 'Rocznik',
},
{
key: 'idUser',
label: 'Documents',
formatter: 'assignDocuments'
},
{
key: 'details',
label: 'Szczegóły',
},
],
It's because in your function you are returning after the first iteration of your loop. You instead need to get all the file names, and then return:
In the example that you shared, they are only assigning one value; the first/ last name as a string. In your case, it appears that you have multiple documents that could be related to a single row in the table.
Try changing your code to return all the docs:
assignDocuments(id) {
const documents = this.$store.state.documentList.filter(e => e.user.idUser === id);
console.log(documents);
let fileNames = [];
// Get the file name for each doc and put into our array
documents.forEach(elem => fileNames.push(elem.filename));
// Return the file names as a concatenated string
return filesNames.length > 0 ? fileNames.join() : 'loading...';
}
Edit:
Based on your comment, I have updated so that you can place this in buttons later on! What you could do is utilize the same function, and then dynamically create your buttons based on the docs that you have for that row.
<template>
<div>
<b-table :items="someTableItems" :fields="fields">
<button v-for="(doc, index) in assignDocuments(someIdHere)"
:key="`docBtn-${index}`"
:class="doc.cssClassObject">
{{ doc.filename }}
</button>
</b-table>
</div>
</template>
assignDocuments(id) {
const documents = this.$store.state.documentList.filter(e => e.user.idUser === id);
console.log(documents);
let files= [];
// Get the file name for each doc and put into our array
documents.forEach(elem => {
const isAccepted = elem.status === "accepted";
files.push({
status: elem.status,
filename: elem.filename,
cssClassObject : { accepted: isAccepted, rejected: !isAccepted }
});
});
// Return the files as an array
return files.length > 0 ? files : 'loading...';
}

How to solve v-switch vuetify only binds one way?

My v-switch from vuetify is only binding one way.
If i load in my data it switches on or off. so its working if i load data in the v-model of the v-switch.
But if i switch the v-switch, it switches off, but does not change anything.
here is the code:
<v-data-table :headers="datatable.headers" :items="datatable.items" class="elevation-1">
<template v-slot:body="{ items }">
<tr v-for="(item, index) in items" :key="index">
<td>{{item.name}}</td>
<td #click="() => { $router.push(`/settings/${item.name.toLowerCase()}`) }"><v-icon small>edit</v-icon></td>
<td><v-switch v-model="inMenu[item.name.toLowerCase()]" :label="`Switch 1: ${inMenu[item.name.toLowerCase()]}`"></v-switch></td>
</tr>
</template>
</v-data-table>
<script>
export default {
data() {
return {
tabs: [
'Content types'
],
tab: null,
datatable: {
items: [],
headers: [{
text: 'Content types', value: "name"
}]
},
settings: null,
inMenu: {},
}
},
mounted() {
this.$axios.get('/settings.json').then(({data}) => {
this.settings = data
});
this.$axios.get('/tables.json').then(({data}) => {
// set all content_types
data.map(item => {
this.datatable.items.push({
name: item
})
})
// check foreach table if it's in the menu
this.datatable.items.forEach(item => {
this.inMenu[item.name.toLowerCase()] = JSON.parse(this.settings.menu.filter(menuitem => menuitem.key == item.name.toLowerCase())[0].value)
})
})
},
updated() {
console.log(this.inMenu)
}
}
</script>
so i clicked on the first switch and it does not change the state
i tried to have a normal prop in the data function.
i made a switch: null prop and it will react fine to that, but not to my code.
Any idea?
My guess is that your data is not reactive when you write:
// check foreach table if it's in the menu
this.datatable.items.forEach(item => {
this.inMenu[item.name.toLowerCase()] = JSON.parse(this.settings.menu.filter(menuitem => menuitem.key == item.name.toLowerCase())[0].value)
})
You should use the $set method instead and write:
// check foreach table if it's in the menu
this.datatable.items.forEach(item => {
this.$set(this.inMenu, item.name.toLowerCase(), JSON.parse(this.settings.menu.filter(menuitem => menuitem.key == item.name.toLowerCase())[0].value)
}))
See https://v2.vuejs.org/v2/guide/reactivity.html for more information on reactivity
Does this solve your problem?

vuetify autocomplete allow unknown items between chips

I am trying to modify the sample code at https://vuetifyjs.com/en/components/autocompletes#example-scoped-slots to allow arbitrary content not matching any autocomplete items in between chips (so user can tag other users in a message similar to slack and facebook)
So for example, the user could type "Sandra" and then select "sandra adams", then type "foo" and then type another space and start typing "John" and the autcomplete would pop up again and allow the user to select "John Smith".
I've been through all the properties in the docs and there doesn't seem to be support for this built in.
I tried using custom filtering to ignore the irrelevant parts of the message when displaying autocomplete options, but the autocomplete seems to remove non-chip content when it loses focus and I can't see a property that allows me to prevent this behavior.
not sure if the autcomplete is the thing to be using or if I would be better off hacking combo box to meet this requirement, because this sample seems closer to what I'm tryng to do https://vuetifyjs.com/en/components/combobox#example-no-data, but then I believe I lose the ajax capabilities that come with automcomplete.
You can achieve this by combining the async search of the autocomplete with the combobox.
For example:
new Vue({
el: '#app',
data: () => ({
activator: null,
attach: null,
colors: ['green', 'purple', 'indigo', 'cyan', 'teal', 'orange'],
editing: null,
descriptionLimit: 60,
index: -1,
nonce: 1,
menu: false,
count: 0,
model: [],
x: 0,
search: null,
entries: [],
y: 0
}),
computed: {
fields () {
if (!this.model) return []
return Object.keys(this.model).map(key => {
return {
key,
value: this.model[key] || 'n/a'
}
})
},
items () {
return this.entries.map(entry => {
const Description = entry.Description.length > this.descriptionLimit
? entry.Description.slice(0, this.descriptionLimit) + '...'
: entry.Description
return Object.assign({}, entry, { Description })
})
}
},
watch: {
search (val, prev) {
// Lazily load input items
axios.get('https://api.publicapis.org/entries')
.then(res => {
console.log(res.data)
const { count, entries } = res.data
this.count = count
this.entries = entries
})
.catch(err => {
console.log(err)
})
.finally(() => (this.isLoading = false))
/*if (val.length === prev.length) return
this.model = val.map(v => {
if (typeof v === 'string') {
v = {
text: v,
color: this.colors[this.nonce - 1]
}
this.items.push(v)
this.nonce++
}
return v
})*/
},
model (val, prev) {
if (val.length === prev.length) return
this.model = val.map(v => {
if (typeof v === 'string') {
v = {
Description: v
}
this.items.push(v)
this.nonce++
}
return v
})
}
},
methods: {
edit (index, item) {
if (!this.editing) {
this.editing = item
this.index = index
} else {
this.editing = null
this.index = -1
}
},
filter (item, queryText, itemText) {
const hasValue = val => val != null ? val : ''
const text = hasValue(itemText)
const query = hasValue(queryText)
return text.toString()
.toLowerCase()
.indexOf(query.toString().toLowerCase()) > -1
}
}
})
<link href='https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons' rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/vuetify/dist/vuetify.min.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vuetify/dist/vuetify.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js" integrity="sha256-mpnrJ5DpEZZkwkE1ZgkEQQJW/46CSEh/STrZKOB/qoM=" crossorigin="anonymous"></script>
<div id="app">
<v-app>
<v-content>
<v-container>
<v-combobox
v-model="model"
:filter="filter"
:hide-no-data="!search"
:items="items"
:search-input.sync="search"
hide-selected
label="Search for an option"
:allow-overflow="false"
multiple
small-chips
solo
hide-selected
return-object
item-text="Description"
item-value="API"
:menu-props="{ closeOnClick: false, closeOnContentClick: false, openOnClick: false, maxHeight: 200 }"
dark
>
<template slot="no-data">
<v-list-tile>
<span class="subheading">Create</span>
<v-chip
label
small
>
{{ search }}
</v-chip>
</v-list-tile>
</template>
<template
v-if="item === Object(item)"
slot="selection"
slot-scope="{ item, parent, selected }"
>
<v-chip
:selected="selected"
label
small
>
<span class="pr-2">
{{ item.Description }}
</span>
<v-icon
small
#click="parent.selectItem(item)"
>close</v-icon>
</v-chip>
</template>
<template
slot="item"
slot-scope="{ index, item, parent }"
>
<v-list-tile-content>
<v-text-field
v-if="editing === item.Description"
v-model="editing"
autofocus
flat
hide-details
solo
#keyup.enter="edit(index, item)"
></v-text-field>
<v-chip
v-else
dark
label
small
>
{{ item.Description }}
</v-chip>
</v-list-tile-content>
</template>
</v-combobox>
</v-container>
</v-content>
</v-app>
</div>
so I ended up building a renderless component that is compatible with vuetify as it goes through the default slot and finds any of the types of tags (textarea, input with type of text, or contenteditable) that tribute supports, and allows you to put arbitrary vue that will be used to build the tribute menu items via a scoped slot.
in future might try to wrap it as a small NPM package to anyone who wants a declarative way to leverage tribute.js for vue in a more flexible way than vue-tribute allows, but for now here's my proof of concept
InputWithMentions.vue
<script>
import Tribute from "tributejs"
// eslint-disable-next-line
import * as css from "tributejs/dist/tribute.css"
import Vue from "vue"
export default {
mounted() {
let menuItemSlot = this.$scopedSlots.default
let tribute = new Tribute({
menuItemTemplate: item =>
{
let menuItemComponent =
new Vue({
render: function (createElement) {
return createElement('div', menuItemSlot({ menuItem: item }))
}
})
menuItemComponent.$mount()
return menuItemComponent.$el.outerHTML
},
values: [
{key: 'Phil Heartman', value: 'pheartman'},
{key: 'Gordon Ramsey', value: 'gramsey'}
]})
tribute.attach(this.$slots.default[0].elm.querySelectorAll('textarea, input[type=text], [contenteditable]'))
},
render(createElement) {
return createElement('div', this.$slots.default)
}
}
</script>
User.vue
<InputWithMentions>
<v-textarea
box
label="Label"
auto-grow
value="The Woodman set to work at once, and so sharp was his axe that the tree was soon chopped nearly through.">
</v-textarea>
<template slot-scope="{ menuItem }">
<v-avatar size="20" color="grey lighten-4">
<img src="https://vuetifyjs.com/apple-touch-icon-180x180.png" alt="avatar">
</v-avatar>
{{ menuItem.string }}
</template>
</InputWithMentions>