Создание приложения

В этой статье мы рассмотрим шаги создания упрощённого клона Instagram с возможностью добавлять посты, фильтры фото и лентой постов, все как в настоящем Instagram!** Вот, что должно получиться в конце:

В приложении можно загрузить изображение, кликнув по иконке “квадрат” в футере. После загрузки изображения его можно отредактировать, используя разные фильтры, а также можно разделять текст на блоки.

В этом уроке рассмотрим созданиe пользовательского интерфейса приложения с помощью Vue.js, сосредоточимся на настройке интерфейса UI, никаких взаимодействий с сервером не планируется, будем редатрировать только фронтенд.

Контекст

Предполагаю, что вы немного знакомы с Vue, если что-то непонятно, то разберетесь в процессе написания кода.

В этом уроке мы поговорим о следующем:

  • Развертывание проекта с помощью vue-cli.
  • Создание компонентов в формате одного файла single-file.
  • Обмен данными и событиями между компонентами.
  • Загрузка файлов с помощью API FileReader.
  • Редактирование изображений с помощью Instagram-подобных фильтров с использованием библиотеки CSSGram (Автор @Una).
  • Перетаскивание прокрутки элементов с помощью библиотеки vue-dragscroll (Автор @don_jon243).

Чтобы сосредоточиться на использовании Vue, мы не будем разбирать стилизацию приложения, просто добавим необходимые стили и вызовем их.

Блоки кода

В процессе создания приложения я буду показывать изменения кода и окончательные файлы. Для показа целых файлов я буду использовать Github gist`ы, когда нужно будет показать какие-то конкретные изменения, то я буду показывать их кусочками файлов:

`

Добавляем этот текст всередине элемента!

` ## Подготовка приложения Отправной точкой нашего приложения будет использование интерфейса командной строки Vue ([vue-cli](https://github.com/vuejs/vue-cli)). Vue-cli - это инструмент созданный командой Vue, который помогает упростить и ускорить создание и разработку приложений Vue. Инструмент объединит наше приложение с менеджером пакетов Webpack, который в свою очередь позволяет создавать компоненты в формате [одного файла - Single-file](https://vuejs.org/v2/guide/single-file-components.html).

Для этого урока мы создадим с помощью vue-cli заготовку в CodeSandBox, редактор онлайн-кода, ориентированный на прототипирование и развертывание веб-приложений.

Для старта используем некоторый исходный шаблон приложения.

Давайте кратко рассмотрим структуру исходного каталога проекта.

data/ filters.js posts.js styles/ app.scss filter-type.scss instagram-post.scss App.vue index.html index.js package.json

Примечание: Редактор CodeSandbox включает файлы конфигурации, необходимые для настройки нашего приложения. Это позволит нам просто сосредоточиться на коде приложения.

data/

Папка data/ содержит данные в приложении, в двух отдельных файлах: filters.js и posts.js.

Файл filters.js ссылается на тип фильтров, который может быть применен к загруженному изображению:

export default [ { name: "normal" }, { name: "clarendon" }, { name: "gingham" }, { name: "moon" }, { name: "lark" }, { name: "reyes" }, { name: "juno" }, { name: "slumber" }, { name: "aden" }, { name: "perpetua" }, { name: "mayfair" }, { name: "rise" }, { name: "hudson" }, { name: "valencia" }, { name: "xpro2" }, { name: "willow" }, { name: "lofi" }, { name: "inkwell" }, { name: "nashville" } ]; posts.js представляет собой набор объектов данных, которые представляют сообщения, которые уже были отправлены в ленту. Если наше приложение сохраняло данные на сервере, мы, вероятно, сделаем запрос GET на сервер для получения подобной информации:

export default [ { username: "socleansofreshh", userImage: "https://s3-us-west-2.amazonaws.com/s.cdpn.io/1211695/me_3.jpg", postImage: "https://s3-us-west-2.amazonaws.com/s.cdpn.io/1211695/tropical_beach.jpg", likes: 36, hasBeenLiked: false, caption: "When you're ready for summer '18 ☀️", filter: "perpetua" }, { username: "djirdehh", userImage: "https://s3-us-west-2.amazonaws.com/s.cdpn.io/1211695/me2.png", postImage: "https://s3-us-west-2.amazonaws.com/s.cdpn.io/1211695/downtown.jpg", likes: 20, hasBeenLiked: false, caption: "Views from the six...", filter: "clarendon" }, { username: "puppers", userImage: "https://s3-us-west-2.amazonaws.com/s.cdpn.io/1211695/pug_personal.jpg", postImage: "https://s3-us-west-2.amazonaws.com/s.cdpn.io/1211695/puppers.jpg", likes: 49, hasBeenLiked: false, caption: "Current mood", filter: "lofi" } ]; Каждый объект post содержит свойства, относящиеся к нему, такие как имя, изображение автора поста, загруженное изображение, кол-во лайков и т.д. Кроме того, каждый объект данных будет содержать свойство filter, которое будет определять применение фильтра к изображению поста.

styles/

Папка styles/ содержит все кастомные CSS, необходимые для нашего приложения. Когда мы будем создавать компоненты, мы будем привязывать правильные стили к компоненту и сфокусируемся на использовании Vue.

App.vue

Папка App.vue - это основной родительский компонент, который должен быть отображен из нашего экземпляра Vue. Если мы откроем файл App.vue, мы увидим простой однофайловый компонент:

`

