Создаем переиспользуемый компонент модального окна на Vue.js
Инструкция по созданию переиспользуемого компонента модального окна на Vue.js.
Оглавление
Модальное окно — один из важнейших элементов пользовательского интерфейса. Он позволяет отображать что-либо или получать ответ от пользователя, не переходя на другие страницы. В данной статье, я создам компонент модального окна на Vue.js, который можно будет повторно использовать на каждой странице.
У компонента будет несколько важных преимуществ:
- Закрытие модального окна по клику на background и на клавишу
Esc
. - Использование слотов для рендера контента.
- Возможность добавления прокрутки для контента.
- Переход анимации при открытии и закрытии модального окна.
- Accessibility c ARIA attributes.
- Гибкая стилизация модального окна
Создание шаблона HTML / CSS
Для начала создаем компонент с любыми именем, базовой разметкой и стилями для отображения модального окна:
<template>
<div class="simple-modal">
<div class="simple-modal-backdrop">
<div class="simple-modal-container">
<div class="simple-modal-content">
<header class="simple-modal-header">
Modal title
</header>
<section class="simple-modal-body">
Modal body
</section>
<footer class="simple-modal-footer">
<button type="button">
Close
</button>
</footer>
</div>
</div>
</div>
</div>
</template>
<style lang="scss">
.simple-modal {
&-backdrop {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
transition: opacity 0.3s ease;
z-index: 9999;
}
&-container {
position: fixed;
top: 0;
right: 0;
left: 0;
bottom: 0;
width: auto;
margin: 16px;
}
&-content {
display: flex;
flex-direction: column;
justify-content: space-between;
width: 100%;
max-width: 500px;
margin: 1.75rem auto;
padding: 20px 30px;
border-radius: 5px;
color: #000;
background-color: #fff;
transform: translate(0, 0);
transition: all 0.3s ease;
box-sizing: border-box;
}
&-header {
padding-bottom: 16px;
font-size: 25px;
text-align: center;
}
&-footer {
display: flex;
flex-direction: column;
justify-content: center;
height: 80px;
text-align: center;
}
}
</style>
Закрытие модального окна
Теперь необходимо создать функционал для закрытия модального окна. В компоненте модального окна создаем метод closeModal
, в нем с помощью emit
функции триггерим родительский компонент, который слушает close
слушатель:
<script>
export default {
name: 'Modal',
methods: {
closeModal() {
this.$emit('close');
},
},
};
</script>
В родительском компоненте, вешаем слушатель close
, который будет переключать состояние модального окна с помощью метода toggleModal
:
<template>
<div class="home">
<Modal
v-show="isShowModal"
@close="toggleModal"
/>
<button @click="toggleModal">
Open Modal
</button>
</div>
</template>
<script>
import Modal from '@/components/app/Modals/Modal';
export default {
components: {
Modal,
},
data: () => ({
isShowModal: false,
}),
methods: {
toggleModal() {
this.isShowModal = !this.isShowModal;
},
},
};
</script>
Теперь в компоненте модального окна нужно повесить события закрытия модального окна на background и кнопку Close
. Однако теперь, если кликнуть внутри модального окна, оно закроется. Происходит это, потому что метод закрытия модального окна, который мы повесили на background срабатывает и на дочерних элементах. Мы можем это исправить, повесив директиву @click.stop
на контент самого модального окна:
<template>
<div class="simple-modal">
<div
class="simple-modal-backdrop"
@click="closeModal"
>
<div class="simple-modal-container">
<div
class="simple-modal-content"
@click.stop
>
<header class="simple-modal-header">
Modal title
</header>
<section class="simple-modal-body">
Modal body
</section>
<footer class="simple-modal-footer">
<button
type="button"
@click="closeModal"
>
Close
</button>
</footer>
</div>
</div>
</div>
</div>
</template>
Закрытие модального окна при нажатии клавиши Esc
Для решения этой задачи, в компоненте модального окна нам нужно будет добавить еще один метод, а так же создать и уничтожить слушатель на событие keydown
. В методе нужно будет сделать две проверки, на то что нажата именно клавиша Esc
и что состояние модального окна равно true
, иначе даже при закрытом модальном окне, при нажатии на Esc
будет переключаться его состояние:
<script>
export default {
name: 'PAModal',
props: {
show: {
type: Boolean,
default: false,
},
},
mounted() {
window.addEventListener('keydown', this.escCloseModal);
},
destroy() {
window.removeEventListener('keydown', this.escCloseModal);
},
methods: {
closeModal() {
this.$emit('close');
},
escCloseModal(e) {
if (this.show && e.key === 'Escape') {
this.closeModal();
}
},
},
};
</script>
Как видите, мы добавили первый props, который будет передавать в компонент его текущее состояние для того, чтобы проходила проверка в методе escCloseModal
. В родительском компоненте прокинем в props состояние модального окна:
<Modal
v-show="isShowModal"
:show="isShowModal"
@close="toggleModal"
/>
Использование слотов для рендера контента
Для вставки html разметки или любого контента в модальное окно, мы будем использовать слоты. Для этого немного модифицируем компонент модального окна и пропишем 3 слота для header
, body
и footer
:
<template>
<div class="simple-modal">
<div
class="simple-modal-backdrop"
@click="closeModal"
>
<div class="simple-modal-container">
<div
class="simple-modal-content"
@click.stop
>
<header class="simple-modal-header">
<slot name="header">
Modal title
</slot>
</header>
<section class="simple-modal-body">
<slot name="body">
Modal body
</slot>
</section>
<footer class="simple-modal-footer">
<slot name="footer" />
<button
type="button"
@click="closeModal"
>
Close
</button>
</footer>
</div>
</div>
</div>
</div>
</template>
В слотах указано содержимое по умолчанию. Давайте в родительском компоненте пропишем уникальное содержимое:
<template>
<div class="home">
<Modal
v-show="isShowModal"
:show="isShowModal"
@close="toggleModal"
>
<template #header>
Наши правила по работе
</template>
<template #body>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
quos rem, rerum sapiente sed temporibus velit.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
quos rem, rerum sapiente sed temporibus velit.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
quos rem, rerum sapiente sed temporibus velit.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
quos rem, rerum sapiente sed temporibus velit.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
quos rem, rerum sapiente sed temporibus velit.
</p>
</template>
</Modal>
<button @click="toggleModal">
Open Modal
</button>
</div>
</template>
Добавление прокрутки для контента
Если содержимое контента модального окна достаточно большое, модальное окно некрасиво растянется. Чтобы этого избежать, нам нужно предусмотреть возможность добавления прокрутки к контенту модального окна. Сделаем это следующим образом:
Добавляем props:
scrollable: {
type: Boolean,
default: false,
},
Новый css селектор:
&-scrollable {
height: 500px;
overflow-y: scroll;
}
В шаблонах модального окна добавляем байнд класса:
<div
class="simple-modal-content"
:class="{ 'simple-modal-scrollable': scrollable }"
@click.stop
>
Теперь переключая props в родительском компоненте, можно добавить к модальному окну прокрутку.
Анимация модального окна
Теперь нам нужно сделать плавную анимацию появления и скрытия модального окна. Обрамляем всю разметку в тег transition с названием стилей анимации:
<template>
<transition name="simple-modal">
<div class="simple-modal">
<div
class="simple-modal-backdrop"
@click="closeModal"
>
<div class="simple-modal-container">
<div
class="simple-modal-content"
:class="{ 'simple-modal-scrollable': scrollable }"
@click.stop
>
<header class="simple-modal-header">
<slot name="header">
Modal title
</slot>
</header>
<section class="simple-modal-body">
<slot name="body">
Modal body
</slot>
</section>
<footer class="simple-modal-footer">
<slot name="footer" />
<button
type="button"
@click="closeModal"
>
Close
</button>
</footer>
</div>
</div>
</div>
</div>
</transition>
</template>
И пишем саму анимацию на чистом css:
.simple-modal-enter,
.simple-modal-leave-to {
opacity: 0;
}
.simple-modal-enter-active,
.simple-modal-leave-active {
transition: opacity 0.2s ease;
}
Добавление accessibility c ARIA attributes
Добавление role = "dialog"
поможет идентифицировать компонент модального окна как диалог приложения, отделенный от остальной части пользовательского интерфейса.
Нам нужно правильно разметить модальное окно с помощью атрибутов aria-labelledby
и aria-describeby
, а так же добавить атрибут aria-label
к кнопке закрытия модального окна. Но сделаем мы это через props, чтобы указывать уникальные идентификаторы для атрибутов:
headerId: {
type: String,
required: true,
default: null,
},
bodyId: {
type: String,
required: true,
default: null,
},
Ну и раскидать props в разметке модального окна:
<template>
<transition name="simple-modal">
<div class="simple-modal">
<div
class="simple-modal-backdrop"
@click="closeModal"
>
<div class="simple-modal-container">
<div
class="simple-modal-content"
:class="{ 'simple-modal-scrollable': scrollable }"
role="dialog"
:aria-labelledby="headerId"
:aria-describedby="bodyId"
@click.stop
>
<header
:id="headerId"
class="simple-modal-header"
>
<slot
:id="bodyId"
name="header"
>
Modal title
</slot>
</header>
<section class="simple-modal-body">
<slot name="body">
Modal body
</slot>
</section>
<footer class="simple-modal-footer">
<slot name="footer" />
<button
type="button"
@click="closeModal"
>
Close
</button>
</footer>
</div>
</div>
</div>
</div>
</transition>
</template>
В родительском компоненте допишем props:
<template>
<div class="home">
<Modal
v-show="isShowModal"
:show="isShowModal"
:scrollable="true"
header-id="modalHeader"
body-id="modalBody"
@close="toggleModal"
>
<template #header>
Наши правила по работе
</template>
<template #body>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
quos rem, rerum sapiente sed temporibus velit.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
quos rem, rerum sapiente sed temporibus velit.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
quos rem, rerum sapiente sed temporibus velit.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
quos rem, rerum sapiente sed temporibus velit.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
quos rem, rerum sapiente sed temporibus velit.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
quos rem, rerum sapiente sed temporibus velit.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
quos rem, rerum sapiente sed temporibus velit.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
quos rem, rerum sapiente sed temporibus velit.
</p>
</template>
</Modal>
<button @click="toggleModal">
Open Modal
</button>
</div>
</template>
Гибкая стилизация модального окна
Теперь реализуем возможность менять оформление модального окна. Для этого создадим новый props:
modalClass: {
type: String,
default: 'simple-modal',
},
В разметке заменим заглавный класс модалки:
<template>
<transition :name="modalClass">
<div :class="modalClass">
<div
:class="`${modalClass}-backdrop`"
@click="closeModal"
>
<div :class="[{'simple-modal-scrollable': scrollable}, `${modalClass}-container`]">
<div
:class="`${modalClass}-content`"
role="dialog"
:aria-labelledby="headerId"
:aria-describedby="bodyId"
@click.stop
>
<header
:id="headerId"
:class="`${modalClass}-header`"
>
<slot
:id="bodyId"
name="header"
>
Modal title
</slot>
</header>
<section :class="`${modalClass}-body`">
<slot name="body">
Modal body
</slot>
</section>
<footer :class="`${modalClass}-footer`">
<slot name="footer" />
<button
type="button"
@click="closeModal"
>
Close
</button>
</footer>
</div>
</div>
</div>
</div>
</transition>
</template>
Теперь мы можем задать класс для модалки, описать его стили и использовать один компонент для разного оформления модальных окон.
Финальный вариант
Компонент модального окна:
<template>
<transition :name="modalClass">
<div :class="modalClass">
<div
:class="`${modalClass}-backdrop`"
@click="closeModal"
>
<div :class="[{'simple-modal-scrollable': scrollable}, `${modalClass}-container`]">
<div
:class="`${modalClass}-content`"
role="dialog"
:aria-labelledby="headerId"
:aria-describedby="bodyId"
@click.stop
>
<header
:id="headerId"
:class="`${modalClass}-header`"
>
<slot
:id="bodyId"
name="header"
>
Modal title
</slot>
</header>
<section :class="`${modalClass}-body`">
<slot name="body">
Modal body
</slot>
</section>
<footer :class="`${modalClass}-footer`">
<slot name="footer" />
<button
type="button"
@click="closeModal"
>
Close
</button>
</footer>
</div>
</div>
</div>
</div>
</transition>
</template>
<script>
export default {
name: 'PAModal',
props: {
show: {
type: Boolean,
default: false,
},
scrollable: {
type: Boolean,
default: false,
},
headerId: {
type: String,
required: true,
default: null,
},
bodyId: {
type: String,
required: true,
default: null,
},
modalClass: {
type: String,
default: 'simple-modal',
},
},
mounted() {
window.addEventListener('keydown', this.escCloseModal);
},
destroy() {
window.removeEventListener('keydown', this.escCloseModal);
},
methods: {
closeModal() {
this.$emit('close');
},
escCloseModal(e) {
if (this.show && e.key === 'Escape') {
this.closeModal();
}
},
},
};
</script>
<style lang="scss">
.simple-modal {
&-backdrop {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
transition: opacity 0.3s ease;
z-index: 9999;
}
&-container {
position: fixed;
top: 0;
right: 0;
left: 0;
bottom: 0;
width: auto;
margin: 16px;
}
&-scrollable {
overflow-x: hidden;
overflow-y: auto;
}
&-content {
display: flex;
flex-direction: column;
justify-content: space-between;
width: 100%;
max-width: 500px;
margin: 1.75rem auto;
padding: 20px 30px;
border-radius: 5px;
color: #000;
background-color: #fff;
box-sizing: border-box;
transform: translate(0, 0);
transition: all 0.3s ease;
}
&-header {
padding-bottom: 16px;
font-size: 25px;
text-align: center;
}
&-footer {
display: flex;
flex-direction: column;
justify-content: center;
height: 80px;
text-align: center;
}
&-enter,
&-leave-to {
opacity: 0;
}
&-enter-active,
&-leave-active {
transition: opacity 0.2s ease;
}
}
</style>
Вызов компонента модального окна в родительском компоненте:
<template>
<div class="home">
<Modal
v-show="isShowModal2"
:show="isShowModal2"
:scrollable="true"
header-id="modalHeader"
body-id="modalBody"
@close="toggleModal"
>
<template #header>
Наши правила по работе
</template>
<template #body>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
quos rem, rerum sapiente sed temporibus velit.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
quos rem, rerum sapiente sed temporibus velit.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
quos rem, rerum sapiente sed temporibus velit.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
quos rem, rerum sapiente sed temporibus velit.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
quos rem, rerum sapiente sed temporibus velit.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
quos rem, rerum sapiente sed temporibus velit.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
quos rem, rerum sapiente sed temporibus velit.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
quos rem, rerum sapiente sed temporibus velit.
</p>
</template>
</Modal>
<button @click="toggleModal">
Open Modal
</button>
</div>
</template>
<script>
import Modal from '@/components/app/Modals/Modal';
export default {
components: {
Modal,
},
data: () => ({
isShowModal: false,
}),
methods: {
toggleModal() {
this.isShowModal = !this.isShowModal;
},
},
};
</script>
Вот и все. Получился простой, но довольно гибкий компонент модального окна.
Время запросов: 0,1240 s
Количество запросов: 28
Источник: cache