Переходим к третьей части нашей инструкции по созданию собственного блога. Напоминаю, что мы пишем на Svelte с TailwindCSS, а в качестве базы данных используем SQLite.
Предыдущие «серии»:
Мы уже создали интерфейс для отображения статей в базовом формате. Теперь нам нужно научиться добавлять такие же базовые посты прямо из приложения, а не редактируя базу данных вручную, как мы делали до этого.
Создаем компонент для написания статьи и отправки ее на сайт
Для начала нам нужен отдельный компонент Svelte, в который мы поместим формы для названия статьи и самого текста (пока обойдемся без картинок, пользователей и тегов). В этот компонент поместим непосредственно поля для текста и кнопку отправки, которая будет запускать функцию для передачи данных в нужную таблицу SQLite.
-
Создаем новый файл в директории routes с названием NewPost.svelte.
-
Создаем внутри блок <script> и записываем в него две переменные:
let postName
и
let postBody
-
Затем формируем форму:
Разбираем построчно.
-
<form> – это тег, позволяющий создать поле, которое по умолчанию предназначено для передачи данных в какие-то внешние инстанции. Мы добавляем к нему атрибут on:submit (это Svelte-атрибут для отслеживания действия submit). Также мы через прямую строку пишем preventDefault, чтобы заблокировать все действия формы по умолчанию (чтобы не происходила перезагрузка страницы). И указываем функцию, которая будет запускаться вместо действий по умолчанию. В нашем случае она называется addPost (позже мы ее задекларируем).
-
<label> – это название для нашего поля. Либо название статьи, либо ее текст.
-
<input type="text" name="name" bind:value={postName} /> – это просто блок с полем для ввода текста. Здесь из интересного только атрибут bind:value. Его идея заключается в том, чтобы «привязать» значение value выбранного поля ввода к уже существующей переменной, чтобы значение переменной postName всегда равнялось значению инпута name. Или значение postBody всегда равнялось значению инпута body.
-
<input type="submit" value="Добавить статью" /> – думаю, тут и без лишних пояснений все понятно. Это кнопка, которая отвечает за добавление статей. В нашем случае эта кнопка будет провоцировать действие on:submit, прописанное в списке атрибутов формы.
Форма готова. Пока что она максимально уродлива, но мы это исправим чуть позже.
Создаем функцию для передачи данных о статье в новую точку доступа
Теперь нужна как раз та самая функция addPost для добавления новых статей.
- Сначала создаем функцию addPost. Она должна быть асинхронной.
-
В ее теле создаем константу с названием dataToAdd. Это информация о запросе, который мы будем передавать в точку доступа. А fetch('/api/addPost' – это как раз название точки доступа. Заметьте, что перед fetch написано await, а это значит, что действие выполняется асинхронно, дожидаясь ответа от точки доступа и базы данных.
-
Сразу после пути к точке доступа через запятую мы ставим фигурные скобки, чтобы создать новый объект с информацией о fetch-запросе. Во-первых, указываем тип fetch. Нам нужен POST-метод, поэтому пишем method: 'POST'. Затем ставим запятую и вторым элементом объекта указываем body: JSON.stringify({postName, postBody}). Функция JSON.stringify нужна, чтобы JSON-объект превратить в строку, а остальное – это переменные с информацией из инпутов.
Готово. Теперь у нас есть функция, которая делает запрос данных. Почти.
Создаем функцию для передачи информации о новой статье в базу данных
Перед тем как продолжить, создадим новую точку доступа с названием addPost.js и добавим ее в директорию api, где лежит наш предыдущий endpoint, отвечающий за загрузку статей на сайт.
Внутри него указываем следующее:
- Сначала импортируем базу данных из $lib, как уже делали это ранее.
-
Создаем функцию post для передачи данных и в качестве ее аргумента передаем request (это как раз те данные, что мы цепляем из компонента NewPost.svelte).
-
Далее создаем константу data и записываем туда наши данные, предварительно превратив их из строки в объект:
JSON.parse(request.body)
-
Потом создаем еще две константы с содержимым полученного объекта. Нам нужны наши postName и postBody.
-
После этого подготавливаем базу данных для работы с новыми данными. Мы последовательно указываем, что нам нужно вставить (INSERT INTO)-данные (title и body) в таблицу Posts, а их новые значения должны быть переданы в качестве аргументов, поэтому пока вместо них указаны вопросительные знаки. Учтите, что здесь важную роль теперь играет непосредственно порядок передачи аргументов.
- Создаем константу sendDbData, через которую будет осуществлен запуск функции run поверх уже подготовленной ранее информации c подменой аргументов:
dbData.run(postName, postBody)
- postName займет место первого вопросительного знака, а postBody – второго. Также нужно сделать пустой возврат, чтобы наша точка доступа функционировала:
return {body: {}}
Готово. Теперь у нас есть все функции и элементы интерфейса, необходимые для добавления на сайт новых статей. При заполнении полей и нажатии на «Добавить статью» в базу данных автоматически попадет информация с заполненных полей. Все просто.
Стилизуем форму
Сейчас форма выглядит неудобной и нелогичной. Непонятно, какой элемент где заканчивается и на что из этого можно нажать. Так происходит отчасти из-за TailwindCSS, потому что по умолчанию этот фреймворк стирает все существующие стили (чтобы вы могли выбрать свой дизайн, предварительно не занимаясь удалением уродских иконок и рамок).
А теперь немного про то, что натворил я.
-
<form> теперь отображается как flex-box с flex-direction: column. То есть это вертикально расположенные блоки. Также добавил небольшой margin, чтобы «оторвать» форму от краев экрана.
-
Инпутам я добавил рамку, чтобы их легче было идентифицировать. А также сделал так, чтобы они растягивались на всю длину блока. За такое поведение отвечает свойство w-full. А еще я заменил второй инпут на textarea, чтобы можно было делать отступы в тексте.
-
Ну и кнопка окрасилась в синий цвет, параллельно добавив другой дизайн курсора, чтобы было понятно, что это кнопка и на нее можно нажать.
Результат:
Решаем возникшие проблемы
По ходу создания предыдущего компонента и добавления новой функциональности у нас возникло две сложности:
-
Чтобы информация о новой статье появилась на странице, надо ее перезагрузить.
-
Поля находятся постоянно на виду и занимают слишком много полезного пространства.
Обе эти проблемы можно быстро решить парой строчек кода и сделать страницу намного симпатичнее, а ее поведение – логичнее.
Обновляем данные на странице
Поведение нашей страницы в текущий момент очевидно. Подгрузка данных из БД происходит лишь при загрузке страницы, поэтому даже после обновления наш сайт остается прежним до тех пор, пока мы не нажмем Ctrl+R или не зайдем на страницу повторно. Но с точки зрения пользователя такое поведение нелогично и нам нужно его исправить. Есть много способов, но мы выберем простую хитрость, которая вдобавок ко всему покажет, как в Svelte работает функция dispatch.
- Сначала нужно добавить в наш блок скрипт импорт пакета createEventDispatcher прямо из Svelte:
import { createEventDispatcher } from 'svelte
- А потом создать константу dispatch, которая будет непосредственно отвечать на запуск функции createEventDispatcher:
const dispatch = createEventDispatcher()
- Теперь у нас есть доступ к этой функции, но воспользуемся мы ею чуть позже. Для начала нужно скорректировать содержимое addPost.js:
Мы изменили тут две вещи: переименовали переменные postName и postBody в title и body соответственно, а также изменили названия переменных в функции dbData.run().
Также в return добавили переименованные переменные. Это необходимо для импорта новых объектов с обновленным данными в компонент Post, который используется для рендеринга постов на странице.
Теперь сделаем свой диспетчер. Сама функция необходима, чтобы передавать данные из одного компонента в другой, т.е. из компонента-наследника компоненту-родителю. Наша задача сейчас передать родителю (index.svelte) информацию о том, что мы добавили новую статью и объект с этой статьей. А родительский компонент отреагирует на эти изменения и отрисует новый пост на главной странице нашего блога.
- Мы добавили в нашу функцию addPost переменную reposponseOfDataToAdd, чтобы хранить ответ от промиса (асинхронного запроса) в addPost.js:
const responseOfDataToAdd = await dataToAdd.json()
- Этот ответ мы и будем передавать родителю. Создаем новый диспетчер, помечаем в нем событие как newPost и через запятую прописываем передаваемый контент:
dispatch('newPost', {reponse: responseOfDataToAdd})
- А вот сейчас можно вернуться в родительский компонент и там обработать диспетчинг.
- Добавляем реакцию на изменения в NewPost (то есть появление диспетчера), добавив наше кастомное событие newPost, и привяжем к этому событию запуск функции reloadPage:
on:newPost={reloadPage}
- А функция reload.page выглядит вот так просто:
const reloadPage = (event) => {}
- Мы передаем в качестве аргумента событие, которое произошло в компоненте-наследнике. А событие, в свою очередь, несет в себе все данные, что были прописаны в функции dispatch:
data.posts.push(event.detail.response)
- Добавляем в массив с постами объекта новый пост, переданный диспетчером. Заметьте, мы обращаемся к response, указанный нами же в функции dispatch.
data.posts = data.posts
Присвоение переменной с постами самой себя запускает ререндеринг страницы. Срабатывает реактивная сущность Svelte и мы сразу видим изменения на экране.
Прячем инпуты под кнопку
Здесь придется потратить чуть больше времени. Мы будем создавать отдельные классы для двух видов формы, расширять конфигурационный файл TailwindCSS и осваивать директиву class в Svelte.
Трудно выбрать, с чего именно начать, потому что все задачи тесно связаны. Попробуем со стилей.
-
Открываем файл app.css (он отвечает за выгрузку Tailwind-классов в наше приложение).
-
И добавляем в него два новых класса, как вы добавляли бы классы в CSS (если знаете о них).
-
В классе .form пишем @apply, а потом просто копируем мои стили:
@apply absolute flex flex-col w-[90%] h-[360px] m-5 p-5 bg-white shadow-md rounded-lg border animate-expand overflow-hidden
-
В классе .form-hidden пишем:
@apply hidden
Кто-то мог заметить свойство animate-expand в классе .form. В TailwindCSS такого не существует, мы его создадим сами.
- Открываем файл tailwind.config.cjs, который лежит в корне проекта.
-
В тело объекта extend вписываем объект animation, а в него свойства анимации. В нашем случае это длительность анимации и ее тип:
expand: 'expand 0.2s linear',
-
Потом добавляем туда же keyframes, а внутрь keyframes добавляем объект expand (по названию анимации), а внутрь прописываем свойства анимации:
expand: {'0%': { width: '0%', height: '0px', transform: 'translateY(-20px)', filter: 'blur(2px)' }, '50%': { width: '92%', height: '362px' }, '100%': { width: '90%', height: '360px' },},
Анимация готова. Но чтобы она сработала, надо привязать появление анимации к какому-то событию. В коде нашего приложения и так уже указано, что анимация срабатывает на объектах с классом form при их появлении в поле видимости.
Осталось настроить смену тех самых классов. Это делается через директиву class в Svelte. С помощью нее можно менять классы в зависимости от значения переменных, указанных в блоке <script>. В нашем коде директива class для <form> настроена так:
{inputShown ? 'form' : form-hidden'}
Если значение переменной inputShown равно true, то применяется класс form, а если inputShown равен false, то будет применен класс form-hidden.
Теперь сделаем кнопку, которая будет менять значение inputShown, а вместе с ней и отображение формы с полями для добавления статей. Привяжем к ней действие:
on:click={toggleInputVisibility}
И зададим дизайн на свой вкус.
А вот так выглядит содержимое функции toggleInputVisibility:
- Сначала объявляем саму функцию и передаем в нее событие, которое ее вызвало:
const toggleInputVisibility = (event) => {}
- Проводим проверку, существует ли event (передало ли событие команду или какой-то другой сценарий на странице), и если результат будет true, то создаем переменную для кнопки, активировавшей функцию:
if (event) { button = event.target }
-
А теперь создаем switch-структуру, которая будет проверять значение переменной inputShown и менять его при необходимости. Если true, то ставим false – если false, то ставим true.
Единственная проблема, оставшаяся в нашем приложении – форма не исчезает даже при добавлении постов. То есть мы нажимаем на кнопку, а форма остается. Это непредсказуемое поведение, которое нужно исправить.
Поэтому в функцию addPost мы добавим еще и инициализацию функции:
toggleInputVisibility()
Также мы обнулим значения переменных postName и postBody, чтобы при повторной попытке добавить статью они были пустыми, а не хранили тот текст, что мы набирали в предыдущий раз. Для этого достаточно переменные переназначить на пустые строки.
Вместо заключения
На этом все. Вот такая у нас получилась форма для добавления статей. Посты сразу попадают в базу данных и вовремя отображаются на странице. Фактически это уже похоже на блог.
В следующей статье мы будем разбирать удаление статей и деплой сего чуда в Netlify и на серверы Timeweb. А после этого снова продолжим модифицировать наше приложение. Добавим туда что-то в духе тегов или категорий, научимся подписывать авторов, создадим отдельные страницы для статей и подключим сторонний markdown-редактор с пагинатором. В общем, впереди еще куча работы.
P.S. Не забудьте сделать коммит!
Читатайте далее: Блог на Svelte.Часть 4: Деплой
Комментарии
необходимо немного подкорректировать код в файле addPost.js
Вместо
export const post = (request) => {
const data = JSON.parse(request.body);
необходимо деструктурировать request и использовать метод .json() А поскольку он возвращает Promise то добавить асинхронность:
export const post = async ({request}) => {
const data = await request.json();