How to programmatically inject content in bootstrap-vue modal body and footer?
If I understand correctly, you'd like to display the Modal content based on different state combinations.
As your descriptions, there should be 2 state:
deletingState: it indicates whether begin deleting
loadingState: it indicates whether is waiting the response from the server
Check Bootstrap Vue Modal Guide, then search keyword= Disabling built-in buttons, you will see we can use cancel-disabled
and ok-disabled
props to control the disable state of default Cancel and OK buttons (or you can use the slot=modal-footer, or modal-ok, modal-cancel.).
Other props you may use: ok-only
, cancel-only
, busy
.
Finally bind v-if
and props with the state combinations to show the content.
Like below demo:
Vue.config.productionTip = false
new Vue({
el: '#app',
data() {
return {
customer: {name: 'demo'},
deletingState: false, // init=false, if pop up modal, change it to true
loadingState: false // when waiting for server respond, it will be true, otherwise, false
}
},
methods: {
deleteCustomer: function() {
this.deletingState = false
this.loadingState = false
this.$refs.myModalRef.show()
},
proceedReq: function (bvEvt) {
if(!this.deletingState) {
bvEvt.preventDefault() //if deletingState is false, doesn't close the modal
this.deletingState = true
this.loadingState = true
setTimeout(()=>{
console.log('simulate to wait for server respond...')
this.loadingState = false
this.deletingState = true
}, 1500)
} else {
console.log('confirm to delete...')
}
},
cancelReq: function () {
console.log('cancelled')
}
}
})
.customer-name {
background-color:green;
font-weight:bold;
}
<!-- Add this to <head> -->
<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>
<!-- Add this after vue.js -->
<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 v-b-modal.modal1 variant="danger" @click="deleteCustomer()">Delete</b-button>
<b-modal title="Delete Customer" centered no-close-on-backdrop no-close-on-esc ref="myModalRef"
@ok="proceedReq($event)" @cancel="cancelReq()" :cancel-disabled="deletingState" :ok-disabled="loadingState" :ok-only="deletingState && !loadingState">
<div v-if="!deletingState">
<p class="my-4">Are you sure, you want to delete customer:<span class="customer-name">{{customer.name}}</span></p>
</div>
<div v-else>
<p v-if="loadingState">
Deleting customer <span class="customer-name">{{customer.name}}</span>
</p>
<p v-else>
Successfully deleted customer <span class="customer-name">{{customer.name}}</span>
</p>
</div>
</b-modal>
</div>
You might prefer to use separate modals, the logic becomes a bit clearer and you can easily add more pathways, for example retry on API error.
console.clear()
const CustomerApi = {
deleteCustomer: (id) => {
return new Promise((resolve,reject) => {
setTimeout(() => {
if (id !== 1) {
reject(new Error('Delete has failed'))
} else {
resolve('Deleted')
}
}, 3000);
});
}
}
new Vue({
el: '#app',
data() {
return {
customer: {id: 1, name: 'myCustomer'},
id: 1,
error: null
}
},
methods: {
deleteCustomer(e) {
e.preventDefault()
this.$refs.modalDeleting.show()
this.$refs.modalDelete.hide()
CustomerApi.deleteCustomer(this.id)
.then(response => {
this.$refs.modalDeleting.hide()
this.$refs.modalDeleted.show()
})
.catch(error => {
this.error = error.message
this.id = 1 // For demo, api success 2nd try
this.$refs.modalError.show()
})
}
}
})
<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 v-b-modal.modal-delete variant="danger">Delete</b-button>
<input type="test" id="custId" v-model="id">
<label for="custId">Enter 2 to make it fail</label>
<b-modal
id="modal-delete"
ref="modalDelete"
title="Delete Customer"
@ok="deleteCustomer"
centered no-close-on-backdrop close-on-esc>
<p class="my-4">Are you sure, you want to delete customer: {{customer.name}}</p>
</b-modal>
<b-modal
ref="modalDeleting"
title="Deleting Customer"
centered no-close-on-backdrop no-close-on-esc
no-fade
:busy="true">
<p class="my-4">Deleting customer: {{customer.name}}</p>
</b-modal>
<b-modal
ref="modalDeleted"
title="Customer Deleted"
centered no-close-on-backdrop close-on-esc
no-fade
:ok-only="true">
<p class="my-4">Customer '{{customer.name}}' has been deleted</p>
</b-modal>
<b-modal
ref="modalError"
title="Error Deleting Customer"
centered no-close-on-backdrop close-on-esc
no-fade
:ok-title="'Retry'"
@ok="deleteCustomer">
<p class="my-4">An error occured deleting customer: {{customer.name}}</p>
<p>Error message: {{error}}</p>
</b-modal>
</div>
Here is a generic wrapper component for Bootstrap-vue modal that takes an array of states and navigates according to the nextState
property. It makes use of computed properties to respond to the state changes.
In the parent, the array of states is also defined in a computed property so that we can add customer (or photo) properties to the messages.
Edit
Added content slots which allow the parent component to define the exact markup inside the modal content.
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>