如何以编程方式在 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-disabledok-disabled属性来控制默认的禁用状态取消OK 按钮(或者您可以使用 slot=modal-footer、或 modal-okmodal-取消。)。

您可能使用的其他道具: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>

【讨论】:

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

【讨论】:

【参考方案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-beginstep-end等来控制模态本身或跳过步骤等。 干杯,谢谢,效果很好。比包装的组件长一点,但也许我只是更熟悉这种模式。当您提到步进器时,我正在考虑将步进器组件放入 b-modal 中,以直接节省对状态转换逻辑的编码。 以声明方式定义状态和路径仍然是一个很大的优势。我认为在对象而不是 html&lt;b-step-modal&gt; 标签)中这样做更灵活。

以上是关于如何以编程方式在 bootstrap-vue 模态正文和页脚中注入内容?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 vue-test-utils 打开 bootstrap-vue 模态?

如何从 vuex 操作中调用 bootstrap-vue 模态和 toast?

如何将 segue 标识符添加到以编程方式进行的模态转换?

xcode/ios:以编程方式启动模态视图控制器时如何传递数据

以编程方式从模态视图控制器切换父选项卡

如何以模态方式呈现标准 UIViewController