🎨 Handle loading state for widgets

This commit is contained in:
Alicia Sykes 2021-12-13 22:40:35 +00:00
parent 0a4d021b4e
commit 5cb588a586
3 changed files with 129 additions and 90 deletions

View File

@ -1,6 +1,5 @@
<template>
<LoadingAnimation v-if="loading" class="loader" />
<div v-else class="weather">
<div class="weather">
<!-- Icon + Temperature -->
<div class="intro">
<p class="temp">{{ temp }}</p>

View File

@ -1,91 +1,115 @@
<template>
<div class="widget-base">
<div :class="`widget-base ${ loading ? 'is-loading' : '' }`">
<!-- Update and Full-Page Action Buttons -->
<Button :click="update" class="action-btn update-btn" v-if="!error && !loading">
<UpdateIcon />
</Button>
<Button :click="fullScreenWidget" class="action-btn open-btn" v-if="!error && !loading">
<OpenIcon />
</Button>
<div v-if="loading">Loading...</div>
<div v-else-if="error" class="widget-error">
<!-- Loading Spinner -->
<div v-if="loading" class="loading">
<LoadingAnimation v-if="loading" class="loader" />
</div>
<!-- Error Message Display -->
<div v-if="error" class="widget-error">
<p class="error-msg">An error occurred, see the logs for more info.</p>
<p class="error-output">{{ errorMsg }}</p>
</div>
<Clock
v-else-if="widgetType === 'clock'"
:options="widgetOptions"
@error="handleError"
:ref="widgetRef"
/>
<Weather
v-else-if="widgetType === 'weather'"
:options="widgetOptions"
@error="handleError"
:ref="widgetRef"
/>
<WeatherForecast
v-else-if="widgetType === 'weather-forecast'"
:options="widgetOptions"
@error="handleError"
:ref="widgetRef"
/>
<TflStatus
v-else-if="widgetType === 'tfl-status'"
:options="widgetOptions"
@error="handleError"
:ref="widgetRef"
/>
<CryptoPriceChart
v-else-if="widgetType === 'crypto-price-chart'"
:options="widgetOptions"
@error="handleError"
:ref="widgetRef"
/>
<CryptoWatchList
v-else-if="widgetType === 'crypto-watch-list'"
:options="widgetOptions"
@error="handleError"
:ref="widgetRef"
/>
<XkcdComic
v-else-if="widgetType === 'xkcd-comic'"
:options="widgetOptions"
@error="handleError"
:ref="widgetRef"
/>
<ExchangeRates
v-else-if="widgetType === 'exchange-rates'"
:options="widgetOptions"
@error="handleError"
:ref="widgetRef"
/>
<StockPriceChart
v-else-if="widgetType === 'stock-price-chart'"
:options="widgetOptions"
@error="handleError"
:ref="widgetRef"
/>
<Jokes
v-else-if="widgetType === 'joke'"
:options="widgetOptions"
@error="handleError"
:ref="widgetRef"
/>
<IframeWidget
v-else-if="widgetType === 'iframe'"
:options="widgetOptions"
@error="handleError"
:ref="widgetRef"
/>
<!-- Widget -->
<div v-else class="widget-wrap">
<Clock
v-if="widgetType === 'clock'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<Weather
v-else-if="widgetType === 'weather'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<WeatherForecast
v-else-if="widgetType === 'weather-forecast'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<TflStatus
v-else-if="widgetType === 'tfl-status'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<CryptoPriceChart
v-else-if="widgetType === 'crypto-price-chart'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<CryptoWatchList
v-else-if="widgetType === 'crypto-watch-list'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<XkcdComic
v-else-if="widgetType === 'xkcd-comic'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<ExchangeRates
v-else-if="widgetType === 'exchange-rates'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<StockPriceChart
v-else-if="widgetType === 'stock-price-chart'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<Jokes
v-else-if="widgetType === 'joke'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<IframeWidget
v-else-if="widgetType === 'iframe'"
:options="widgetOptions"
@loading="setLoaderState"
@error="handleError"
:ref="widgetRef"
/>
<!-- No widget type specified -->
<div v-else>{{ handleError('No widget type was specified') }}</div>
</div>
</div>
</template>
<script>
// Import form elements, icons and utils
import ErrorHandler from '@/utils/ErrorHandler';
import Button from '@/components/FormElements/Button';
import UpdateIcon from '@/assets/interface-icons/widget-update.svg';
import OpenIcon from '@/assets/interface-icons/open-new-tab.svg';
import LoadingAnimation from '@/assets/interface-icons/loader.svg';
// Import available widgets
import Clock from '@/components/Widgets/Clock.vue';
import Weather from '@/components/Widgets/Weather.vue';
import WeatherForecast from '@/components/Widgets/WeatherForecast.vue';
@ -104,6 +128,7 @@ export default {
Button,
UpdateIcon,
OpenIcon,
LoadingAnimation,
Clock,
Weather,
WeatherForecast,
@ -134,25 +159,33 @@ export default {
}
return this.widget.type.toLowerCase();
},
/* Returns the users specified widget options, or empty object */
/* Returns users specified widget options, or empty object */
widgetOptions() {
return this.widget.options || {};
},
/* A unique string to reference the widget by */
widgetRef() {
return `widget-${this.widgetType}-${this.index}`;
},
},
methods: {
/* Calls update data method on widget */
update() {
this.$refs[this.widgetRef].update();
},
/* Shows message when error occurred */
handleError(msg) {
this.error = true;
this.errorMsg = msg;
},
/* Opens current widget in full-page */
fullScreenWidget() {
this.$emit('navigateToSection');
},
/* Toggles loading state */
setLoaderState(loading) {
this.loading = loading;
},
},
};
</script>
@ -162,6 +195,7 @@ export default {
.widget-base {
position: relative;
padding-top: 0.75rem;
// Refresh and full-page action buttons
button.action-btn {
height: 1rem;
min-width: auto;
@ -184,7 +218,7 @@ export default {
right: 1.75rem;
}
}
// Error message output
.widget-error {
p.error-msg {
color: var(--warning);
@ -199,6 +233,20 @@ export default {
margin: 0.5rem auto;
}
}
// Loading spinner
.loading {
margin: 0.2rem auto;
text-align: center;
svg.loader {
width: 100px;
}
}
// Hide widget contents while loading
&.is-loading {
.widget-wrap {
display: none;
}
}
}
</style>

View File

@ -1,49 +1,41 @@
/**
* Mixin that all pre-built and custom widgets extend from.
* Manages loading state, error handling and data updates.
*/
import ProgressBar from 'rsup-progress';
import ErrorHandler from '@/utils/ErrorHandler';
import LoadingAnimation from '@/assets/interface-icons/loader.svg';
const WidgetMixin = {
components: {
LoadingAnimation,
},
props: {
/* The options prop is an object of settings for a given widget */
options: {
type: Object,
default: {},
},
},
data: () => ({
loading: true, // Indicates current loading status, to display spinner
progress: new ProgressBar({ color: 'var(--progress-bar)' }),
}),
methods: {
/* Overridden by widget component. Re-fetches and renders any external data *
* Called by parent component, and triggered either by user or time interval */
/* Re-fetches external data, called by parent. Usually overridden by widget */
update() {
// eslint-disable-next-line no-console
console.log('No update method configured for this widget');
console.log('No update method configured for this widget'); // eslint-disable-line no-console
},
/* Called when an error occurs */
/* Called when an error occurs. Logs to handler, and passes to parent component */
error(msg, stackTrace) {
ErrorHandler(msg, stackTrace);
this.$emit('error', msg);
},
/* When a data request update starts, show loader */
startLoading() {
this.loading = true;
this.$emit('loading', true);
this.progress.start();
},
/* When a data request finishes, hide loader */
finishLoading() {
this.loading = false;
this.$emit('loading', false);
setTimeout(() => { this.progress.end(); }, 500);
},
},
mounted() {
// If the mounted function isn't overridden,then hide loader
this.loading = false;
},
};
export default WidgetMixin;