首页 文章

如何以编程方式在bootstrap-vue模式的主体和页脚中注入内容?

提问于
浏览
8

我想使用bootstrap vue modal component在vuejs app中实现此功能:

当用户单击页面UI上的“删除”按钮时:

  • 它显示了其正文中包含动态内容的模式:“您确定要删除customer:customer_name_here”

  • 如果用户点击“取消”按钮:模态消失 .

  • 如果用户点击“确定”按钮:

  • 它将模态正文内容更改为:'删除客户'customer_name_here'...,它会禁用“取消”和“确定”按钮,并调用API来删除客户 .

从API收到成功响应时:

  • 它将模态正文内容更改为:'Successfully deleted customer ' 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
        })

4 回答

  • 2

    如果我理解正确,您希望根据不同的状态组合显示模态内容 .

    作为您的描述,应该有2个状态:

    • deletionState:表示是否开始删除

    • loadingState:表示是否正在等待服务器的响应

    检查Bootstrap Vue Modal Guide,然后搜索keyword = Disabling built-in buttons ,您将看到我们可以使用 cancel-disabledok-disabled props来控制默认 CancelOK 按钮的禁用状态(或者您可以使用slot = modal-footer ,或 modal-okmodal-cancel . ) .

    您可以使用的其他道具: ok-onlycancel-onlybusy .

    最后用状态组合绑定 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>
    
  • 1

    这是Bootstrap-vue模式的通用包装器组件,它根据 nextState 属性获取状态数组和导航 . 它利用 computed properties 来响应状态变化 .

    在父级中,状态数组也在计算属性中定义,以便我们可以向消息添加客户(或照片)属性 .

    Edit

    添加了内容槽,允许父组件在模态内容中定义精确标记 .

    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>
    
  • 4

    您可能更喜欢使用单独的模态,逻辑变得更清晰,您可以轻松添加更多路径,例如重试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>
    
  • -1

    正如我们在评论中讨论的那样,另一种解决方案就像Quasar Stepper .

    • 设计一个组件作为步骤(下面的演示名称为 b-step-modal ),

    • 然后使用一个模态步进器(下面的演示名称为 b-stepper-modal )作为父级 .

    • 然后你只需要列出你所有的步骤作为 modal-stepper 的孩子 . 如果您想要禁用按钮或跳过一步等,您可以使用步骤挂钩(下面演示提供 step-beginstep-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>
    

相关问题