` Однофайловые компоненты - это невероятно полезная фича, которая позволяет нам определять все HTML/CSS и JS компоненты в одном vue** файле.

В нашем файле App.vue компонент ** в данный момент просто отображает простое приветственное сообщение. **

index.html

Файл index.html является корневой страницей разметки нашего приложения:

`

` В *index.html* мы указываем внешние стили для нашего приложения. Мы добавили [Bulma](https://bulma.io/documentation/) в качестве CSS фреймворка, [Font Awesome](https://fontawesome.com/) для иконок и [CSSGram](https://una.im/CSSgram/) для реализации фильтров Instagram.

Элемент div с id приложения является элементом DOM, на который будет примонтировано приложение Vue, как это задано в файле index.js.

index.js

Файл index.js* представляет собой отправную точку Vue приложения:

`import Vue from “vue”; import App from “./App”;

/* eslint-disable no-new */ new Vue({ el: “#app”, render: h => h(App) }); ` В верхней части файла index.js мы импортируем библиотеку Vue и компонент App. Затем мы создаем новый экземпляр Vue, объявляя его через new Vue ({…}). Экземпляр Vue является отправной точкой для всех приложений Vue и принимает объект параметров options, содержащий детали экземпляра.

В приведенном выше экземпляре мы указываем элемент DOM с идентификатором приложения, в котором должно быть установлено наше приложение Vue, и объявляем, что компонент App является самым верхним родительским компонентом, который должен быть отрендерен.

Теперь, когда у нас есть представление о том, как реализована структура нашего приложения, мы можем начать создавать приложение.

1) Домашняя страница

Первое, что мы начнем делать - это создавать ленту постов Главной страницы, создав основной компонент приложения App и привязав данные из файла data/posts.js.

На высоком уровне мы можем разбить компоновку будущего приложения на три раздела: шапку (phone-header), тело (phone-body) и подвал (phone-footer).

Большинство функциональных возможностей приложения будут находится в секции phone-body; поэтому было бы целесообразно создать отдельный компонент, который представлял бы этот раздел. Мы создадим этот компонент как PhoneBody.vue в папке components/:

components/ PhoneBody.vue data/ styles/ ... В файле PhoneBody.vue мы создадим простой однофайловый компонент:

`

` В теге script файла App.vue мы импортируем компонент PhoneBody и определим его свойства в components

`

` Мы сопоставили объявление phone-body с объектом компонента PhoneBody. Это позволит нам объявить недавно импортированный компонент в качестве phone-body в шаблоне приложения. Мы сделаем это и, кроме того, создадим элементы, которые представляют разделы шапки (phone-header) и подвала (phone-footer). Наш файл App.vue станет таким:

`

` Мы разместили изображение лого Instagram в phone-header; и иконки Главная и загрузить в phone-footer. На данный момент наше приложение будет выглядеть так:

Прежде чем мы сможем заполнять контент в разделе phone-body, давайте оценим данные, представляющие приложение.

Поскольку наше приложение довольно простое, то мы не будем использовать средства управления состоянием, вместо этого будем направлять все данные в самый верхний компонент приложения для родителей. Детальные компоненты будут более презентабельными и просто будут иметь свои данные в виде параметра, переданного из приложения.

Для начала мы будем импортировать сообщения и фильтровать массивы данных и объявлять их как свойства данных с тем же именем в функции data() компонента App.

`

` Затем мы можем передавать массивы в posts и filters как параметры, через которые мы объявляем компонент phone-body. Мы будем использовать сокращенный синтаксис для директив v-bind:

` ` С этими изменениями весь файл App.vue будет выглядеть так:

`

` С массивом posts, доступным в виде параметров в компоненте PhoneBody, мы теперь можем отрендерить список элементов, которые являют собою отправленные посты. Отрендерим список с помощью директивыо Vue v-for.

Так как каждый пост содержит много разметки мы создадим компонент VuegramPost, который будет использоваться директивой v-for для построения списка. В папке components/ мы создадим файл компонента VuegramPost.vue:

components/ VuegramPost.vue PhoneBody.vue data/ styles/ Компонент VuegramPost будет шеллом, который выводит свойства объекта единичного объекта post:

Продолжим и создадим компонент VuegramPost в файле VuegramPost.vue:

`

` Тут есть несколько моментов, на которые стоит обратить внимание:

  • В script мы заявили, что компонент VuegramPost ожидает параметры объекта post, как показано в требовании проверки параметров (props: {post: Object}).
  • Мы связываем параметры объекта post c компонентом шаблона используя синтаксис Mustache: ( {{ }} ) и директиву :v-bind.
  • Мы указываем, что стили компонента должны быть получены из стилей vuegram-post.scss.

Теперь нам нужно использовать директиву v-for для рендеринга списка VuegramPost компонентов на основе коллекции данных posts. В файле PhoneBody.vue мы сначала объявим posts и filters параметры, которые передаются нам, чтобы мы могли использовать их в компоненте PhoneBody. Мы сделаем это, указав требование проверки параметра - они оба posts и filters должны быть массивами:

`

` Мы также импортируем компонент VuegramPost и объявим его в свойстве components компонента PhoneBody:

`

` Наконец, мы отобразим список постов в шаблоне и файл PhoneBody.vue обновится до следующего вида:

`

` Поскольку мы итерируем по коллекции posts, то post - это отдельный пост в директиве v-for. В каждый отрендеренный vuegram-post мы также передаем итерированный объект post и его параметры могут быть доступны в компоненте.

Теперь мы можем прокручивать ленту постов в нашем приложении!

Перед продолжением, давайте разберемся какой функционал “лайков” нужно будет добавить для постов Instagram. В реальном Instagram мы можем добавить в избранное щелкнув по иконке сердца под изображением поста или дважды щелкнув это изображение.

У нас есть два параметра в объекте post, связанных с функциональностью “лайков”, которые мы собираемся реализовать:

{ ..., likes: 36, hasBeenLiked: false, ..., }, likes - это число, которое мы будем увеличивать на один, когда пользователь «лайкает» пост. Поскольку мы не будем позволять пользователю постоянно увеличивать количество лайков к одному и тому же посту, hasBeenLiked будет той переменной логического типа, которая будет показывать лайкнут ли пост.

В script компонента VuegramPost мы сначала создадим метод like() в свойстве methods(), который будет “лайкать” или отменять “лайк”:

` ` В методе *like()* мы используем [тернарный оператор](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator), чтобы условно увеличить или уменьшить *post.likes* значение, основанное на значении *post.hasBeenLiked*. Затем мы переключаем значение переменной логического типа *post.hasBeenLiked*.

В шаблоне теперь мы можем добавить обработчики событий на элементах, которые пользователь будет нажимать, чтобы лайкнуть пост. Мы будем использовать сокращенный синтаксис для директивы v-on и укажем обработчик клика dblclick на изображении поста и обработчик клика click на иконке сердца.

` ` Мы также применили привязку условного класса к иконке сердца, чтобы условно добавить класс *.fas*, если логическая переменная *post.hasBeenLiked* истинна. Этот класс заполняет иконку сердца красным цветом, показывая, что пост понравился пользователю.

С этими изменениями файл VuegramPost.vue будет иметь следующий вид:

`

` Теперь, когда пользователь либо щелкает иконку сердца, либо дважды кликает на изображении, он/она «лайкнет» сообщение:

Ниже приведен пример нашего приложения на этом этапе!

2) Процесс отправки

Когда пользователь начинает процесс отправки, мы хотим изменить UI (пользовательский интерфейс) в зависимости от того, где находится пользователь (например, если пользователь находится на втором этапе, ему/ей должно быть предложено выбрать фильтр, который должен быть применен к изображению). В приложении из реального мира мы, вероятно, захотели бы использовать соответствующую библиотеку маршрутизации, такую как Vue Router, или, по крайней мере, построить настраиваемое решение маршрутизации.

Однако, для нашего приложения мы сделаем что-то гораздо проще. Мы изменим UI приложения на основе параметра step. Когда пользователь находится на step === 1, ему/ей будет показан лента новостей. На step === 2 будет возможность выбрать фильтр, и на этапе step === 3 пользователю будет предложено предоставить описание поста.

Элементы, отображаемые в заголовке (например, ссылки «Cancel» и «Next»), также будут условно отображаться на основе значения step.

В родительском компоненте App давайте сначала представим step свойство и установим его значение равным 1. Поскольку мы будем использовать свойство step, чтобы определять, как будут отображаться элементы в секции phone-body, мы передадим его как параметры для компонента phone-body.

` ` Когда пользователь проходит процесс отправки; мы хотим, чтобы конечный результат включал отправку нового поста на ленту домашней страницы. Иными словами, мы хотим, чтобы пользователь мог пушнуть (т.е. представить) новый пост объект в коллекцию сообщений (т.е. в файл *posts.js*). Мы будем управлять свойствами *username* и *userImage*, но нам нужно будет полчуать остальное от пользователя.

