William Zink
William Zink

Reputation: 206

How to extend component and use slot

Goal: I want to be able to have a modal template that I can extend in other pages in my Vue.js (Nuxt.js) application

ModalTemplate.vue:

<!-- Base Modal Component -->
<template>
    <!-- Modal -->
    <div class="modal opacity-0 pointer-events-none fixed w-full h-full top-0 left-0 flex items-center justify-center">
        <div class="modal-overlay absolute w-full h-full bg-gray-900 opacity-50"></div>
    
        <!-- Modal Container -->
        <div class="modal-container bg-gray-300 w-5/12 mx-auto rounded shadow-lg z-50 overflow-y-auto">

            <!-- Top Right escape button (needs to be within the container for z-index purposes) -->
            <div class="modal-close absolute top-0 right-0 cursor-pointer flex flex-col items-center mt-4 mr-4 text-white text-sm z-50">
                <fa icon="times" class="fa-2x"></fa>
                <span class="text-sm">(Esc)</span>
            </div>

            <div class="modal-content">
                <!-- Title of Modal -->
                <div class="modal-title-container">
                    <slot name="modal-header"></slot>
                </div>

                <!-- Body of Modal -->
                <div class="modal-body-container">
                    <slot></slot>
                </div>

                <!-- Footer of Modal -->
                <div class="modal-footer-container">
                    <slot name="modal-footer"></slot>
                </div>
            </div>

        </div>

    </div>
</template>

<script>
export default {
  name: 'ModalTemplate',
  data() {
    return {
    }
  },
  methods: {
    toggleModal: function() {
      var body = document.querySelector('body');
      var modal = document.querySelector('.modal');
      modal.classList.toggle('opacity-0');
      modal.classList.toggle('pointer-events-none');
      body.classList.toggle('modal-active');
    },
    modalHidden: function () {
      this.toggleModal();
      this.messageBus.$emit('closing')
    },
    keyDownPressed: function(keyPressed) {
      var isEscape = false
      if (keyPressed.key === "Escape" || keyPressed.key === "Esc") {
        isEscape = true
      } else {
        isEscape = (keyPressed.keyCode === 27)
      }

      if (isEscape && document.querySelector('body').classList.contains('modal-active')) {
        this.modalHidden();
      }
    }
  },
  created: function() {
    window.addEventListener('keydown', this.keyDownPressed)
  },
  destroyed: function() {
    window.removeEventListener('keydown', this.keyDownPressed)
  },
  mounted: function() {
    this.toggleModal();

    var closeModalSelector = document.querySelectorAll('.modal-close')
    for (var i = 0; i < closeModalSelector.length; i++) {
      closeModalSelector[i].addEventListener('click', this.modalHidden)
    }
    const overlay = document.querySelector('.modal-overlay')
    overlay.addEventListener('click', this.modalHidden);
  }
}
</script>


<style lang="postcss">
.modal-page {
    @apply pointer-events-none;
    @apply fixed;
    @apply w-full;
    @apply h-full;
    @apply top-0;
    @apply left-0;
    @apply flex;
    @apply items-center;
    @apply justify-center;
}

.modal-overlay {
    @apply absolute;
    @apply w-full;
    @apply h-full;
    @apply bg-gray-900;
}

.modal-container {
    @apply bg-gray-300;    
    @apply mx-auto;
    @apply rounded;
    @apply shadow-lg;
    @apply z-50;
    @apply overflow-y-auto;
}

.modal-content {
    @apply py-4;
}

.modal-title-container {
    @apply flex;
    @apply justify-between;
    @apply items-center;
    @apply border-b;
    @apply border-gray-400;
    @apply px-4;
    @apply pb-3;
}

.modal-body-container {
    @apply py-2;
    @apply px-4;
}

.modal-footer-container {
    @apply flex;
    @apply border-t;
    @apply border-gray-400;
    @apply px-4;
    @apply pt-2;
}
</style>

CertificateDetailsModal.vue:

<template>

    <ModalTemplate ref="modal">
        <template v-slot:modal-header>
            This is a header
        </template>
    </ModalTemplate>
    
</template>

<script>
import ModalTemplate from '~/components/Modals/ModalTemplate'

export default {
    name: 'DetailsModal',
    components: {
        ModalTemplate
    },
    model: {
        prop: 'certificate',
        event: 'input'
    },
    props: {
        certificate: {
            type: Object,
            default: null
        }
    },
    mounted() {
    },
    methods: {
        closeModal: function() {
            alert('closing modal!')
            this.$store.dispatch('certificates/loadCertificates')
            this.$emit('input', null);
        }
    }
}
</script>

<style scoped>
</style>

Attempts:

I looked at extending the modal, but it gave me quite a few errors when I tried to dismiss the modal (I can provide the code if needed).

Question:

How can I extend the Modal (getting all the functionality of the functions) while adding additional functionality in the CertificateDetailsModal (such as functions, methods, and html)?

Upvotes: 0

Views: 1008

Answers (1)

tony19
tony19

Reputation: 138536

You could re-declare the slots in your wrapper component's template. For instance, the following template declares a modal-footer slot and a default slot (unnamed assumed to have a name of default):

<!-- CertificateDetailsModal.vue -->
<template>
  <ModalTemplate>
    <template v-slot:modal-header>
        My header
    </template>
    <template v-slot:modal-footer> <!-- pass `modal-footer` slot to ModalTemplate -->
      <slot name="modal-footer"></slot>
    </template>
    <slot /> <!-- pass `default` slot to ModalTemplate -->
  </ModalTemplate>
</template>

Then your app could use the CertificateDetailsModal like this:

<!-- App.vue -->
<template>
  <CertificateDetailsModal>      
    <template v-slot:modal-footer>
      <footer>My footer</footer>
    </template>

    <span>My default</span>
  </CertificateDetailsModal>
</template>

demo

Upvotes: 2

Related Questions