bootstrap vue modal component :を使用してvuejsアプリにこの機能を実装したい
ユーザーがページUIの[削除]ボタンをクリックすると:
本文に動的コンテンツを含むモーダルが表示されます。「顧客を削除してもよろしいですか:customer_name_here」
ユーザーが「キャンセル」ボタンをクリックすると、モーダルは消えます。
ユーザーが「OK」ボタンをクリックした場合:
モーダルボディのコンテンツを「Deleting customer 'customer_name_here' ...」に変更し、[キャンセル]および[OK]ボタンを無効にし、APIを呼び出して顧客を削除します。
APIから正常な応答を受け取った場合:
これまでのコード:
<b-button v-b-modal.modal1 variant="danger">Delete</b-button>
<b-modal id="modal1" title="Delete Customer"
@ok="deleteCustomer" centered no-close-on-backdrop -close-on-esc ref="modal">
<p class="my-4">Are you sure, you want to delete customer:</p>
<p>{{customer.name}}</p>
</b-modal>
Vue JSコード:
deleteCustomer(evt) {
evt.preventDefault()
this.$refs.modal.hide()
CustomerApi.deleteCustomer(this.customer.id).then(response => {
// successful response
})
私が正しく理解していれば、さまざまな状態の組み合わせに基づいてモーダルコンテンツを表示したいと思います。
説明として、2つの状態があるはずです。
deletedState:削除を開始するかどうかを示します
loadingState:サーバーからの応答を待機しているかどうかを示します
Bootstrap Vue Modal Guide をチェックしてから、keyword =組み込みボタンを無効にする、 cancel-disabled
およびok-disabled
プロパティを使用して、デフォルトの無効状態Cancelおよび[〜#〜] ok [〜#〜]ボタン(または、slot =modal-footer、またはmodal-ok、modal-cancel。 )。
使用できるその他の小道具:ok-only
、cancel-only
、busy
。
最後に、v-if
とpropsを状態の組み合わせにバインドして、コンテンツを表示します。
以下のデモのように:
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>
以下に、状態の配列を取り、nextState
プロパティに従ってナビゲートするBootstrap-vueモーダルの汎用ラッパーコンポーネントを示します。 計算されたプロパティを使用して、状態の変化に応答します。
親では、状態の配列も計算されたプロパティで定義されているため、メッセージに顧客(または写真)プロパティを追加できます。
編集
親コンポーネントがモーダルコンテンツ内の正確なマークアップを定義できるコンテンツスロットを追加しました。
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>
個別のモーダルを使用することをお勧めします。ロジックが少し明確になり、たとえばAPIエラーで再試行するなど、パスウェイを簡単に追加できます。
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>