如何以编程方式在 bootstrap-vue 模态正文和页脚中注入内容?
Posted
技术标签:
【中文标题】如何以编程方式在 bootstrap-vue 模态正文和页脚中注入内容?【英文标题】:How to programmatically inject content in bootstrap-vue modal body and footer? 【发布时间】:2019-01-30 23:49:27 【问题描述】:我想在 vuejs 应用中使用 bootstrap vue modal component 实现这个功能:
当用户点击页面 UI 上的删除按钮时:
它显示了在其主体中具有动态内容的模态: “您确定要删除客户: customer_name_here”
如果用户点击“取消”按钮:模式消失。
如果用户点击“确定”按钮:
它将模态正文内容更改为: '删除客户'customer_name_here' ... ,它禁用取消和确定按钮,并调用 API 删除客户。
从 API 收到成功响应时:
它将模态正文内容更改为: '已成功删除客户'customer_name_here' 仅在模态页脚中显示“确定”按钮,如果单击模态页脚,该按钮将消失。目前为止的代码:
<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
)
【问题讨论】:
那么您遇到的问题是什么?看来使用v-if
/v-show
会达到目的。比如如果删除,显示警告信息和确定/取消按钮,然后隐藏删除按钮
【参考方案1】:
如果我理解正确,您希望根据不同的状态组合显示 Modal 内容。
正如你的描述,应该有两种状态:
deletingState:表示是否开始删除
loadingState:表示是否在等待服务器响应
勾选Bootstrap Vue Modal Guide,然后搜索keyword=禁用内置按钮,你会看到我们可以使用cancel-disabled
和ok-disabled
属性来控制默认的禁用状态取消 和 OK 按钮(或者您可以使用 slot=modal-footer、或 modal-ok、modal-取消。)。
您可能使用的其他道具: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>
【讨论】:
Sphinx 谢谢你的好答案效果很好,但我无法弄清楚一旦 deleteState 设置为 true 谁将其设置回 false。 @ace 有很多选择。 1. 弹出模态框时始终设置为 false(如上例所示),2. 在单击“取消”按钮或第二次单击“确定”时设置为 false。 3.监听hide事件,如果隐藏,设置状态为false【参考方案2】:您可能更喜欢使用单独的模态,逻辑变得更加清晰,您可以轻松添加更多路径,例如重试 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>
【讨论】:
Richard 感谢您提供另一个聪明的解决方案。下一次冒险我试图弄清楚如何使模态内容动态化,这意味着这些模态可以重复用于删除其他类型的对象,例如照片,在这种情况下,文本将是你确定要删除这张照片,删除照片,照片已被删除。 干杯。通用模态的想法掠过我的脑海,但看不到具体的模式。也许是一个具有渲染功能的功能组件,参考Vue NYC - VueJS Render Functions / Functional Components - Dan Aprahamian 和daprahamian/vue-render-functions-example 一个想法是定义三个数据变量initialDeleteText,deletingText,deletedText,客户的默认值,但照片对象的值会改变 确实,文本位问题不大,可以像 customer.name
一样通过插值来处理。
我正在考虑一个封装组件,它采用一组状态。正如 Sphinx 指出的那样,这是关于状态的,但是当通用版本应该采用至少 3 个列表时,他只有两个状态,第 4 个(错误)是可选的,以防不需要。状态应该是具有要显示的各种文本的属性的对象,哪些按钮是可见的,按钮应该调用的功能,以及每次按钮单击后的下一个状态。所有声明性都像基本的b-modal
API。我将发布一个示例。【参考方案3】:
正如我们在 cmets 中所讨论的,另一种解决方案类似于 Quasar Stepper。
设计一个组件作为步骤(在下面的演示中名称为b-step-modal
),
然后使用一个模态步进器(在下面的演示中名称为b-stepper-modal
)作为父级。
那么你只需要列出你作为modal-stepper
的孩子的所有步骤。如果您想禁用按钮或跳过一步等,您可以使用 step-hook(下面的演示提供 step-begin
和 step-end
)来实现目标。
如下粗略演示:
Vue.config.productionTip = false
let bModal = Vue.component('BModal')
Vue.component('b-stepper-modal',
provide ()
return
_stepper: this
,
extends: bModal,
render(h)
let _self = this
return h(bModal, props: _self.$props, ref: '_innerModal', on:
ok: function (bvEvt)
_self.currentStep++
if(_self.currentStep < _self.steps.length)
bvEvt.preventDefault()
, _self.$slots.default)
,
data()
return
steps: [],
currentStep: 0
,
methods:
_registerStep(step)
this.steps.push(step)
,
show ()
this.$refs._innerModal.show()
)
Vue.component('b-step-modal',
inject:
_stepper:
default ()
console.error('step must be child of stepper')
,
props: ['stepBegin', 'stepEnd'],
data ()
return
isActive: false,
stepSeq: 0
,
render(h)
return this.isActive ? h('p', , this.$slots.default) : null
,
created ()
this.$watch('_stepper.currentStep', function (newVal, oldVal)
if(oldVal)
if(typeof this.stepEnd === 'function') this.stepEnd()
else
if(typeof this.stepBegin === 'function') this.stepBegin()
this.isActive = (newVal === this.stepSeq)
)
,
mounted ()
this.stepSeq = this._stepper.steps.length
this._stepper._registerStep(this)
this.isActive = this._stepper.currentStep === this.stepSeq
)
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')
,
testStepBeginHandler: function ()
this.deletingState = true
this.loadingState = true
setTimeout(() =>
console.log('simulate to wait for server respond...')
this.loadingState = false
this.deletingState = true
, 1500)
,
testStepEndHandler: function ()
console.log('step from show to hide')
)
<!-- 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-stepper-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">
<b-step-modal>
<div>
<p class="my-4">Are you sure, you want to delete customer:<span class="customer-name">customer.name</span></p>
</div>
</b-step-modal>
<b-step-modal :step-begin="testStepBeginHandler" :step-end="testStepEndHandler">
<div>
<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-step-modal>
</b-stepper-modal>
</div>
【讨论】:
【参考方案4】:这是一个用于 Bootstrap-vue 模态的通用包装器组件,它采用一组状态并根据 nextState
属性进行导航。它利用计算属性来响应状态变化。
在父级中,状态数组也在计算属性中定义,以便我们可以将客户(或照片)属性添加到消息中。
编辑
添加了内容槽,允许父组件在模态内容中定义准确的标记。
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 :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>
【讨论】:
出于通用目的,我认为使用 slott&scoped-slot 会更好,就像实现 Quasar Stepper 一样。 从原理上讲,步进器并不是一个坏主意,但请看一下示例 - 代码与内容注入模式一样长。您可能希望松开步长指示器,并且按钮应位于固定的页脚位置,而不是随内容高度而改变位置。并且需要分支逻辑而不仅仅是线性步骤。 这是one rough demo,我的想法,使用模板控制内容,使用step-begin
和step-end
等来控制模态本身或跳过步骤等。
干杯,谢谢,效果很好。比包装的组件长一点,但也许我只是更熟悉这种模式。当您提到步进器时,我正在考虑将步进器组件放入 b-modal 中,以直接节省对状态转换逻辑的编码。
以声明方式定义状态和路径仍然是一个很大的优势。我认为在对象而不是 html(<b-step-modal>
标签)中这样做更灵活。以上是关于如何以编程方式在 bootstrap-vue 模态正文和页脚中注入内容?的主要内容,如果未能解决你的问题,请参考以下文章
如何使用 vue-test-utils 打开 bootstrap-vue 模态?
如何从 vuex 操作中调用 bootstrap-vue 模态和 toast?