Search code examples
laravelvue.jsmodal-dialogvuejs3laravel-breeze

How do I write a vue popup modal for a login / register component


What I am trying to design is a new Login / Register modal for using with the standard Vue Login & Register scaffolding that comes with Laravel Breeze.

Exactly what I am trying to achieve is this:

Login / Register Button -> Clicked and a modal opens The modal contains a header section where there is a "Login" tab and a "Register" tab, by default the login tab is selected. It then contains a body section that is a panel that switches between the "login.vue" file and the "register.vue" file.

It is from my understanding that I need to do the following:

  1. Create a button that when clicked open the modal component.

  2. Create a Vue component to house the modal code & tabs that conditionally render the login or register page.

My file structure is as follows

resources | js | Pages | Auth \-\> Login.vue (standard breeze code)

resources | js | Pages | Auth \-\> Register.vue (standard breeze code)

resources | js | Pages | Modal \-\> LoginRegister.vue

Thanks in advance, I would really appreciate guidance.


Solution

  • The solution for the tabbed component (LoginRegister.vue) that switches between the login & register page was to use headless UI and create the tabs & dynamically rendered login or register component, with just importing the login.vue & register.vue, this is how I did it:

    <template>
        <div class="mx-auto max-w-xl px-2 sm:px-0">
            <TabGroup>
                <TabList class="flex rounded-xl bg-blue-900/20 p-1 space-x-1">
                    <Tab
                        as="template"
                        v-slot="{ selected }"
                    >
                        <button
                            :class="[
                  'w-full rounded-lg py-2.5 p-4 font-medium leading-5 text-slate-800 text-blue-700',
                  'ring-white ring-opacity-60 ring-offset-2 ring-offset-blue-400 focus:outline-none focus:ring-2',
                  selected
                    ? 'bg-white shadow'
                    : 'text-blue-100 hover:bg-white/[0.12] hover:text-white',
                ]"
                        >
                            Login
                        </button>
                    </Tab>
                    <Tab
                        as="template"
                        v-slot="{ selected }"
                    >
                        <button
                            :class="[
                  'w-full rounded-lg py-2.5 p-4 font-medium leading-5 text-blue-700',
                  'ring-white ring-opacity-60 ring-offset-2 ring-offset-blue-400 focus:outline-none focus:ring-2',
                  selected
                    ? 'bg-white shadow'
                    : 'text-blue-100 hover:bg-white/[0.12] hover:text-white',
                ]"
                        >
                            Register
                        </button>
                    </Tab>
                </TabList>
                <TabPanels class="mt-2">
                    <TabPanel class="rounded-xl bg-white p-3 ring-white ring-opacity-60 ring-offset-2 ring-offset-blue-400 focus:outline-none focus:ring-2">
                        <Login/>
                    </TabPanel>
                    <TabPanel class="rounded-xl bg-white p-3 ring-white ring-opacity-60 ring-offset-2 ring-offset-blue-400 focus:outline-none focus:ring-2">
                        <Register/>
                    </TabPanel>
                </TabPanels>
            </TabGroup>
        </div>
    </template>
    
    
    <script setup>
    import Login from "@/Pages/Auth/Login.vue";
    import Register from "@/Pages/Auth/Register.vue";
    import {Tab, TabGroup, TabList, TabPanel, TabPanels} from '@headlessui/vue'
    </script>
    

    I Then was able to use a modal component I found, which is the following code:

    <script setup>
    import { computed, onMounted, onUnmounted, watch } from 'vue';
    import LoginRegister from "@/Pages/Auth/LoginRegister.vue";
    
    const props = defineProps({
        show: {
            type: Boolean,
            default: false,
        },
        maxWidth: {
            type: String,
            default: '2xl',
        },
        closeable: {
            type: Boolean,
            default: true,
        },
    });
    
    const emit = defineEmits(['close']);
    
    watch(() => props.show, () => {
        if (props.show) {
            document.body.style.overflow = 'hidden';
        } else {
            document.body.style.overflow = null;
        }
    });
    
    const close = () => {
        if (props.closeable) {
            emit('close');
        }
    };
    
    const closeOnEscape = (e) => {
        if (e.key === 'Escape' && props.show) {
            close();
        }
    };
    
    onMounted(() => document.addEventListener('keydown', closeOnEscape));
    
    onUnmounted(() => {
        document.removeEventListener('keydown', closeOnEscape);
        document.body.style.overflow = null;
    });
    
    const maxWidthClass = computed(() => {
        return {
            'sm': 'sm:max-w-sm',
            'md': 'sm:max-w-md',
            'lg': 'sm:max-w-lg',
            'xl': 'sm:max-w-xl',
            '2xl': 'sm:max-w-2xl',
        }[props.maxWidth];
    });
    </script>
    
    <template>
        <teleport to="body">
            <transition leave-active-class="duration-200">
                <div v-show="show" class="fixed inset-0 overflow-y-auto px-4 py-6 sm:px-0 z-50" scroll-region>
                    <transition
                        enter-active-class="ease-out duration-300"
                        enter-from-class="opacity-0"
                        enter-to-class="opacity-100"
                        leave-active-class="ease-in duration-200"
                        leave-from-class="opacity-100"
                        leave-to-class="opacity-0"
                    >
                        <div v-show="show" class="fixed inset-0 transform transition-all" @click="close">
                            <div class="absolute inset-0 bg-gray-500 opacity-75" />
                        </div>
                    </transition>
    
                    <transition
                        enter-active-class="ease-out duration-300"
                        enter-from-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
                        enter-to-class="opacity-100 translate-y-0 sm:scale-100"
                        leave-active-class="ease-in duration-200"
                        leave-from-class="opacity-100 translate-y-0 sm:scale-100"
                        leave-to-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
                    >
                        <div v-show="show" class="mb-6 bg-white rounded-lg overflow-hidden shadow-xl transform transition-all sm:w-full sm:mx-auto" :class="maxWidthClass">
                            <div v-if="show">
                               <LoginRegister />
                            </div>
                        </div>
                    </transition>
                </div>
            </transition>
        </teleport>
    </template>