В общем, нам нужно захватить выбранный фильтр selectedFilter, который хочет применить пользователь, изображение image, которое они хотели бы загрузить, и подпись к сообщению caption. Учитывая эти свойства, давайте объявим для них пустые начальные значения в компоненте App и передадим их как параметры в phone-body:

` ` Обратите внимание на то, как мы используем директиву [v-model](https://vuejs.org/v2/guide/forms.html) для привязки значения параметра *caption*. Это связано с тем, что мы хотим *избежать* прямой мутации параметра *caption* родительского элемента из дочернего компонента. Объясню это чуть подробнее в конце статьи.

При инициализации основных свойств мы создадим метод, ответственный за направление пользователя с шага 1 на шаг 2. Одним из действий, ответственных за это, будет загрузка изображения по клику на иконку загрузки в футере (phone-footer):

В App.vue мы создадим новый метод uploadImage(), который будет нести ответственность за загрузку изображения и затем направлять пользователя на шаг 2.

Элемент input type = “file” позволяет пользователям выбирать один или несколько файлов с их устройства для загрузки. Поэтому в разделе футере шаблона App.vue мы создадим элемент ввода (type=“file”), в котором есть слушатель событий, который вызывает метод uploadImage() при срабатывании:

` ` Поскольку мы хотим, чтобы иконка загрузки являлась действием, связанным с загрузкой изображения, мы установим *visibility: hidden* для класса, применяемого *input*. Кроме того, мы обернули элемент *label for = “file”* вокруг иконки загрузки. Когда пользователь нажимает на иконку загрузки (т.е. элемент *label*), он будет обрабатываться так, как если бы он кликнул на *input*.

Элемент input имеет слушатель событий change, который вызывает метод uploadImage() при срабатывании. Мы создадим этот сопроводительный метод uploadImage() в свойстве methods в script теге файла App.vue. Чтобы загрузить изображения, мы будем использовать API FileReader. Вот метод uploadImage() в целом:

` ` Давайте рассмотрим, что делает этот метод.

  • Когда пользователь загружает изображение, объект события имеет список объектов файлов, к которым можно получить доступ из evt.target.files.
  • Если файлов нет, мы возвращаемся раньше. Если файлы существуют, мы продолжаем установку new FileReader() в переменную reader.
  • FileReader() позволяет асинхронно читать содержимое файлового объекта. Мы используем функцию readAsDataUrl из переменной reader для чтения содержимого загруженного файла (то есть первого файлового объекта file из evt.target.files).
  • Когда содержимое файла считывается с readAsDataUrl, запускается обработчик события onload, с помощью которого мы используем для установки параметра компонента image в значение target.result события. Затем мы устанавливаем значение step компонента равным 2.
  • Браузер Chrome не запускает событие изменения, если мы решили загрузить одно и то же изображение дважды. Чтобы выполнить небольшое изменение, чтобы обойти это, мы прямо установили значение поля ввода в пустую строку в конце метода. Теперь, когда пользователь снова пытается повторно загрузить тот же файл; это всегда будет воспринято как событие изменения change.

Со всеми изменениями, которые мы изложили выше; файл App.vue будет выглядеть следующим образом:

`

` В этот момент мы сможем успешно загружать файлы изображений.

Примечание: Для простоты мы не будем создавать функции ограничения загрузки пользователем определенных файлов изображений. Если пользователь загружает не изображения (т.е. не .jpg, .png, .gif), то ничего не будет отображаться на месте изображения.

Ленту домашней страницы следует отображать только в том случае, если пользователь находится на первом этапе. Чтобы проверить это, мы добавим оператор v-if для соответствующего элемента DOM в файле PhoneBody.vue со step === 1. Чтобы получить доступ к передаваемому параметру step, мы определим его в свойстве компонентов props:

` ` Теперь лента будет отображаться только тогда, когда пользователь находится на первом начальном этапе.

3) Экран фильтра

