Search code examples
typescriptvue-componentvuejs3bootstrap-5

Vuejs 3 and Bootstrap 5 Modal Reusable Component Show Programmatically


Tring to create a (semi) reusable Modal Component based on Bootstrap 5,with vuejs 3 and composible API. Managed to get it partially working,
Given (Mostly standard Bootstrap 5 modal, but with classes being added based on 'show' prop, and slots in body and footer):

<script setup lang="ts">
defineProps({
  show: {
    type: Boolean,
    default: false,
  },
  title: {
    type: String,
    default: "<<Title goes here>>",
  },
});
</script>

<template>
  <div class="modal fade" :class="{ show: show, 'd-block': show }"
    id="exampleModal" tabindex="-1" aria-labelledby="" aria-hidden="true">
    <div class="modal-dialog">
      <div class="modal-content">
        <div class="modal-header">
          <h5 class="modal-title" id="exampleModalLabel">{{ title }}</h5>
          <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
        </div>
        <div class="modal-body">
          <slot name="body" />
        </div>
        <div class="modal-footer">
          <slot name="footer" />
          <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
            Close
          </button>              
        </div>
      </div>
    </div>
  </div>
</template>

and being 'called' by

<script setup lang="ts">
import { ref } from "vue";
import Modal from "@/components/Common/Modal.vue";

let modalVisible= ref(false);

function showModal(){
 modalVisible.value = true;
}
</script>

<template>
  <button @click="showModal">Show Modal</button>
  <Modal title="Model title goes here" :show="modalVisible">
    <template #body>This should be in the body</template>
    <template #footer>
      <button class="btn btn-primary">Extra footer button</button>
    </template>
</Modal>
</template>

I get a modal 'shown' but the fade in animation doesn't work ,and the backdrop isn't visible, and the data-bs- buttons in the modal don't work ( i.e. it won't close). I feel its something to do with my whole approach.

enter image description here

NOTE. I cannot use a standard button with data-bs-toggle="modal" data-bs-target="#exampleModal" attributes as the actual trigger of this model comes from the logic of another component (as in just setting a bool), and the reusable modal component will be independent of its trigger --- it also doesn't feel the proper 'Vue' way to do it.

So I think I'm just 'showing' the html, and I need to instantiate a bootstrap modal somehow... just not sure how to do it

package.json (well the relavant ones)

"dependencies": {
    "@popperjs/core": "^2.11.2",
    "bootstrap": "^5.1.3",
    "vue": "^3.2.31",
  },

Code sand box here (Couldn't get the new Composition API and TS with working on code sandbox, so its a slight re-write with the standard options API approach, so code is slightly different, but exibits the same behaviour)


Solution

  • OK.. so a few more hours I came up with a solution, posting here as it may help others. The bootstrap modal 'Object' needs to be created. So first had to import the modal object from bootstrap. Its creation needed a DOM reference, so had to add a ref to the html element, and a ref prop in the script to hold the link to it. The DOM references in Vue aren't populated until the component is mounted so the construction of the Bootstrap modal object needs to be done in Onmounted as the ref will now link to the actual DOM element. Then instead of passing a show prop down, as making this keep in sync between parent and child was cumbersome, I just exposed a show method on the dialog component itself (also feels a bit more elegant). Since <script setup> objects are CLOSED BY DEFAULT, the exposure of the method is done via defineExpose.. and we now all disco

    <script setup lang="ts">
    import { onMounted, ref } from "vue";
    import { Modal } from "bootstrap";
    defineProps({
      title: {
        type: String,
        default: "<<Title goes here>>",
      },
    });
    let modalEle = ref(null);
    let thisModalObj = null;
    
    onMounted(() => {
      thisModalObj = new Modal(modalEle.value);
    });
    function _show() {
      thisModalObj.show();
    }
    defineExpose({ show: _show });
    </script>
    
    <template>
      <div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby=""
        aria-hidden="true" ref="modalEle">
        <div class="modal-dialog">
          <div class="modal-content">
            <div class="modal-header">
              <h5 class="modal-title" id="exampleModalLabel">{{ title }}</h5>
              <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body">
              <slot name="body" />
            </div>
            <div class="modal-footer">
              <slot name="footer"></slot>
              <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
                Close
              </button>
            </div>
          </div>
        </div>
      </div>
    </template>
    

    and the 'parent'

    <script setup lang="ts">
    import { ref } from "vue";
    import Modal from "@/components/Common/Modal.vue";
    
    let thisModal= ref(null);
    
    function showModal(){
     thisModal.value.show();
    }
    </script>
    
    <template>
      <button @click="showModal">Show Modal</button>
      <Modal title="Model title goes here" ref="thisModal">
        <template #body>This should be in the body</template>
        <template #footer>
          <button class="btn btn-primary">Extra footer button</button>
        </template>
    </Modal>
    </template>
    

    .. probably should additionally should add an OnUnmount to clean up the object to be tidy.