Skip to content

Instantly share code, notes, and snippets.

@jennevdmeer
Last active May 8, 2019 14:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jennevdmeer/7cd81b6fe68196685d8f083b6931d92e to your computer and use it in GitHub Desktop.
Save jennevdmeer/7cd81b6fe68196685d8f083b6931d92e to your computer and use it in GitHub Desktop.
vue promise button

promise is the promise object, this property is being watched so will reset on change. And default to default if none is provided.

reset on 0 wont reset the button's finaly state, however when a number is entered it will reset to 'default' after said timeout is finished.

disable can be used to disabled the button (button also disables itself while loading, so need to do anything with that).

Slots are optional, will default to well default content. Can also use CSS classes to change apearence.

Events are passed down with v-on="$listeners", so whatever you like.

<PromiseButton @click="onClick" :reset="5000" :promise="promise" :disabled="false">
    <template v-slot:rejected>Foei!</template>
    <template v-slot:resolved>Aloha.</template>
    <template v-slot:loading>Loading</template>
    <template v-slot:default>Versturenz</template>
</PromiseButton>

Your click handler just needs to update the promise property, eg something like:

onClick() {
    this.promise = new Promise((r, e) => 
        setTimeout(() => {
            (Math.random > .5)
                ? r('Resolved.')
                : e('Error.')
        }, 1000);
}

However the promise can come from anything, eg creating an api load call in mounted, sending said promise along. Would only enabled the button once the it has been resolved, rejected or reset.

<template>
<button v-on="$listeners" :type="type" :class="Classes" :disabled="Loading || disabled">
<slot v-if="Loading" name="loading"></slot>
<slot v-else-if="Resolved" name="resolved"></slot>
<slot v-else-if="Rejected" name="rejected"></slot>
<slot v-else>Versturen</slot>
</button>
</template>
<script>
import ExtendedPromise from '@App/ExtendedPromise';
export default {
name: '',
props: {
disabled: Boolean,
promise: Promise,
type: {
type: String,
default: 'button'
},
reset: {
type: Number,
default: 0
}
},
data: () => ({
resetTimeout: undefined,
internalPromise: undefined
}),
computed: {
Loading () { return this.internalPromise && this.internalPromise.pending; },
Resolved() { return this.internalPromise && this.internalPromise.resolved; },
Rejected() { return this.internalPromise && this.internalPromise.rejected; },
Classes() {
const classes = [ 'promise-btn' ];
if (this.Loading) {
classes.push('promise-btn-loading');
} else if (this.Resolved) {
classes.push('promise-btn-resolved');
} else if (this.Rejected) {
classes.push('promise-btn-rejected');
}
return classes;
}
},
watch: {
promise(promise) {
if (this.internalPromise) {
this.internalPromise.cancel();
this.$emit('canceled', this.internalPromise);
}
if (this.resetInterval) {
clearInterval(this.resetInterval);
this.resetInterval = undefined;
}
let extendedPromise;
if (promise instanceof ExtendedPromise) {
extendedPromise = promise;
} else {
extendedPromise = new ExtendedPromise(promise);
}
extendedPromise
.then(r => {
this.loaded = true
this.$emit('resolved', r);
})
.catch(e => {
this.error = true
this.$emit('rejected', e);
})
.finally(f => {
this.loading = false;
this.$emit('finally');
if (this.reset > 0) {
setTimeout(() => this.internalPromise = undefined, this.reset);
}
});
this.$emit('loading', extendedPromise);
this.internalPromise = extendedPromise;
}
}
};
</script>
// Some wip class for cancelable promises (well not realy cancels them but ignores their handlers).
// Also has some extra state properties.
const call = (cancelablePromise, callables, args) => {
if (cancelablePromise.canceled) {
return;
}
for (let k in callables) {
callables[k].apply(
cancelablePromise,
args
);
}
};
export default class ExtendedPromise {
constructor(promise) {
if (!promise instanceof Promise) {
throw new Error('Invalid argument, expected first argument to be of type Promise got ' + typeof(promise));
}
this.canceled = false;
this.resolved = false;
this.rejected = false;
this.pending = true;
this.callables = {
rejectors: [],
resolvers: [],
finalizers: []
};
let self = this;
this.promise = promise
.then(function() {
self.resolved = true;
call(self, self.callables.resolvers, arguments);
})
.catch(function() {
self.rejected = true;
call(self, self.callables.rejectors, arguments);
})
.finally(function() {
self.pending = false;
call(self, self.callables.finalizers, arguments);
});
}
cancel() { this.canceled = true; }
continue() { this.canceled = false; }
then(callable) {
if (typeof(callable) === 'function') {
this.callables.resolvers.push(callable);
}
return this;
}
catch(callable) {
if (typeof(callable) === 'function') {
this.callables.rejectors.push(callable);
}
return this;
}
finally(callable) {
if (typeof(callable) === 'function') {
this.callables.finalizers.push(callable);
}
return this;
}
remove(callable) {
if (typeof(callable) !== 'function') {
return;
}
var index = this.callables.resolvers.indexOf(callable);
if (-1 !== index) {
return this.callables.resolvers.splice(index, 1)[0];
}
var index = this.callables.rejectors.indexOf(callable);
if (-1 !== index) {
return this.callables.rejectors.splice(index, 1)[0];
}
var index = this.callables.finalizers.indexOf(callable);
if (-1 !== index) {
return this.callables.finalizers.splice(index, 1)[0];
}
}
}
.promise-btn {
@extend .btn, .btn-primary;
&-loading { @extend .btn-secondary; }
&-resolved { @extend .btn-success; }
&-rejected { @extend .btn-error; }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment