vue gallery adding loading spinner - javascript

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;
}
}
}

Related

How to pass data after page loading to components in VueJS?

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

nuxtjs vuejs #error is not firing in a component when trying to load a fallback image

I have a component that I feed with props.
When an image throws a 404 I want to load a fallback image. So far so good.
However the #error function never fires when an image is broken and I can't figure out why! I never get 'hello event' in the console.
The component is on the first level of a nuxt page, my setup is a static SPA.
I tried to implement it as mentioned at the end of this github issue: https://github.com/vuejs/vue/issues/5404
<template>
<div class="channel">
<div class="img">
<img :src="imgUrl" :alt="name" :title="name" #error="getFallBackImage" />
</div>
</div>
</template>
<script>
export default {
data: function() {
return {
chanCover: this.cover
}
},
props: {
name: {
type: String,
required: true
},
cover: {
type: String,
required: true
}
},
computed: {
imgUrl() {
console.log(this.chanCover)
return "channels/"+this.chanCover+".jpg"
},
},
methods: {
getFallBackImage(event) {
console.log("hello event")
this.chanCover = "default"
}
}
}
</script>

How to call a method in a Vue component from programmatically inserted component

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.

Data value not updated with tabs in Vue js

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”

How can I use a setTimeout to remove a message banner + VueJs 2?

I have a vueJs transition which brings in a message banner into view once a button is clicked. I want to use a setTimeout to remove the banner after a certain amount of time if a user does not interact with the UNDO button.
My Code for the message banner
HTML
<div v-for="card in cards">
<credit-card v-if="!card.creditCardRemoved" :card="card" #remove="removeCard(card)"></credit-card>
<transition name="fade" #leave="removeMessage">
<div class="o-message-banner" v-if="card.creditCardRemoved == true">
<div class="pull-left"> Your <strong> {{ card.name }} </strong> credit/cheque card has been removed </div>
<a class="o-undo" #click="undoDelete"> Undo </a>
</div>
</transition>
</div>
JS
data: {
return {
cards: [{
name: 'visa',
creditCardRemoved: false
}, {
name: 'mastercard',
creditCardRemoved: false
}],
card: '',
message: false
}
},
methods: {
undoDelete: function(card) {
card.creditCardRemoved = false;
},
removeCard: function(card) {
card.creditCardRemoved = true;
this.removeMessage();
},
removeMessage: function() {
setTimeout(function() {
this.message = !this.message;
}, 5000);
}
}

Categories

Resources