I am practicing the code example below.
I applied Vuetify.
This is the edit screen showing the data fetched from the API.
The problem is that the selected v-radio is not checked.
What is the problem?
<v-radio-group v-model="designType" :mandatory="false" row>
<v-radio
v-for="n in 3"
:key="n"
:label="`Type ${n}`"
:value="n"
:checked="n === designType"
:aria-checked="n === designType"
/>
{{designType}} //<<----- test value is no problem
</v-radio-group>
...
// script
data() => ({
...
designType: 1,
...
}),
mounted() {
this.edit = true
this.formEdit()
,
methods: {
async formEdit() {
const apiUrl = 'http://127.0.0.1:3100/cards/'
await this.$axios.get(apiUrl + this.$route.params.id).then(res => {
this.designType = res.data.designType
})
}
}
Related
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
);
}
When I make a search with Vuetify <v-autocomplete> and my API, mytext and myvalue are correctly updated and displayed in the suggestions only if write a word like FOO, if I write a string like FOO BAR, then I get the correct result with console.log(response.data) in the API call method, but I get nothing in the suggestions of <v-autocomplete>.
<template>:
<v-autocomplete
v-model="select"
:loading="loading"
:items="items"
item-text="mytext"
item-value="myvalue"
:search-input.sync="search"
hide-no-data
hide-details
label="My Autocomplete"
>
<template v-slot:item="data">
<v-list-item-content>
<v-list-item-title v-html="data.item.mytext"></v-list-item-title>
<v-list-item-subtitle
v-html="data.item.myvalue"
></v-list-item-subtitle
></v-list-item-content>
</template>
</v-autocomplete>
<script>:
<script>
export default {
data() {
return {
select: null,
loading: false,
items: [],
search: null
}
},
watch: {
search(val) {
console.log('search: ' + val)
val && val !== this.select && this.query(val)
}
},
methods: {
async query(v) {
this.loading = true
await this.$axios
.get('/api/foo', {
params: {
search: this.search
}
})
.then((response) => {
console.log(response.data)
this.items = response.data
})
.catch((error) => {
console.log(error)
})
.finally(() => {
this.loading = false
})
}
}
}
</script>
The search variable seems to be linked to the items variable.
You can apply no-filter prop to your v-autocomplete component.
<v-autocomplete
...
no-filter
...
>
</v-autocomplete>
As written in documentation for this prop:
Do not apply filtering when searching. Useful when data is being
filtered server side
https://vuetifyjs.com/en/api/v-autocomplete/#props
I finally fixed it by adding this prop to <v-autocomplete>:
:filter="() => true"
I have a Vuetiy combobox that's part of a form in which the user can search voor data by email and it returns a object (a visitor):
<EmailInput
v-model="selectedVisitor"
:required="true"
:autocomplete="true"
:items="visitors"
/>
<template>
<div>
<v-text-field
v-if="!autocomplete"
:rules="[
(v) => !!v || $t('error.emailRequired'),
(v) => /.+#.+\..+/.test(v) || $t('error.emailInvalid'),
]"
outlined
:label="label"
required
#input="$emit('input', $event)"
/>
<v-combobox
v-if="autocomplete"
:rules="[
(v) => !!v || $t('error.emailRequired'),
]"
:label="label"
outlined
append-icon="arrow_drop_down"
:items="items"
item-text="email"
item-value="email"
#input="$emit('input', $event)"
>
<template v-slot:selection="data">
{{ data.item.email || data.item }}
</template>
<template v-slot:item="data">
{{ data.item.name }} - {{ data.item.email }}
</template>
</v-combobox>
</div>
</template>
<script>
export default {
props: {
required: Boolean,
items: Array,
autocomplete: Boolean,
},
computed: {
label() {
if (this.required === true) {
return this.$i18n.t('labels.emailReq');
}
return this.$i18n.t('labels.email');
},
},
};
</script>
If an existing visitor is found, the other form fields are filled, otherwise just the email is sent to the newVisitor object.
selectedVisitor(val) {
if (typeof val === 'object' && val !== null) {
this.newAppointment.visitorId = val.id;
this.existingVisitor = true; // we don't have to write it to the db
Object.assign(this.newVisitor, val); // for displaying the data only, object assign because db array was edited too when data was modified or deleted
} else if (val === null || val === '') {
this.newAppointment.visitorId = '';
this.newVisitor = {};
} else {
this.newVisitor.email = val;
}
},
The user then clicks a button and the data gets sent to the multipleVisitors array.
The problem is with the function that repopulates these form fields if the user wants to edit the data:
editVisitor(email) {
// get the visitor that needs to be edited
const visitorArray = this.multipleVisitors.filter((el) => el.email === email);
const visitor = visitorArray[0];
// set the visitor to the form
this.newVisitor = visitor;
// variable set to combobox
this.selectedVisitor = visitor.email;
// remove the visitor from the visitor array
this.multipleVisitors = this.multipleVisitors.filter((el) => el.email !== email);
},
The form is filled in except for the combobox which stays empty. If I look at vue dev tools the variable assigned to the v-model does contain the email address.
I've tried assigning the whole object to selectedVisitor but that does not change anything.
this could maybe help
<v-combobox :items="MyFillFuntion()"></v-combobox>
<v-combobox :items="MyFillFuntion(scopeData)" # optional></v-combobox>
and don't forget
<script>
export default {
data: () => ({
MyFillFuntion() {
let array_i_need_to_fill = [];
// your process ... ...
// and finally
return array_i_need_to_fill;
}
}),
props: {
required: Boolean,
items: Array,
autocomplete: Boolean,
},
computed: {
label() {
if (this.required === true) {
return this.$i18n.t('labels.emailReq');
}
return this.$i18n.t('labels.email');
},
},
};
</script>
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>
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 :)