Related
I'm trying to call a method from a child component which is programatically inserted.
Here is my code.
MultipleFileUploader.vue
<template>
<div class="form-group" id="multiple-file-uploader">
<div>
<multiple-file-uploader-part
:name="uploadername" :index="1"
#remove="deleteUploader" #fileselected="fileSelected($event)">
</multiple-file-uploader-part>
</div>
</div>
</template>
<script>
import MultipleFileUploaderPart from './MultipleFileUploaderPart.vue';
let index_count = 1;
export default {
components: {
'multiple-file-uploader-part':MultipleFileUploaderPart,
},
props: {
uploadername: {
type: String,
default: 'files',
}
},
data() {
return {
next_id:1,
}
},
methods: {
fileSelected: function (target) {
var UploaderPart = Vue.extend(MultipleFileUploaderPart);
new UploaderPart().$on('fileselected','fileSelected')
.$mount('#multiple-file-uploader');
},
deleteUploader: function (idToRemove) {
this.uploaders = this.uploaders.filter(
uploaders_id => {
return uploaders_id.id !== idToRemove;
}
)
}
},
}
</script>
<style scoped>
</style>
MultipleFileUploaderPart.vue
<template>
<div v-bind:id="name + '['+index+']'">
<div class="input-group margin">
{{index}}
<input type="file" accept="application/pdf,image/jpeg,image/png"
v-bind:name="name + '['+index+']'"
v-on:change="fileSelectedMethod($event.target)">
<div class="input-group-btn">
<button #click="removeClicked"
class="btn btn-danger btn-sm"
v-if="index != 1"
type="button">
Delete{{index}}
</button>
</div>
</div>
<p v-if="size_error" style="color: red">File size must be less than 2MB</p>
</div>
</template>
<script>
export default {
props: {
name: {
type: String,
},
index: {
type: Number,
},
},
data() {
return {
size: '',
size_error: false,
}
},
methods: {
removeClicked: function () {
document.getElementById(this.name+'[' + this.index + ']' ).remove();
this.$emit('remove', this.index);
},
fileSelectedMethod: function (target) {
this.size = target.files[0].size;
if (this.size < 2000000) {
this.size_error = false;
this.$emit('fileselected', target);
} else {
target.value = null;
this.size_error = true;
console.log(target.files);
}
}
}
}
</script>
<style scoped>
I'm trying to achieve is that when a file input is filled with a file, a MultipleFileUploaderPart is created. And when the file input element in this component is filled, another MultipleFileUploaderPart is inserted.
I'd like to call MultipleFileUploader 's fileSelected method from newly inserted components so that I can create another component.
I also want to remove a MultipleFileUploaderPart component when the delete button is clicked.
How can I achieve this? or is there a better way?
EDIT:
This is what I originally had.
MultipleFileUploader.vue
<template>
<div class="form-group">
<div>
<multiple-file-uploader-part
v-for="uploader in uploaders"
:name="uploadername" :index="uploader.id"
#remove="deleteUploader" #fileselected="fileSelected($event)">
slot
</multiple-file-uploader-part>
</div>
</div>
</template>
<script>
import MultipleFileUploaderPart from "./MultipleFileUploaderPart";
let index_count = 1;
export default {
//name: "MultipleFileUploader",
components: {MultipleFileUploaderPart},
props: {
uploadername: {
type: String,
default: 'files',
}
},
data() {
return {
uploaders: [
{
id: index_count++,
},
]
}
},
methods: {
fileSelected: function (target) {
if(target.value){
this.uploaders.push({
id: index_count++,
})
}
},
deleteUploader: function (idToRemove) {
this.uploaders = this.uploaders.filter(
uploaders_id => {
return uploaders_id.id !== idToRemove;
}
)
}
},
}
</script>
MultipleFileUploaderPart.vue
<template>
<div class="input-group margin">
{{index}}
<input type="file" accept="application/pdf,image/jpeg,image/png"
v-bind:name="name + '['+index+']'"
v-on:change="fileSelectedMethod($event.target)">
<div class="input-group-btn">
<button #click="$emit('remove',index)"
class="btn btn-danger btn-sm"
v-if="index != 1"
type="button">
Delete{{index}}
</button>
</div>
<br>
<p v-if="size_error" style="color: red">File size must be less than 2MB</p>
</div>
</template>
<script>
export default {
props: {
name: {
type: String,
},
index: {
type: Number,
},
},
data() {
return {
size: '',
size_error: false,
}
},
methods: {
checkFileSize: function () {
},
fileSelectedMethod: function (target) {
console.log(target);
console.log(target.files);
this.size = target.files[0].size;
console.log(this.size);
if (this.size < 2000000) {
this.size_error = false;
this.$emit('fileselected', target);
} else {
target.value = null;
this.size_error = true;
console.log(target.files);
}
}
}
}
</script>
And this happens. please click
When I click 'Delete'Button, correct child coponent is deleted but the file in the input form stays there. that's why I'm seeking for another approach.
Declare uploaders as an array of objects that contain all needed props for creation of MultipleFileUploaderPart.
Use v-for on MultipleFileUploaderPart in the main MultipleFileUploader to reactively generate MultipleFileUploaderPart components
Use $emit from MultipleFileUploaderPart to MultipleFileUploader to emit creation and deletion events so that MultipleFileUploader can add or remove elements in the uploaders array.
Please don't delete or create elements from DOM directly, let the VueJs do this work.
I am very new to vue.js and fumbling my way though it, forgive me if my terms are incorrect. I am creating a touchscreen application that needs to be ADA compliant (only the bottom part of the screen is accessible, so i have to use buttons for interaction).
I have a parent component with a carousel creating an array of slides, pulling data from my child component.
parent component HTML
<carousel :navigateTo="selectedListIndex" #pageChange="OnPageChange">
<slide v-for="(member, index) in selectedList" :key="index">
<MemberBioPage :member="member"/>
</slide>
</carousel>
parent component SCRIPT:
export default {
data () {
return {
currentPage: 0
}
},
components: {
MemberBioPage,
Carousel,
Slide
},
computed: {
selectedList () {
return this.$store.state.selectedList
},
selectedListIndex () {
return this.$store.state.selectedListIndex
}
},
methods: {
OnPageChange (newPageIndex) {
console.log(newPageIndex)
this.currentPage = newPageIndex
}
}
}
within my child component, i have bio copy being pulled from my data and arrow buttons that allow you to scroll the text. There is an outer container and an inner container to allow the scrolling and based on the height that the content takes up in the container will determine when the arrows disable or not.
child component HTML:
<div class="member-bio-page">
<div class="bio">
<div class="portrait-image">
<img :src="member.imgSrc" />
</div>
<div class="bio-container">
<div class="inner-scroll" v-bind:style="{top: scrollVar + 'px'}">
<h1>{{ member.name }}</h1>
<div class="description-container">
<div class="para">
<p v-html="member.shortBio"></p>
</div>
</div>
</div>
</div>
<div class="scroll-buttons">
<div>
<!-- set the class of active is the scroll variable is less than 0-->
<img class="btn-scroll" v-bind:class="{ 'active': scrollVar < 0 }" #click="scrollUp" src="#/assets/arrow-up.png">
</div>
<div>
<!-- set the class of active is the scroll variable is greater than the height of the scrollable inner container-->
<img class="btn-scroll" v-bind:class="{ 'active': scrollVar > newHeight }" #click="scrollDown" src="#/assets/arrow-down.png">
</div>
</div>
</div>
</div>
child component SCRIPT:
<script>
export default {
props: [
'member', 'currentPage'
],
data () {
return {
scrollVar: 0,
outerHeight: 0,
innerHeight: 0,
newHeight: -10
}
},
mounted () {
this.outerHeight = document.getElementsByClassName('bio-container')[0].clientHeight
this.innerHeight = document.getElementsByClassName('inner-scroll')[0].clientHeight
this.newHeight = this.outerHeight - this.innerHeight
return this.newHeight
},
methods: {
scrollUp () {
console.log(this.scrollVar)
this.scrollVar += 40
},
scrollDown () {
console.log(this.scrollVar)
this.scrollVar -= 40
},
showVideo () {
this.$emit('showContent')
}
}
}
</script>
I am able to get the height of the first bio i look at, but on page change it keeps that set height. I basically want the code in mounted to be able to rerun based on the index of the slide i am on. I need 'newHeight' to update on each page change. I tried grabbing the 'currentPage' from my parent component using props, but it pulls undefined.
here is all a snippet from my data to show you what data i currently have:
{
index: 12,
name: 'Name of Person',
carouselImage: require('#/assets/carousel-images/image.jpg'),
imgSrc: require('#/assets/bio-page-image-placeholder.jpg'),
shortBio: '<p>a bunch of text being pulled</p>',
pin: require('#/assets/image-of-pin.png')
}
this is also my store just in case
const store = new Vuex.Store({
state: {
foundersList: founders,
chairmanList: chairmans,
selectedList: founders,
selectedListIndex: -1
},
mutations: {
setSelectedState (state, list) {
state.selectedList = list
},
setSelectedListIndex (state, idx) {
state.selectedListIndex = idx
}
}
})
Alright, so this is a good start. Here's a few things I would try:
Move the code you currently have in mounted to a new method called calculateHeight or something similar.
Call the method from your scrollUp and scrollDown methods.
So your final code would look something like this:
export default {
props: [
'member', 'currentPage'
],
data () {
return {
scrollVar: 0,
outerHeight: 0,
innerHeight: 0,
newHeight: -10
}
},
mounted () {
this.calculateHeight();
},
methods: {
calculateHeight() {
this.outerHeight = document.getElementsByClassName('bio-container')[0].clientHeight
this.innerHeight = document.getElementsByClassName('inner-scroll')[0].clientHeight
this.newHeight = this.outerHeight - this.innerHeight
},
scrollUp () {
console.log(this.scrollVar)
this.scrollVar += 40
this.calculateHeight()
},
scrollDown () {
console.log(this.scrollVar)
this.scrollVar -= 40
this.calculateHeight()
},
showVideo () {
this.$emit('showContent')
}
}
}
I created a gallery using Vue, Nuxt.
The full code of my gallery you can find on GitHub,
and you can see a live demo here vue gallery demo
Most of the logic is in the vue-lighbox component.
<script>
export default {
props: {
thumbnails: {
type: Array,
required: true,
},
images: {
type: Array,
required: true,
},
thumbnailPath: {
type: String,
required: true,
},
imagePath: {
type: String,
required: true,
},
},
data() {
return {
visible: false,
currentImage: 0,
}
},
methods: {
Toggle(index) {
this.currentImage = index
this.visible = !this.visible
},
Next() {
if (this.currentImage < this.images.length - 1) {
this.currentImage++
} else {
this.currentImage = 0
}
},
Prev() {
if (this.currentImage > 0) {
this.currentImage--
} else {
this.currentImage = this.images.length - 1
}
},
},
}
</script>
<template>
<div class="thumb_container">
<div v-for="(thumbnail, index) in thumbnails" :key="thumbnail" class="thumbnail" #click="Toggle(index)">
<img :src="thumbnailPath + thumbnail" />
<div class="plus">
<i class="icon icon-plus" />
</div>
<div class="color-overlay"></div>
</div>
<div v-if="visible" class="lightbox">
<i class="icon-cancel" #click="Toggle()" />
<i class="icon-left" #click="Prev()" />
<i class="icon-right" #click="Next()" />
<img :key="currentImage" :src="imagePath + [currentImage + 1] +'.jpg'" />
</div>
</div>
</template>
A quick break down of my component:
I have small thumbnail images and big images.
The thumbnail images are displayed with for loop. Whenever I click at some of the thumbnails the corresponding big image appears.
The gallery is working fine as intended, however, I have a problem with loading the photos from user experience aspect. So I need to implement a loading spinner while the big image is loading.
I really don't know the right approach to this. Does someone have an example to share or some hint to give me?
How can I check when a fallowing image is loaded?
You can use vue's v-on:load directive to determine whether to show an image or a spinner. In my app I've made this into a custom component:
<template>
<div>
<img :src="src"
:alt="alt"
#load="onLoaded"
#error="onError"
v-show="loaded && !error"
key="image"/>
<spinner-component v-show="loaded == false || error"/>
</div>
</template>
export default {
mounted() {
},
data() {
return {
loaded: false,
error: false
}
},
props: {
src: {
type: String
},
alt: {},
width: {},
height: {}
},
computed: {
style({ width, height }) {
return {
width: width,
height: height,
objectFit: "contain"
}
}
},
methods: {
onLoaded() {
this.loaded = true;
},
onError() {
this.error = true;
}
}
}
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.
Hi guys Im trying to make my custom tabs, with Vue js but Im having a problem since like my data property is not getting updated :(...
Here is the case Im having trouble with:
When I open 3 tabs, If I open my Modal on the first tab and then close that first tab, I will be switched to second tab but my Modal that was from first tab stays open like it is modal from the first tab instead of second... I would like each tab to have its own modal instance.
Here I posted bellow gif of what is happening. Basically I dont want my modal to apear again on next tab, when previous is closed :)
Seems like my data values, are not destroyed with first tab, and they are just replicated onto the second tab, Im trying to figure out what is the issue for few days now but no succes...
Here is my App.vue
<template>
<div id="app">
<div class="event-tabs wrapper">
<div class="is-flex">
<div class="tabs is-boxed control">
<ul>
<li v-for="(newEvent, index) in newEventList" :key="index" :class="selectedEventClass(index)"
#click.left="selectEvent(index)" #click.middle="discardEvent(index)">
<span class="event-tab-title">
TAB
</span>
<span class="event-tab-close" #click.stop="closeEvent(index)">
<i class="fa fa-times"></i>
</span>
</li>
<li class="add-tab">
<a #click.prevent="createEvent" :title="'Create Tab'">
<span>+</span>
</a>
</li>
</ul>
</div>
</div>
<div class="tab-content">
<tab v-for="(event, index) in newEventList" :event="event" :index="index"
v-if="showEventTab" v-show="index === selectedEvent" :key="index"
ref="eventTab"></tab>
</div>
</div>
</div>
</template>
<script>
import tab from './components/EventTab.vue';
export default {
name: 'app',
components: {
tab,
},
computed: {
newEventList() {
return this.$store.getters['eventModule/getNewList'];
},
selectedEvent() {
return this.$store.getters['eventModule/getSelectedNew'];
},
eventToEdit() {
return this.$store.state.event.eventToEdit;
},
showEventTab() {
return this.newEventList.length > 0;
},
},
methods: {
selectedEventClass(eventIndex) {
return (eventIndex === this.selectedEvent) ? 'is-active' : '';
},
createEvent() {
this.$store.dispatch('eventModule/create');
},
selectEvent(eventIndex) {
this.$store.dispatch('eventModule/select', { eventIndex });
},
closeEvent(eventIndex) {
this.$store.dispatch('eventModule/close', { eventIndex });
},
},
}
</script>
<style lang="scss">
#import './assets/scss/main';
</style>
My Tab component:
<template>
<div class="event-form" v-if="event">
<div class="columns">
<div class="column is-half">
<div class="field">
<h1>This is the TAB number {{ index}} </h1>
</div>
<p class="control">
<button class="button is-danger" #click.prevent="openDialog">
Open Dialog
</button>
</p>
<modalDialog type="none" :show="modal.show"
:className="'eventTabModal'" :title="'Test modal'"
:text="'Test modal'"
#hide="closeDiscardModal">
<h3>Modal is active</h3>
</modalDialog>
</div>
</div>
</div>
</template>
<script>
import modalDialog from './ModalDialog.vue';
export default {
components: {
modalDialog,
},
props: ['event', 'index'],
data() {
return {
eventDefault: {},
/**
* Discard event modal
*/
modal: {
show: false,
},
};
},
computed: {
eventList() {
return this.$store.getters['event/getNewList'];
},
eventTypeList() {
return this.$store.getters['eventType/getList'];
},
},
methods: {
/**
* Opens discarded Modal
*/
closeDiscardModal() {
this.modal = {
show: false,
};
},
openDialog() {
this.modal = {
show: true,
};
},
},
}
</script>
My Modal component for displaying Dialog:
<template>
<transition name="fade">
<div class="modal is-active" v-show="shouldShowModal" :class="className">
<div class="modal-background" #click="hideModal"></div>
<div class="modal-card">
<header class="modal-card-head" v-if="title">
<p class="modal-card-title">{{ title }}</p>
</header>
<section class="modal-card-body">
<slot>
{{ text }}
</slot>
</section>
<footer class="modal-card-foot" v-if="type !== 'none'">
<template v-if="type === 'confirm'">
<a class="button is-success" #click.prevent="buttonClicked('yes')">Yes</a>
<a class="button is-danger" #click.prevent="buttonClicked('no')">No</a>
</template>
<template v-else-if="type === 'info'">
<a class="button" #click.prevent="buttonClicked('ok')">Ok</a>
</template>
</footer>
</div>
<button class="modal-close is-large" #click="hideModal"></button>
</div>
</transition>
</template>
<script>
export default {
props: {
show: {
type: Boolean,
default: false,
},
title: {
type: String,
default: '',
},
text: {
type: String,
default: '',
},
type: {
type: String,
default: 'info',
},
className: {
type: String,
default: '',
},
},
data() {
return {
shouldShowModal: this.show,
};
},
watch: {
show(newValue) {
this.shouldShowModal = newValue;
},
},
methods: {
hideModal() {
this.shouldShowModal = false;
this.$emit('hide');
},
buttonClicked(type) {
this.hideModal();
this.$emit('buttonClicked', type);
},
},
};
</script>
And My store module for Tabs:
const eventModule = {
namespaced: true,
state: {
/**
* List of opened tabs
*/
newList: [],
selectedNew: 0,
savedList: [],
eventToEdit: null,
},
getters: {
getNewList(state) {
return state.newList;
},
getSelectedNew(state) {
return state.selectedNew;
},
getSavedList(state) {
return state.savedList;
},
},
mutations: {
addNew(state, { location } = {}) {
state.newList.push({
typeId: null,
active: true,
logs: [],
});
},
removeNew(state, index) {
state.newList.splice(index, 1);
},
setNew(state, { index = state.selectedNew, event }) {
state.newList.splice(index, 1, event);
},
selectNew(state, selectedNew) {
state.selectedNew = selectedNew;
},
},
actions: {
/**
* opens tab for creating new event
*
* #param context
* #param location
* #param stopProp
* #returns {*}
*/
create(context, { location, stopProp } = {}) {
const newList = context.getters.getNewList;
context.commit('addNew', { location });
context.commit('selectNew', newList.length - 1);
// if (!stopProp) {
// context.dispatch('stateChanged', null, { root: true });
// }
return Promise.resolve();
},
/**
* Saves event
* #param context
* #param event
* #return {Promise|Promise.<TResult>}
*/
save(context, { event, index, hideMessage }) {
const method = (event.id) ? 'patch' : 'post';
// const data = { event, userId: context.rootGetters['user/getData'].id };
const data = { event };
const payload = { method, url: 'event', data, hideMessage };
return context.dispatch('server/http', payload, { root: true })
.then((response) => {
context.commit('setNew', { event: response.data.object, index });
context.dispatch('loadList');
})
.catch(error => Promise.reject(error));
},
select(context, { eventIndex, stopProp }) {
context.commit('selectNew', eventIndex);
},
opened(context) {
const event = JSON.parse(JSON.stringify(context.state.eventToEdit));
context.state.eventToEdit = null;
context.dispatch('create', { stopProp: true });
context.commit('setNew', { event });
},
/**
* Closes for event
* #param context
* #param eventIndex
* #param stopProp
* #return {Promise|Promise.<TResult>}
*/
close(context, { eventIndex, stopProp }) {
const newList = context.getters.getNewList;
const selectedNew = context.getters.getSelectedNew;
context.commit('removeNew', eventIndex);
if (selectedNew >= newList.length && selectedNew > 0) {
context.commit('selectNew', selectedNew - 1);
}
},
},
};
export default eventModule;
Also Here is the link to my github page where full test code is located if someone wants to take a look:
Codesandbox link
Thanx in advance.
Solved it. The problem is with keys in v-for, :key prop, should be unique, so here is how I solved it, in mutations addNew, add new property tabId add like this:
state.newList.push({
tabId: new Date.now(),
typeId: null,
active: true,
briefing: false,
logs: [],
});
and App.vue change :key=“index” to :key=“event.tabId”