itsource

bootstrap-vue 모달 본문과 바닥글에 콘텐츠를 프로그래밍 방식으로 주입하려면 어떻게 해야 합니까?

mycopycode 2022. 9. 29. 00:17
반응형

bootstrap-vue 모달 본문과 바닥글에 콘텐츠를 프로그래밍 방식으로 주입하려면 어떻게 해야 합니까?

부트스트랩 vue 모드 컴포넌트를 사용하여 vuejs 앱에서 다음 기능을 구현합니다.

사용자가 페이지 UI의 [Delete]버튼을 클릭하면 다음과 같이 됩니다.

  • "고객을 삭제하시겠습니까: customer_name_here"라는 동적 콘텐츠가 포함된 모달입니다.

  • 사용자가 '취소' 버튼을 클릭하는 경우:모달은 사라진다.

  • 사용자가 '확인' 버튼을 클릭하는 경우:

  • 모달 본문의 내용을 다음과 같이 변경합니다.고객 'customer_name_here'를 삭제하면 Cancel 버튼과 OK 버튼이 비활성화되고 API가 호출되어 고객을 삭제합니다.

API에서 응답이 정상적으로 수신된 경우:

  • 모달 본문의 내용이 다음과 같이 변경됩니다. '고객 'customer_name_here'를 성공적으로 삭제했습니다.
  • 모달 바닥글에 OK 버튼만 표시합니다.모달 클릭 시 해당 버튼은 사라집니다.

지금까지의 코드는 다음과 같습니다.

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

제가 이해한 것이 맞다면, 당신은 다른 상태의 조합에 따라 모달 콘텐츠를 표시하고자 합니다.

설명대로 다음 두 가지 상태가 있습니다.

  1. deleting State: 삭제를 시작할지 여부를 나타냅니다.

  2. loading State: 가 서버로부터의 응답을 대기하고 있음을 나타냅니다.

Bootstrap Vue Modal Guide를 확인하고 검색 키워드= 기본 제공 버튼을 비활성화하면 사용할 수 있습니다.cancel-disabled그리고.ok-disabled기본 Cancel 버튼 OK 버튼의 비활성화 상태를 제어하는 소품(또는 slot=modal-filename 또는 modal-ok, modal-filename 사용 가능)

기타 사용할 수 있는 소품:ok-only,cancel-only,busy.

마지막으로 바인드v-if소품이나 스테이트 콤비네이션이 들어가 있습니다.

아래와 같은 데모:

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>

별도의 모달 사용을 선호할 수 있습니다. 그러면 논리가 좀 더 명확해지고 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>

코멘트에서도 설명했듯이 Quasar Stepper와 같은 솔루션이 있습니다.

  1. 하나의 컴포넌트를 스텝으로 설계합니다(이름:b-step-modal아래 데모에서)

  2. 다음으로 1개의 모달스테퍼를 사용합니다(이름은b-stepper-modal아래 데모에서)를 부모로 합니다.

  3. 그럼 넌 네 모든 단계를 나열하기만 하면 돼modal-stepper버튼을 비활성화하거나 스텝을 건너뛰고 싶은 경우 스텝 훅을 사용할 수 있습니다(아래 데모에서는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>

컴포넌트는 일련의 하여 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 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>

언급URL : https://stackoverflow.com/questions/52005443/how-to-programmatically-inject-content-in-bootstrap-vue-modal-body-and-footer

반응형