Переходим к третьей части нашей инструкции по созданию собственного блога. Напоминаю, что мы пишем на 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, потому что по умолчанию этот фреймворк стирает все существующие стили (чтобы вы могли выбрать свой дизайн, предварительно не занимаясь удалением уродских иконок и рамок).

Сразу оговорюсь по поводу дизайна. Это вещь сугубо индивидуальная и не каждому понравится тот стиль, что выбрал я. Поэкспериментируйте на сайте Tailwind Play и подберите собственный уникальный стиль. Это несложно.
А теперь немного про то, что натворил я.
- <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 равняется true или false, я поясню. Любое положительное значение в JavaScript считается true. А отрицательные (undefined, null) автоматически признаются false.
Теперь сделаем кнопку, которая будет менять значение 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. Не забудьте сделать коммит!