So I am trying to create a page that has the possibility of receiving data from an API through a so called "repository", which has been made by us. The data retreival goes absolutely fine and it gets everything. The frontend also shows it correctly, mostly. There are some few random pieces of data that only show up once something has been changed in the frontend.
Example:
I am trying to get some info about a quiz, this quiz has a few main things: a theme, title, description and a time limit. Most of these are being displayed just fine, except for the theme and time limit. The time limit only shows up when I change the theme, same for the theme itself. Also when there are no questions the theme will also not show at first, it will only pop-up when I add a question.
To get data from the api I have created a few nested requests. Here you can see the page that gets the data initially:
<template>
<q-page class="quiz-editor flex">
<div class="question-listing" v-bind:class="getTheme()">
<div class="question-listing-add">
<q-btn color="white" text-color="black" style="float:left" v-on:click="back"><i class="fa fa-arrow-left"/></q-btn>
<q-btn style="background: green; color: white; float:right" v-on:click="addQuestion"><i class="fa fa-plus"/></q-btn>
<div style="clear:both"></div>
<div v-drag-and-drop="options">
<q-list
#reordered="reordered($event, questions);"
>
<QuestionCard
v-for="question in questions"
:key="question.position" :data-id="question.position"
:question="question"
:editMode="true"
#edit="loadQuestion"
#remove="removeQuestion">
</QuestionCard>
</q-list>
</div>
</div>
</div>
<div class="editor-sidebar">
<div class="editor-title">
<h3>{{editorTitle}}</h3>
<EditorGeneralBar v-bind:currentQuiz.sync="quiz" v-if="editorTitle === 'General'" #submitQuiz="submitQuiz" #themeSelected="selectTheme"></EditorGeneralBar>
<EditorQuestionBar v-if="editorTitle === 'Question'" #cancel="backToGeneral" #submitQuestion="editQuestion2"></EditorQuestionBar>
</div>
</div>
</q-page>
</template>
<script>
import QuestionCard from '../components/QuestionCard';
import EditorGeneralBar from '../components/EditorGeneralBar';
import EditorQuestionBar from '../components/EditorQuestionBar';
import QuizTemplateRepository from "../remote/quiz/QuizTemplateRepository";
import QuizFormRepository from "../remote/quiz/QuizFormRepository";
import AnswerRepository from "../remote/quiz/AnswerRepository";
import QuestionRepository from "../remote/quiz/QuestionRepository";
import { mapGetters, mapActions } from "vuex";
export default {
components: {QuestionCard, EditorGeneralBar, EditorQuestionBar},
name: 'QuizEditor',
data() {
return {
count: 1,
editorTitle: "General",
newQuiz: true,
newQuestion: true,
currentQuestion: null,
quiz: {
tn: "",
title: "",
description: "",
timeLimit: 60,
theme: 1
},
options: {
multipleDropzonesItemsDraggingEnabled: false,
dropzoneSelector: ".q-list",
draggableSelector: ".question-card"
},
loadedTemplateHash: "",
currentPosition: 1,
questions: []
}
},
computed: {
...mapGetters("SingleQuizModule", ["getQuiz", "getQuestion"])
},
mounted() {
var QE = this;
//this.loadedTemplateHash = typeof this.$route.params.chosenTemplateHash == 'undefined' ? "" : this.$route.params.chosenTemplateHash
this.loadedTemplateHash = "5fmdkeq82";
if(this.loadedTemplateHash != "") {
QuizTemplateRepository.getTemplate(this.loadedTemplateHash, this.$store.state.authLogin.token).then(function (res) {
QE.quiz = {
tn: QE.loadedTemplateHash,
title: res.data.template.label,
description: res.data.template.description,
};
QuizTemplateRepository.getTemplateContent(QE.quiz.tn, QE.$store.state.authLogin.token).then(function (res) {
const templateContent = JSON.parse(res.data.content.content);
var questions = templateContent.questions;
QE.quiz.theme = templateContent.properties.theme;
QE.quiz.timeLimit = templateContent.properties.timeLimit;
QE.quiz.questions = questions;
QE.saveQuiz(QE.quiz);
// loop through the questions.
questions.forEach(question => {
// get the questions by their question hash.
QuestionRepository.getQuestion(question, QE.$store.state.authLogin.token).then(function (resQuest) {
var vogh = resQuest.data.var[0].vogh;
// get the answers from the question.
AnswerRepository.getAnswerGroupAnswers(vogh, QE.$store.state.authLogin.token).then(function(resAnswer) {
var quest = {
name: resQuest.data.var[0].name,
hash: resQuest.data.var[0].vh,
vogh: resQuest.data.var[0].vogh,
label: resQuest.data.var[0].label,
position: resQuest.data.var[0].position,
description: "",
answers: [],
isNew: false
}
// loop through the answers and add them to the question answer array.
resAnswer.data.varoptiongroup.forEach(answer => {
answer.position = QE.getPositionString(answer.position);
answer.isNew = false;
if(answer.value > 0)
answer.isCorrect = true;
else
answer.isCorrect = false;
quest.answers.push(answer);
});
QE.questions.push(quest);
QE.currentPosition++;
});
QE.saveQuiz(QE.quiz);
});
});
});
});
} else {
this.saveQuiz(this.quiz);
}
},
For the above code the methods part is really big and it all works anyway. The component which shows some generic data of the quiz is the EditorGeneralBar, this loads everything just fine but except for the time limit, which as I said, only shows up when I change a theme. This is how the component looks like:
<template>
<div class="bar-content">
<q-form
#submit="submit"
class="q-gutter-md"
>
<q-input
filled
v-model="quiz.title"
label="Title"
lazy-rules
:rules="[ val => val && val.length > 0 || 'Please type something']"
/>
<q-input
filled
type="text"
v-model="quiz.description"
label="Description"
lazy-rules
:rules="[ val => val && val.length > 0 || 'Please type something']"
/>
{{quiz.timeLimit}}
<q-input
filled
type="number"
v-model="quiz.timeLimit"
label="Time limit"
lazy-rules
:rules="[ val => val && val.length > 0 || 'Please type something']"
/>
<input type="text" :value="quiz.timeLimit"/>
<q-file filled bottom-slots v-model="quiz.thumbnail" label="Thumbnail">
<template v-slot:before>
<q-icon name="folder_open" />
</template>
<template v-slot:hint>
A thumbnail for the quiz.
</template>
<template v-slot:append>
<q-btn round dense flat icon="add" #click.stop />
</template>
</q-file>
<p>Themes</p>
<div class="theme-list">
<div class="theme-1 theme-preview" v-on:click="selectTheme(1)"></div>
<div class="theme-2 theme-preview" v-on:click="selectTheme(2)"></div>
<div class="theme-3 theme-preview" v-on:click="selectTheme(3)"></div>
<div class="theme-4 theme-preview" v-on:click="selectTheme(4)"></div>
<div class="theme-5 theme-preview" v-on:click="selectTheme(5)"></div>
</div>
<div>
<q-btn label="Save" type="submit" color="primary"/>
</div>
</q-form>
</div>
</template>
<script>
import { mapGetters, mapActions } from "vuex";
export default {
name: 'EditorGeneralBar',
data() {
return {
quiz: {}
}
},
props: {
currentQuiz: {
type: Object,
default: function() {
return {
tn: "",
title: "",
description: "",
timeLimit: 60,
theme: 1,
questions: []
}
}
}
},
computed: {
...mapGetters("SingleQuizModule", ["getQuiz"]),
},
mounted() {
this.quiz = this.currentQuiz;
},
methods: {
...mapActions("SingleQuizModule", [
"saveQuiz"
]),
submit:function() {
this.saveQuiz(this.quiz);
this.$emit("submitQuiz");
},
selectTheme:function(theme) {
this.quiz.theme = theme
this.saveQuiz(this.quiz);
this.$emit("themeSelected");
}
},
watch: {
currentQuiz: function(quiz, oldQuiz) {
console.log("seen child quiz: ", quiz);
console.log("seend child old: ", oldQuiz);
this.quiz = quiz;
this.currentQuiz = quiz;
this.currentQuiz.timeLimit = quiz.timeLimit;
}
}
}
</script>
Above console output boils down to this:
seen child quiz: The correct quiz with the correct data which I want to show in the UI
seen child old: The old wrong quiz data, which will will be replaced by the new data in the function
In the case of emited events it stores it to the state, my attempt at fixing this by utilizing vuex. These events will be caught and handled in the QuizEditor page(the first piece of code). These do nothing more than this:
handler: function() {
this.quiz = this.getQuiz //the state getter.
}
And for the getTheme() function this has been made:
getTheme: function() {
return "theme-"+this.quiz.theme;
},
Does anybody know how to correctly handle this "two-way binding" and showing of data in the UI? I made several attempts and this is the final result.
Related
I am working on a small project to get into Vue js. It is supposed to be a Habittracker. I currently have a bug, if you reload the page and add new habits, my function that is supposed to change the background does not work properly.
here it shows the bug
and here is how my array looks:
0
:
{id: 0, title: "1", ready: false}
1
:
{id: 1, title: "123", ready: false}
2
:
{id: 2, title: "123", ready: false}
3
:
{id: 0, title: "123", ready: true}
I get why it is not working because I am using a counter to assign the id, which resets to 0 when reloaded.
<div class="q-pa-md" v-for="(habit, index) in habits" :key="habit.id">
<q-card class="my-card" :id="habit.id" ref="card">
<q-card-section>
<q-checkbox
id="checkbox"
v-model="habit.ready"
#click="changeToTransparent(habit)"
>
</q-checkbox>
{{ habit.title }}
<q-btn-dropdown flat class="more" icon="more_horiz">
<q-list>
<q-item clickable v-close-popup #click="deletehabit(index)">
<q-item-section>
<q-item-label>Delete</q-item-label>
</q-item-section>
</q-item>
<q-item clickable v-close-popup #click="edithabitbutton(index)">
<q-item-section>
<q-item-label>Edit</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</q-card-section>
</q-card>
<div>
let counter = 0;
const habits = ref([]);
const addHabit = () => {
habits.value.push({ id: counter++, title: habittitle.value, ready: false });
savetolocalstorage();
habittitle.value = "";
};
const changeToTransparent = (habit) => {
if(document.getElementById(habit.id) != null) {
if (habit.ready) {
document.getElementById(habit.id).style.backgroundColor =
"rgba(170,193,200,0.25)";
savetolocalstorage();
} else {
document.getElementById(habit.id).style.backgroundColor = "";
savetolocalstorage();
}
}
}
Any ideas on how I could fix this?
You need to load your localStorage and set the length to the counter value. I made a working example here. I also improved your code so that it's more in line with Vue's concepts. As #Rahul Purohit pointed out, you will need to JSON.stringify the result when saving and JSON.parse it when loading.
<template>
<q-input label="Title" v-model="habitTitle" />
<q-btn label="Add habit" #click="addHabit" />
<div class="q-pa-md" v-for="(habit, index) in habits" :key="habit.id">
<q-card
class="my-card"
:id="habit.id"
:ref="(el) => (cardRefs[habit.id] = el)"
>
<q-card-section>
<q-checkbox
id="checkbox"
v-model="habit.ready"
#click="changeToTransparent(habit)"
>
</q-checkbox>
{{ habit.id }} {{ habit.title }}
<q-btn-dropdown flat class="more" icon="more_horiz">
<q-list>
<q-item clickable v-close-popup #click="deletehabit(index)">
<q-item-section>
<q-item-label>Delete</q-item-label>
</q-item-section>
</q-item>
<q-item clickable v-close-popup #click="edithabitbutton(index)">
<q-item-section>
<q-item-label>Edit</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</q-card-section>
</q-card>
</div>
</template>
<script setup>
const { ref, onMounted } = require("vue");
// it can be just a let.
const counter = ref(0);
const habits = ref([]);
const habitTitle = ref("test");
const cardRefs = ref({});
const saveToLocalStorage = () => console.log("saved");
const addHabit = () => {
habits.value.push({
id: counter.value++,
title: habitTitle.value,
ready: false,
});
saveToLocalStorage();
habitTitle.value = "";
};
const changeToTransparent = (habit) => {
if (cardRefs.value[habit.id] != null) {
if (habit.ready) {
cardRefs.value[habit.id].$el.style.backgroundColor =
"rgba(170,193,200,0.25)";
saveToLocalStorage();
} else {
cardRefs.value[habit.id].$el.style.backgroundColor = "";
saveToLocalStorage();
}
}
};
onMounted(() => {
// Load habits from localStorage
// This is just an example
habits.value = [
{
id: 0,
title: "Testing new habit",
ready: true,
},
{
id: 1,
title: "Testing new habit",
ready: false,
},
{
id: 2,
title: "Testing new habit",
ready: false,
},
];
counter.value = habits.value.length;
});
</script>
Ok from the given context, I believe you're not using any backend and are saving all the entries on the local storage.
If that is the case, you must be storing your data in some array to LS like habits: []
Here instead of initiating counter to 0 you can add a lifecycle method.
beforeMount() {
counter = JSON.parse(localStorage.getItem("habits")).length
}
In my project I use Vue.js and Nuxt.js and I have this page.
This page is settings page, where user can changes his settings. As you can see, this is only one page, where user can switch between tabs.
<template>
<div class="account-wrapper">
<div class="avatar" #click="redirect('/account/me')">
<img class='avatar-box' src="../../../assets/img/testava.jpg" alt="ava">
<div class="avatar-text">
<h2 class="nmp">{{ personalSettings.username }}</h2>
<p class="paragraph opacity nmp">Public profile</p>
</div>
</div>
<div class="side-bar">
<div v-for="item in accountHeaderItems" :key="item.title" class="flex">
<div v-if="item.active" class="vertical-line" />
<p :class="[item.active ? 'item item-active' : 'item']" #click="changeSubsection(item)">
{{ item.title }}
</p>
</div>
</div>
<personal-information v-if="currentSection === 'Public account'" :personal-settings="personalSettings" />
<security-settings v-else-if="currentSection === 'Security settings'" :security-settings="securitySettings" />
<site-settings v-else />
</div>
</template>
<script>
import SecuritySettings from '~/components/pageComponents/settings/SecuritySettings'
import PersonalInformation from '~/components/pageComponents/settings/PersonalInformation'
import SiteSettings from '~/components/pageComponents/settings/SiteSettings'
import { getUserSettings } from "~/api";
export default {
name: 'Settings',
components: {
SecuritySettings,
PersonalInformation,
SiteSettings
},
data() {
return {
accountHeaderItems: [
{ title: 'Public account', active: true },
{ title: 'Security settings', active: false },
{ title: 'Appearance settings', active: false },
{ title: 'Notifications', active: false }
],
currentSection: 'Public account',
personalSettings: {},
securitySettings: {},
}
},
async mounted() {
if (localStorage.getItem('token') !== null) await this.getUsersSettings(localStorage.getItem('token'))
else await this.$router.push('/')
},
methods: {
async getUsersSettings(token) {
const userSettings = await getUserSettings(token)
if (userSettings.status === -1)
return this.$router.push('/')
this.personalSettings = userSettings.personalSettings
this.securitySettings = userSettings.securitySettings
},
changeSubsection(item) {
this.currentSection = item.title
this.accountHeaderItems.forEach(header => {
header.active = item.title === header.title
})
},
redirect(path) {
this.$router.push(path)
},
}
}
</script>
The problem is when page loads. When in async mounted() I get data I want to pass it to my components. And here is the problem, when I try to do that it seems to work fine, but there is strange behaviour, I always need to switch between tabs, to make data be visible on page.
For example - in personalSettings object there is field first_name. So, in personal-information component in custom Input I want to show this data in this way (in mounted I make copy of object to prevent mutations):
<Input
v-model="personalInfo.first_name"
:title="'First name'"
:title-class="'small'"
:additional-class="'small'"
/>
...
props: {
personalSettings: {
type: Object,
default: () => {}
}
},
data() {
return {
personalInfo: {},
loading: false,
showPopup: false
}
},
mounted() {
this.personalInfo = this.personalSettings
},
Everything seems to be fine, but, actually, I have to switch to another tab and switch back to this tab to see this data. What's wrong? How can I prevent this behaviour and show data in correct way?
There are many ways to do it, you can use Store, and emit changes and Data you want to use late.
See: https://vuex.vuejs.org/guide/#the-simplest-store
I'm building an application to power the backend of a website for a restaurant chain. Users will need to edit page content and images. The site is fairly complex and there are lots of nested pages and sections within those pages. Rather than hardcode templates to edit each page and section, I'm trying to make a standard template that can edit all pages based on data from the route.
I'm getting stuck on the v-model for my text input.
Here's my router code:
{
path: '/dashboard/:id/sections/:section',
name: 'section',
component: () => import('../views/Dashboard/Restaurants/Restaurant/Sections/Section.vue'),
meta: {
requiresAuth: true
},
},
Then, in my Section.vue, here is my input with the v-model. In this case, I'm trying to edit the Welcome section of a restaurant. If I was building just a page to edit the Welcome text, it would work no problem.:
<vue-editor v-model="restInfo.welcome" placeholder="Update Text"></vue-editor>
This issue is that I need to reference the "welcome" part of the v-model dynamically, because I've got about 40 Sections to deal with.
I can reference the Section to edit with this.$route.params.section. It would be great if I could use v-model="restInfo. + section", but that doesn't work.
Is there a way to update v-model based on the route parameters?
Thanks!
Update...
Here is my entire Section.vue
<template>
<div>
<Breadcrumbs :items="crumbs" />
<div v-if="restInfo">
<h3>Update {{section}}</h3>
<div class="flex flex-wrap">
<div class="form__content">
<form #submit.prevent>
<vue-editor v-model="restInfo.welcome" placeholder="Update Text"></vue-editor>
<div class="flex">
<button class="btn btn__primary mb-3" #click="editText()">
Update
<transition name="fade">
<span class="ml-2" v-if="performingRequest">
<i class="fa fa-spinner fa-spin"></i>
</span>
</transition>
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
import { VueEditor } from "vue2-editor"
import Loader from '#/components/Loader.vue'
import Breadcrumbs from '#/components/Breadcrumbs.vue'
export default {
data() {
return {
performingRequest: false,
}
},
created () {
this.$store.dispatch("getRestFromId", this.$route.params.id);
},
computed: {
...mapState(['currentUser', 'restInfo']),
section() {
return this.$route.params.section
},
identifier() {
return this.restInfo.id
},
model() {
return this.restInfo.id + `.` + this.section
},
crumbs () {
if (this.restInfo) {
let rest = this.restInfo
let crumbsArray = []
let step1 = { title: "Dashboard", to: { name: "dashboard"}}
let step2 = { title: rest.name, to: { name: "resthome"}}
let step3 = { title: 'Page Sections', to: { name: 'restsections'}}
let step4 = { title: this.$route.params.section, to: false}
crumbsArray.push(step1)
crumbsArray.push(step2)
crumbsArray.push(step3)
crumbsArray.push(step4)
return crumbsArray
} else {
return []
}
},
},
methods: {
editText() {
this.performingRequest = true
this.$store.dispatch("updateRest", {
id: this.rest.id,
content: this.rest
});
setTimeout(() => {
this.performingRequest = false
}, 2000)
}
},
components: {
Loader,
VueEditor,
Breadcrumbs
},
beforeDestroy(){
this.performingRequest = false
delete this.performingRequest
}
}
</script>
Try to use the brackets accessor [] instead of . :
<vue-editor v-model="restInfo[section]"
I have a page with Vuetify Autocomplete component, and REST API backend with '/vendors' method. This method takes limit, page and name parameters and returns JSON with id and name fields.
I made some code with lazy list load on user input event. But now I want to add the ability to load this list on user scroll event.
For example, by default there is a list of 100 vendors. User scrolled this list until the end, then "some event" is called and loads next 100 of vendors. Then user keeps scrolling and the action is repeated.
Is it possible to made this with Vuetify Autocomplete component, or should i use another library?
Example code of current component is shown below:
<template>
<v-autocomplete
:items="vendors"
v-model="selectedVendorId"
item-text="name"
item-value="id"
label="Select a vendor"
#input.native="getVendorsFromApi"
></v-autocomplete>
</template>
<script>
export default {
data () {
return {
page: 0,
limit: 100,
selectedVendorId: null,
vendors: [],
loading: true
}
},
created: function (){
this.getVendorsFromApi();
},
methods: {
getVendorsFromApi (event) {
return new Promise(() => {
this.$axios.get(this.$backendLink
+ '/vendors?limit=' + this.limit
+ '&page=' + this.page
+ '&name=' + (event ? event.target.value : ''))
.then(response => {
this.vendors = response.data;
})
})
}
}
}
</script>
I was able to get auto-loading going with the Vuetify AutoComplete component. It's a bit of a hack, but basically the solution is to use the v-slot append item, the v-intersect directive to detect if that appended item is visible, and if it is, call your API to fetch more items and append it to your current list.
<v-autocomplete
:items="vendors"
v-model="selectedVendorId"
item-text="name"
item-value="id"
label="Select a vendor"
#input.native="getVendorsFromApi"
>
<template v-slot:append-item>
<div v-intersect="endIntersect" />
</template>
</v-autocomplete>
...
export default {
methods: {
endIntersect(entries, observer, isIntersecting) {
if (isIntersecting) {
let moreVendors = loadMoreFromApi()
this.vendors = [ ...this.vendors, ...moreVendors]
}
}
}
}
In my use case, I was using API Platform as a backend, using GraphQL pagination using a cursor.
<component
v-bind:is="canAdd ? 'v-combobox' : 'v-autocomplete'"
v-model="user"
:items="computedUsers"
:search-input.sync="search"
item-text="item.node.userProfile.username"
hide-details
rounded
solo
:filter="
(item, queryText, itemText) => {
return item.node.userProfile.username.toLocaleLowerCase().indexOf(queryText.toLocaleLowerCase()) > -1
} "
:loading="loading"
item-value="username"
class="text-left pl-1"
color="blue-grey lighten-2"
:label="label"
>
<template v-slot:selection="{ item }">
<v-chip v-if="typeof item == 'object'">
<v-avatar left>
<v-img v-if="item.node.userProfile.image" :src="item.node.userProfile.image" />
<v-icon v-else>mdi-account-circle</v-icon>
</v-avatar>
{{ item.node.userProfile.firstName }} {{ item.node.userProfile.lastName }}
</v-chip>
<v-chip v-else-if="typeof item == 'string'">
{{ item }}
</v-chip>
</template>
<template v-slot:item="{ item: { node } }">
<v-list-item-avatar >
<img v-if="node.userProfile.avatar" :src="node.userProfile.avatar" />
<v-icon v-else>mdi-account-circle</v-icon>
</v-list-item-avatar>
<v-list-item-content class="text-left">
<v-list-item-title>
{{ $t('fullName', { firstName: node.userProfile.firstName, lastName: node.userProfile.lastName } )}}
</v-list-item-title>
<v-list-item-subtitle v-html="node.userProfile.username"></v-list-item-subtitle>
</v-list-item-content>
</template>
<template v-slot:append-item="">
<div v-intersect="endIntersect" >
</div>
</template>
</component>
import { VCombobox, VAutocomplete } from "vuetify/lib";
import debounce from "#/helpers/debounce"
import { SEARCH_USER_BY_USERNAME } from "#/graphql/UserQueries";
const RESULTS_TO_SHOW = 5
export default {
props: {
canAdd: {
type: Boolean,
default: false,
},
value: [Object, String],
label: String,
},
components: { VCombobox, VAutocomplete },
apollo: {
users: {
query: SEARCH_USER_BY_USERNAME,
variables() {
return {
username: this.search,
numberToShow: RESULTS_TO_SHOW,
cursor: null,
}
},
watchLoading(isLoading) {
this.loading = isLoading
},
skip() {
if (this.search) {
return !(this.search.length > 1)
}
return true
},
},
},
data() {
return {
user: this.value,
search: "",
cursor: null,
loading: false,
};
},
watch: {
user(newValue) {
let emit = newValue
if (newValue) {
emit = newValue.node
}
this.$emit("input", emit);
},
value(newValue) {
if (this.user && this.user.node != newValue) {
if (newValue == null) {
this.user = null
}
else {
this.user = { node: newValue };
}
}
},
search(newValue) {
this.debouncedSearch(newValue)
},
},
methods: {
endIntersect(entries, observer, isIntersecting) {
if (isIntersecting && this.users && this.users.pageInfo.hasNextPage) {
let cursor = this.users.pageInfo.endCursor
this.$apollo.queries.users.fetchMore({
variables: { cursor: cursor},
updateQuery: (previousResult, { fetchMoreResult }) => {
let edges = [
...previousResult.users.edges,
...fetchMoreResult.users.edges,
]
let pageInfo = fetchMoreResult.users.pageInfo;
return {
users: {
edges: edges,
pageInfo: pageInfo,
__typename: previousResult.users.__typename,
}
}
}
})
}
},
debouncedSearch: debounce(function (search) {
if (this.users) {
this.$apollo.queries.users.refetch({
username: search,
numberToShow: RESULTS_TO_SHOW,
cursor: null,
});
}
}, 500),
filter(item, queryText) {
return item.node.userProfile.username.toLocaleLowerCase().indexOf(queryText.toLocaleLowerCase()) > -1
}
},
computed: {
computedUsers() {
if (this.users){
return this.users.edges
}
return []
},
skip() {
if (this.search) {
return this.search.length > 1
}
return false
}
}
};
</script>
Update June 12, 2021:
If you are using Vuetify 2.X, use Brettins' solution based on append-item slot and v-intersect directive.
Old answer:
Looks like it's not possible with default v-autocomplete component (at least in vuetify 1.5.16 or lower).
The component that provides the most similar functionality is VueInfiniteAutocomplete.
But keep in mind that in this case there may be problems with styles, validation, etc.
There is an example with this library.
<template>
<div>
<vue-infinite-autocomplete
:data-source="getAsyncOptions"
:fetch-size="limit"
v-on:select="handleOnSelect"
:value="autocompleteViewValue"
>
</vue-infinite-autocomplete>
</div>
</template>
<script>
export default {
data () {
return {
selectedVendorId : null,
limit: 100,
autocompleteViewValue: null
}
},
methods: {
getAsyncOptions(text, page, fetchSize) {
return new Promise((resolve, reject) => {
resolve(
this.$axios.get(this.$backendLink
+ '/vendors?limit=' + fetchSize
+ '&page=' + page
+ '&name=' + text)
.then(response => {
//Response MUST contain 'id' and 'text' fields, and nothing else.
//If there are other fields, you should remove it here
//and create 'id' and 'text' fields in response JSON by yourself
return response.data;
})
)
});
},
handleOnSelect(selectedItem) {
this.autocompleteViewValue = selectedItem.text;
this.selectedVendorId = selectedItem.id;
}
}
}
</script>
P.S.: If you just want to use v-autocomplete component with server-side pagination, you could create a "Load more..." button using append-item slot, as suggested in this issue.
My application is similar to a quiz. each question may have radio button answers, integers, text etc.
Consider the case for multiple choice question. I have a vue for the Radio button options. in the parent Vue i have a reset button for each of the question. If I click on the reset, it should removed the selected answer for that particular question.
How can I achieve this given that the reset button is in Parent vue and the answer to be reset is in the child Vue?
Parent:
<template>
<div class="inputContent">
<p class="lead" v-if="title">
{{title}}
<span v-if="valueConstraints.requiredValue" class="text-danger">* .
</span>
</p>
<b-alert variant="danger" show v-else>
This item does not have a title defined
</b-alert>
<!-- If type is radio -->
<div v-if="inputType==='radio'">
<Radio :constraints="valueConstraints" :init="init"
:index="index" v-on:valueChanged="sendData" />
</div>
<!-- If type is text -->
<div v-else-if="inputType==='text'">
<TextInput :constraints="valueConstraints" :init="init" v-
on:valueChanged="sendData"/>
</div>
<div class="row float-right">
<b-button class="" variant="default" type=reset #click="reset">
Reset1
</b-button>
<b-button class="" variant="default" v-
if="!valueConstraints.requiredValue" #click="skip">
Skip
</b-button>
</div>
</div>
</template>
<style></style>
<script>
import { bus } from '../main';
import Radio from './Inputs/Radio';
import TextInput from './Inputs/TextInput';
export default {
name: 'InputSelector',
props: ['inputType', 'title', 'index', 'valueConstraints',
'init'],
components: {
Radio,
TextInput,
},
data() {
return {
};
},
methods: {
skip() {
this.$emit('skip');
},
// this emits an event on the bus with optional 'data' param
reset() {
bus.$emit('resetChild', this.index);
this.$emit('dontKnow');
},
sendData(val) {
this.$emit('valueChanged', val);
this.$emit('next');
},
},
};
</script>
the child vue:
<template>
<div class="radioInput container ml-3 pl-3">
<div v-if="constraints.multipleChoice">
<b-alert show variant="warning">
Multiple Choice radio buttons are not implemented yet!
</b-alert>
</div>
<div v-else>
<b-form-group label="">
<b-form-radio-group v-model="selected"
:options="options"
v-bind:name="'q' + index"
stacked
class="text-left"
#change="sendData"
>
</b-form-radio-group>
</b-form-group>
</div>
</div>
</template>
<style scoped>
</style>
<script>
import _ from 'lodash';
import { bus } from '../../main';
export default {
name: 'radioInput',
props: ['constraints', 'init', 'index'],
data() {
return {
selected: null,
};
},
computed: {
options() {
return _.map(this.constraints['itemListElement'][0]['#list'], (v) => {
const activeValueChoices = _.filter(v['name'], ac => ac['#language'] === "en");
return {
text: activeValueChoices[0]['#value'],
value: v['value'][0]['#value'],
};
});
},
},
watch: {
init: {
handler() {
if (this.init) {
this.selected = this.init.value;
} else {
this.selected = false;
}
},
deep: true,
},
},
mounted() {
if (this.init) {
this.selected = this.init.value;
}
bus.$on('resetChild', this.resetChildMethod);
},
methods: {
sendData(val) {
this.$emit('valueChanged', val);
},
resetChildMethod(selectedIndex) {
this.selected = false;
},
},
};
</script>
One way would be to use an event bus
in your main js add:
//set up bus for communication
export const bus = new Vue();
in your parent vue:
import {bus} from 'pathto/main.js';
// in your 'reset()' method add:
// this emits an event on the bus with optional 'data' param
bus.$emit('resetChild', data);
in your child vue
import {bus} from 'path/to/main';
// listen for the event on the bus and run your method
mounted(){
bus.$on('resetChild', this.resetChildMethod());
},
methods: {
resetChildMethod(){
//put your reset logic here
}
}