Предварительный просмотр изображения и список фильтров

Когда пользователь загружает изображение и направляется на 2 этап, мы хотим отобразить выбранное изображение и фильтры, которые можно применить к нему.

Прежде чем мы начнем отображать список фильтров, давайте создадим стартовый шаблон для этого второго шага. Мы начнем с добавления нового элемента div, который условно отображается, когда параметр step равен 2 в шаблоне компонента PhoneBody. Поскольку мы будем использовать image, который загрузил пользователь, мы также объявим этот параметр в props компонента.

` ` Мы установили, что ожидаемый тип файла *image* должен быть *String*, поскольку [значение аттрибута input файла часто представлен в виде строки](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#Value).

Когда мы нажимаем иконку загрузки и загружаем изображение, мы перенаправляемся на шаг 2:

Давайте теперь посмотрим, как создать список параметров фильтра, которые пользователь может выбрать. Подобно тому, как мы использовали директиву v-for для отображения списка сообщений в ленте, мы будем использовать директиву v-for для отображения списка фильтрующих элементов на основе сбора данных фильтров.

Мы создадим новый компонент FilterType, который будет отвечать за отображение фильтра на изображении.

В компоненте/папке мы создадим новый файл компонента FilterType.vue:

components/ FilterType.vue InstagramPost.vue PhoneBody.vuedata/ styles/ ... Сначала мы заполним файл компонента FilterType.vue исходным содержимым:

`

` Давайте рассмотрим, что содержит этот компонент:

  • Мы используем синтаксис Mustache для привязки filter.name к шаблону.
  • Шаблон также содержит изображениe, которому выставлены стиль background-image, который в свою очередь привязан к image свойству.
  • К этому же изображению мы привязываем класс filter.name. Это нам позволит использовать преимущества библиотеки CSSGram. Мы будем применять фильтры подобные Instagram к изображениям просто добавляя класс с именем фильтра непосредственно к элементу.
  • Мы валидируем свойство filter и image, которые использует этот компонент.
  • И в конце мы задаём, что источник стилей компонентов находится в файле styles/filter-type.scss.

В файле PhoneBody.vue теперь мы можем отобразить список компонентов FilterType. Мы будем отображать этот список в элементе div class = “filter-container” и файл PhoneBody.vue станет таким:

` ` Мы импортируем компонент *FilterType* и объявляем его как тип фильтра в свойстве компонентов *PhoneBody*. Мы рендерим список *filter-type* компонентов на основе *filter* параметров, доступных в этом компоненте. Для каждого отрендереного элемента списка мы передаем итерированный объект *filter* и фактическое изображение *image*.

С обновлениями весь наш файл PhoneBody.vue будет выглядеть следующим образом:

`

` Теперь, когда пользователь перенаправляется на второй шаг - они смогут видеть и скроллить превью фильтров:

Выбор фильтра

Со списком представленных фильтров мы хотим дать пользователю возможность выбрать фильтр, который приведет к включению фильтра в расширенный предварительный просмотр изображения.

Выбранная фильтрация в настоящее время передается от компонента приложения до PhoneBody. Чтобы в конечном итоге отобразить выбранный фильтр при просмотре изображения, нам нужно объявить опору и связать его с классом в элементе большого изображения div (т.e. div class=“selected-image”).

Это приведет к обновлению файла PhoneBody.vue:

` ` Теперь у нас есть свойство *selectedFilter*, которое используется для определения типа фильтра, который следует применять на выбранном элементе *div* изображения. Теперь мы посмотрим, как мы можем изменить этот свойство *selectedFilter*, исходя из того, какой фильтр выбран пользователем. Поскольку мы будем излекать данные для компонента *App* из наследника наследника (PhoneBody), то будем с этой целью использовать [кастомное событие](https://ru.vuejs.org/v2/guide/components-custom-events.html#%D0%A1%D1%82%D0%B8%D0%BB%D1%8C-%D0%B8%D0%BC%D0%B5%D0%BD%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F-%D1%81%D0%BE%D0%B1%D1%8B%D1%82%D0%B8%D0%B9)

Компонент FilterType является дочерним компонентом компонента PhoneBody и внуком компонента App.

Когда пользователь нажимает на определенный тип фильтра из списка фильтров (т.e. кликает по компоненту FilterType), нам нужно сообщить компоненту App, что значение selectedFilter должно измениться. Поскольку мы будем отправлять информацию на два уровня наверх; то мы будем использовать EventBus для прямой передачи специального события на два уровня вверх при выборе типа фильтра.

An EventBus - это экземпляр Vue, который позволяет изолированным компонентам подписываться и публиковать события между собой. Он предлагает быстрое и простое решение для обработки и передачи данных между компонентами, хотя и имеет некоторые ограничения.

Давайте посмотрим на это в действии. В корне папки; мы создадим файл event-bus.js:

... styles/ App.vue event-bus.js ... В файле event-bus.js мы создадим и экспортируем новый экземпляр Vue:

`import Vue from “vue”; const EventBus = new Vue();

export default EventBus; ` Теперь мы можем начать создание диспетчера событий и слушатель.

Мы создадим диспетчер событий в файле FilterType.vue. В FilterType мы добавим слушатель кликов, который при срабатывании запускает настраиваемое событие filter-selected. Когда мы запускаем событие, мы передаем объект, который содержит значение выбранного фильтра. Мы будем использовать экземпляр EventBus для создания настраиваемого события:

` ` Далее в родительском компоненте приложения *App* мы создадим слушатель событий в хуке жизненного цикла компонентов [*created()*](https://vuejs.org/v2/api/#created).

Хук created() запускается сразу как только Vue экземпляра/компонента был создан и можно получить доступ к данным экземпляра и его событиям.

Создав слушатель событий в хуке created(), мы создадим слушателя в момент создания компонента. В callback-функции слушателя мы устанавливаем свойство selectedFilter компонента приложения в значение filter из объекта обработчика события:

` ` Теперь, когда мы выбираем фильтр из списка фильтров, свойство *selectedFilter* будет обновляться в приложении. Так как *selectedFilter* является параметром, переданным из компонента *PhoneBody*, наш UI будет ререндерить отображение выбраного фильтра на большом превью изображении:

Навигация вперед

Единственное, что осталось на этом этапе, это создать элементы навигации, чтобы пользователь мог перейти к 3 шагу или отменить процесс отправки.

В заголовке телефона мы хотим вывести «Cancel» и «Next», когда пользователь находится на 2 шаге. Фактически, мы хотим отобразить ссылку «Cancel», когда пользователь находится на 2 шаге . Это можно сделать используя v-if директиву,

` ` Мы добавили новый анкор элемент, который отображается только когда значение шага компонента равно 2 или 3. Мы также привязали слушатель кликов к этому элементу для вызова метода *goToHome()*, который ещё не определен.

Метод goToHome() будет возвращать пользователя на 1 шаг. К тому же он сбросит все потенциально заполняемые значения, которые мог ввести пользователь - например, image, selectedFilter и caption. Мы разместим этот метод в свойствах methods() компонента:

` ` Иконка «Главной» в футере телефона должна выполнять те же функции и направлять пользователя на первый шаг при нажатии.

В результате, мы присоединим тот же самый слушатель событий к иконке “Главной”:

` ` Когда мы теперь кликнем по иконке «Главная» в нижнем колонтитуле телефона или ссылку «Cancel» в заголовке, мы вернемся к главному экрану.

Давайте также условно отобразим ссылку «Next». Ссылка Next будет отображаться только тогда, когда пользователь находится на step === 2, так как ссылка Share будет показана на следующем шаге. Мы добавим этот элемент ниже cancel-cta в шаблоне приложения:

` ` Для элемента «Next» мы просто подключили слушатель кликов, чтобы увеличить значение *step* на 1.

Одна вещь, которую мы хотели бы - это позволить бы пользователю загружать изображение, если не на домашнем индексном экране (т.е. при step ! == 1 ). Чтобы пользователь не мог кликнуть на input для загрузки изображения мы свяжем свойство disabled для input type = file и будем применять его при step ! == 1:

`

` input загружаемого изображения теперь будет отключен, если пользователь не находится на первом шаге. Это завершение построения второго шага нашего UI. Со всеми внесенными изменениями наш файл App.vue будет выглядеть следующим образом:

`

` Если мы протестируем наше приложение, мы сможем загрузить изображение и выбрать наш фильтр выбора.

Все, что нам осталось сделать, это захватить введенный пользователем текст на третьем шаге и это позволит пользователю поделиться созданным постом!

4) Завершение процесса оправки сообщения

Захват контента

Когда пользователь находится на 3 шаге, мы хотим отобразить выбранный предварительный просмотр изображения, но в этом случае также присутствует вход в textarea, где мы можем позволить пользователю отправить подпись в свой пост.

В шаблоне PhoneBody.vue мы представим новый элемент, который отображается только тогда, когда шаг равен 3. Этот элемент будет содержать элемент предварительного просмотра div class = “selected-image”, как и второй шаг, но теперь также будет содержать поле textarea:

` ` Нам нужно получить значение, которое вводит пользователь и привязать его к *caption* в родительском *App* компоненте. Чтобы это произошло, мы будем передавать параметр *caption* в компонент *PhoneBody* и использовать директиву [v-model](https://vuejs.org/v2/guide/forms.html#Text) в *textarea* для создания двухсторонней привязки данных. Хотя обычно это работает, *в этом случае это не сработает*.

Использование v-модели в PhoneBody для непосредственной привязки к полю caption не будет работать, потому что дочерний компонент PhoneBody, напрямую будет мутировать значение данных родительского компонента (App). Это известная плохая практика, что приведёт к генерации Vue предупреждения в консоли.

Мы можем избежать этого, объявив более специфичную двухстороннюю привязку данных. Во-первых, мы уже объявили атрибут v-model, где компонент PhoneBody рендерится в шаблоне App.vue:

` ` *v-model* по сути связывает параметр *value* и создает обработчик ввода, который изменяет параметр *value*. Эта штука в документации Vue объясняет это еще немного - [Использование `v-model` на компонентах](https://ru.vuejs.org/v2/guide/components.html#%D0%98%D1%81%D0%BF%D0%BE%D0%BB%D1%8C%D0%B7%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-v-model-%D0%BD%D0%B0-%D0%BA%D0%BE%D0%BC%D0%BF%D0%BE%D0%BD%D0%B5%D0%BD%D1%82%D0%B0%D1%85).

В PhoneBody мы можем теперь объявить значение, которое будет передаваться вниз и привязать его к полю textarea поста. Наш обновлённый файл PhoneBody.vue станет выглядеть следующим образом:

` ` Чтобы захватить в режиме реального времени ввод пользователя, нам нужно будет выпустить новое кастомное *input* событие и передать новое значение ввода.

` `

Поскольку PhoneBody является прямым дочерним элементом App, мы можем использовать интерфейс событий PhoneBody, чтобы напрямую передать событие.

Теперь, когда пользователь вводит подпись в PhoneBody, свойство данных caption в приложении обновляется с помощью настраиваемого input события, которое запускается.

Теперь файл PhoneBody.vue будет выглядеть следующим образом:

`

`

Публикация поста

Чтобы завершить отправку поста, мы должны предоставить пользователю возможность публиковать пост по клику на ссылку «Share» в шапке:

Ссылка на Share должна присутствовать только в том случае, если пользователь находится на 3м (последнем) шаге. Поэтому мы добавим этот элемент условно в заголовок раздела App:

` ` Мы подключили слушатель кликов, который вызывает метод *sharePost()* при нажатии ссылки «Share». Этот метод будет делать три вещи:

Он подготовит post объект, который будет содержать данные, предоставленные пользователем

В дополнение к тому, что пользователь отправил, подготовленный post объект будет содержать информацию про имя пользователя username и изображение постера userImage. Для простоты мы просто зададим имя пользователя fullstack_vue, а userImage как изображение логотипа Vue.

Он будет пушить новый post объект в массив posts приложения

Чтобы пушнуть новый элемент в начало массива, мы будем использовать нативный метод Array.unshift().

Он сбросит всю информацию и вернет пользователя на главную страницу

У нас уже есть метод goToHome(), который мы, наконец, будем использовать для сброса информации о пользователе и установки значения шага step назад в 1.

Со всем этим мы укажем этот метод sharePost() в свойстве methods() приложения:

` ` Мы не будем делать больше обновлений для компонента *App*, теперь *App.vue* будет выглядеть так на конечном этапе:

`

` Теперь мы можем пройти весь процесс отправки поста!

5) Перетягивание-прокрутка

UI нашего приложения почти закончен. Мы добавим одну новую дополнительную функцию для завершения урока.

Как вы могли заметить, нам пришлось прокручивать ленту (и список фильтров) для навигации по списку элементов. В традиционных мобильных приложениях мы часто можем перемещаться по приложению перетягиванием экрана. Чтобы включить аналогичную функцию с перетаскиванием в нашем приложении, мы будем использовать vue-dragscroll библиотеку.

Для включения новой библиотеки в наше приложение нам нужно установить её с помощью yarn или npm (или получить к ней доступ через CDN):

npm install vue-dragscroll --save В нашей песочнице уже есть библиотека vue-dragscroll как зависимость.

С доступной библиотекой мы сначала зарегистрируем ее в нашем приложении. В файле index.js мы импортируем библиотеку vue-dragscroll и укажем Vue.use(), чтобы использовать плагин в нашем модульном приложении:

import Vue from "vue"; import App from "./App"; import VueDragscroll from "vue-dragscroll"; Vue.use(VueDragscroll); /* eslint-disable no-new */ new Vue({ el: "#app", render: h => h(App) }); С установленным плагином мы можем использовать директивы v-dragscroll в наших шаблонах. Мы хотим, чтобы лента и списки фильтров приложения можно было перетаскивать вдоль осей Y и X соответственно:

В шаблоне PhoneBody.vue мы укажем возможность вертикального перетаскивания в ленте, добавив v-dragscroll.y в элемент ленты. Аналогично, мы добавим v-dragscroll.x в контейнер фильтров, который содержит список типов фильтров на 2 шаге.

` ` И наконец-то наше приложение завершено! Со всем реализованным мы можем перетаскивать посты в ленте, загружать изображение, выбирать фильтр, добавлять текст к фото и публиковать посты!

Вывод

Без учета стилизации нашего приложения с помощью CSS мы прошли через урок создание полного UI, который имитирует процес отправки фото и сообщений в Instagram на мобильном устройстве. К тому же мы воспользовались библиотекой CSSGram для добавления фильтров как в Instagram, и vue-dragscroll для добавления функциональности прокрутки и перетаскивания постов.

Если вы застряли в любой момент и/или имеете дополнительные вопросы, вы более чем можете оставить комментарий или сообщение прямо мне! Обратная связь всегда приветствуется!