console.clear()
// Mock CustomerApi
const CustomerApi = {
deleteCustomer: (id) => {
console.log('id', id)
return new Promise((resolve,reject) => {
setTimeout(() => {
if (id !== 1) {
reject(new Error('Delete has failed'))
} else {
resolve('Deleted')
}
}, 3000);
});
}
}
// Wrapper component to handle state changes
Vue.component('state-based-modal', {
template: `
<b-modal
ref="innerModal"
:title="title"
:ok-disabled="okDisabled"
:cancel-disabled="cancelDisabled"
:busy="busy"
@ok="handleOk"
:ok-title="okTitle"
@hidden="hidden"
v-bind="otherAttributes"
>
<div class="content flex-grow" :style="{height: height}">
<!-- named slot applies to current state -->
<slot :name="currentState.id + 'State'" v-bind="currentState">
<!-- default content if no slot provided on parent -->
<p>{{message}}</p>
</slot>
</div>
</b-modal>`,
props: ['states', 'open'],
data: function () {
return {
current: 0,
error: null
}
},
methods: {
handleOk(evt) {
evt.preventDefault();
// save currentState so we can switch display immediately
const state = {...this.currentState};
this.displayNextState(true);
if (state.okButtonHandler) {
state.okButtonHandler()
.then(response => {
this.error = null;
this.displayNextState(true);
})
.catch(error => {
this.error = error.message;
this.displayNextState(false);
})
}
},
displayNextState(success) {
const nextState = this.getNextState(success);
if (nextState == -1) {
this.$refs.innerModal.hide();
this.hidden();
} else {
this.current = nextState;
}
},
getNextState(success) {
// nextState can be
// - a string = always go to this state
// - an object with success or fail pathways
const nextState = typeof this.currentState.nextState === 'string'
? this.currentState.nextState
: success && this.currentState.nextState.onSuccess
? this.currentState.nextState.onSuccess
: !success && this.currentState.nextState.onError
? this.currentState.nextState.onError
: undefined;
return this.states.findIndex(state => state.id === nextState);
},
hidden() {
this.current = 0; // Reset to initial state
this.$emit('hidden'); // Inform parent component
}
},
computed: {
currentState() {
const currentState = this.current;
return this.states[currentState];
},
title() {
return this.currentState.title;
},
message() {
return this.currentState.message;
},
okDisabled() {
return !!this.currentState.okDisabled;
},
cancelDisabled() {
return !!this.currentState.cancelDisabled;
},
busy() {
return !!this.currentState.busy;
},
okTitle() {
return this.currentState.okTitle;
},
otherAttributes() {
const otherAttributes = this.currentState.otherAttributes || [];
return otherAttributes
.reduce((obj, v) => { obj[v] = null; return obj; }, {})
},
},
watch: {
open: function(value) {
if (value) {
this.$refs.innerModal.show();
}
}
}
})
// Parent component
new Vue({
el: '#app',
data() {
return {
customer: {id: 1, name: 'myCustomer'},
idToDelete: 1,
openModal: false
}
},
methods: {
deleteCustomer(id) {
// Return the Promise and let wrapper component handle result/error
return CustomerApi.deleteCustomer(id)
},
modalIsHidden(event) {
this.openModal = false; // Reset to start condition
}
},
computed: {
avatar() {
return `https://robohash.org/${this.customer.name}?set=set4`
},
modalStates() {
return [
{
id: 'delete',
title: 'Delete Customer',
message: `delete customer: ${this.customer.name}`,
okButtonHandler: () => this.deleteCustomer(this.idToDelete),
nextState: 'deleting',
otherAttributes: ['centered no-close-on-backdrop close-on-esc']
},
{
id: 'deleting',
title: 'Deleting Customer',
message: `Deleting customer: ${this.customer.name}`,
okDisabled: true,
cancelDisabled: true,
nextState: { onSuccess: 'deleted', onError: 'error' },
otherAttributes: ['no-close-on-esc'],
contentHeight: '250px'
},
{
id: 'deleted',
title: 'Customer Deleted',
message: `Deleting customer: ${this.customer.name}`,
cancelDisabled: true,
nextState: '',
otherAttributes: ['close-on-esc']
},
{
id: 'error',
title: 'Error Deleting Customer',
message: `Error deleting customer: ${this.customer.name}`,
okTitle: 'Retry',
okButtonHandler: () => this.deleteCustomer(1),
nextState: 'deleting',
otherAttributes: ['close-on-esc']
},
];
}
}
})
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css" />
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<script src="//unpkg.com/babel-polyfill@latest/dist/polyfill.min.js"></script>
<script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.js"></script>
<div id="app">
<b-button @click="openModal = true" variant="danger">Delete</b-button>
<input type="test" id="custId" v-model="idToDelete">
<label for="custId">Enter 2 to make it fail</label>
<state-based-modal
:states="modalStates"
:open="openModal"
@hidden="modalIsHidden"
>
<template slot="deleteState" scope="state">
<img alt="Mindy" :src="avatar" style="width: 150px">
<p>DO YOU REALLY WANT TO {{state.message}}</p>
</template>
<template slot="errorState" scope="state">
<p>Error message: {{state.error}}</p>
</template>
</state-based-modal>
</div>