Продолжаем серию уроков по связке Laravel и VueJs, в которой мы пишем приложение — финансовый менеджер. Сегодня будем добавлять стили, наводить порядок в «дизайне» и описывать документацию API.
Шаг №1. Установка tailwindcss.
Для оформления и «дизайна» мы будем использовать один из доступных CSS фреймворков. Но вы можете использовать уже один из готовых html шаблонов(я часто беру бессплатные шаблоны админок у colorlib). Я давно хотел попробовать поработать с TailwindCss, поэтому в данной серии статтей буду использовать именно его.
Установка в vue очень простая, чтобы не раздувать статью оставляю пару простых комманд:
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
npx tailwind init
Далее откроем файл в корне проекта webpack.mix.js
и добавим содержимое:
const mix = require('laravel-mix'); const tailwindcss = require('tailwindcss'); mix.js('resources/js/app.js', 'public/js') .sass('resources/sass/app.scss', 'public/css') .options({ processCssUrls: false, postCss: [tailwindcss('./tailwind.config.js')] });
Смело запускаем сборку ассетов командой npm run prod
или npm run watch
и на этом все, подключение стилей завершено, можем приступать к созданию нашего великолепного UI.
Шаг №2. Страница логина.
Первым делом сделаем страницу логина. Сам пример для формы аутентификации я взял уже из готовых компонентов tailwindcss.
resources/js/pages/Login.vue
<template> <div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8"> <div class="max-w-md w-full space-y-8"> <div> <h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900"> Вход в учетную запись </h2> </div> <div class="rounded-md shadow-sm -space-y-px"> <div> <label for="email-address" class="sr-only">Email address</label> <input id="email-address" name="email" type="email" v-model="form.email" autocomplete="email" required class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" placeholder="Email address"> </div> <div> <label for="password" class="sr-only">Password</label> <input id="password" name="password" type="password" v-model="form.password" autocomplete="current-password" required class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" placeholder="Password"> </div> </div> <div class="flex items-center justify-between"> <div class="text-sm"> <a href="#" class="font-medium text-indigo-600 hover:text-indigo-500"> Forgot your password? </a> </div> </div> <div> <button type="submit" class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> <span class="absolute left-0 inset-y-0 flex items-center pl-3"> <svg class="h-5 w-5 text-indigo-500 group-hover:text-indigo-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> <path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"/> </svg> </span> Sign in </button> </div> </div> </div> </template> <script> import axios from 'axios'; import {API_LOGIN_URL} from '../api/auth'; export default { name: 'Login', data() { return { pending: false, loggedIn: false, form: { email: null, password: null } } }, methods: { sendForm() { if (this.pending === false) { this.pending = true; axios.post(API_LOGIN_URL, this.form) .then(response => { this.loggedIn = true; }) .catch(errors => { }) .then(() => { this.pending = false; }); } } } } </script> <style scoped> </style>
Вот и страница готова, только ей не хватает валидации. Валидация нужна для того, чтобы предотвратить ввод «неправильных» данных. Чтобы предотвратить ситуацию, когда вместо email адреса, пользователь вводит случайный набор символов или вписывает в поле слишком длинное значение, которое не поместится в колонку базы данных. В данном случае, валидация у нас будет выполняться как на фронтенде, так и на бекенде.
Для чего это нужно? Таким образом, на бекенде мы точно будем знать, что неправильные данные не попадут в логику нашего приложения и будут остановлены на контроллере. А со стороны фронтенда мы предотвратим попытку отправки неправильных данных на бекенд и сможем отобразить ошибки.
Шаг №3. Валидация.
Для валидации я использовал библиотеку https://vee-validate.logaretm.com/v3/.
Устанавливаем командой:
npm install vee-validate --save
Теперь нужно подключить локализацию ошибок, делается это в файле resources/js/app.js
:
import { extend, localize } from 'vee-validate'; import ru from 'vee-validate/dist/locale/ru.json'; import * as rules from 'vee-validate/dist/rules'; Object.keys(rules).forEach(rule => { extend(rule, rules[rule]); }); localize('ru', ru);
Добавляем валидацию в компонент resources/js/pages/Login.vue
:
import { ValidationProvider } from 'vee-validate';
После импорта зарегистрируем компонент валидации в нашем Login
компоненте:
components: { ValidationProvider }
После всех манипуляций мы готовы писать наши валидации. Для того, чтобы проверить какое-либо поле, нам нужно его добавить внутри тегов:
<ValidationProvider :rules="{ required: true }" name="password" v-slot="{ errors }"> // Тут инпут </ValidationProvider>
Для примера приведу валидацию email и пароля(файл resources/js/pages/Login.vue
):
<div> <label for="email" class="sr-only">Email address</label> <ValidationProvider :rules="{ required: true, email: true }" name="email" v-slot="{ errors }"> <input id="email" name="email" type="email" v-model="form.email" autocomplete="email" required class="appearance-none rounded-none relative block w-full px-3 py-2 border placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" :class="[errors[0] ? 'border-red-500' : 'border-gray-300']" placeholder="Email address"> </ValidationProvider> </div> <div> <ValidationProvider :rules="{ required: true }" name="password" v-slot="{ errors }"> <label for="password" class="sr-only">Password</label> <input id="password" name="password" type="password" v-model="form.password" autocomplete="current-password" required class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" :class="[errors[0] ? 'border-red-500' : 'border-gray-300']" placeholder="Password"> </ValidationProvider> </div>
С этим разобрались, но как быть с ошибками которые возвращает нам бекенд? Большой плюс этой библиотеки для валидации в том, что в неё можно вручную добавлять ошибки валидации. Поэтому, когда мы получим с бекенда ответ с ошибками, добавим их и они автоматически отобразятся на форме.
Добавим новый компонент ValidationObserver
.
Заменим эту строку:
import {ValidationProvider} from 'vee-validate';
На эту:
import {ValidationProvider, ValidationObserver} from 'vee-validate';
И не забываем зарегистрировать компонент.
Меняем шаблон компонента:
<template> <div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8"> <div class="max-w-md w-full space-y-8"> <div> <h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900"> Вход в учетную запись </h2> </div> <ValidationObserver ref="form" v-slot="{ handleSubmit }"> <form class="mt-8 space-y-6" @submit.prevent="handleSubmit(sendForm)"> <div class="rounded-md shadow-sm -space-y-px"> <div> <label for="email" class="sr-only">Email address</label> <ValidationProvider :rules="{ required: true, email: true }" name="email" v-slot="{ errors }"> <input id="email" name="email" type="email" v-model="form.email" autocomplete="email" required class="appearance-none rounded-none relative block w-full px-3 py-2 border placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" :class="[errors[0] ? 'border-red-500' : 'border-gray-300']" placeholder="Email address"> </ValidationProvider> </div> <div> <ValidationProvider :rules="{ required: true }" name="password" v-slot="{ errors }"> <label for="password" class="sr-only">Password</label> <input id="password" name="password" type="password" v-model="form.password" autocomplete="current-password" required class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" :class="[errors[0] ? 'border-red-500' : 'border-gray-300']" placeholder="Password"> </ValidationProvider> </div> </div> <div class="flex items-center justify-between"> <div class="text-sm"> <a href="#" class="font-medium text-indigo-600 hover:text-indigo-500"> Forgot your password? </a> </div> </div> <div> <button type="submit" class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> <span class="absolute left-0 inset-y-0 flex items-center pl-3"> <svg class="h-5 w-5 text-indigo-500 group-hover:text-indigo-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> <path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"/> </svg> </span> Sign in </button> </div> </form> </ValidationObserver> </div> </div> </template>
Теперь приступим к обработке ошибок с бекенда. Нам нужно проверить код ответа и если он равняется 422
, тогда добавим ошибки из ответа в наш валидатор.
Переходим в метод sendForm
и в catch()
добавляем проверки:
sendForm() { if (this.pending === false) { this.pending = true; axios.post(API_LOGIN_URL, this.form) .then(response => { this.loggedIn = true; }) .catch(errors => { switch (errors.response.status) { case 422: this.$refs.form.setErrors(errors.response.data.errors) break; } }) .then(() => { this.pending = false; }); } }
Конструкцию switch
на будущее, мы будем обрабатывать другие коды ответа, а на данном этапе нам достаточно одного case
.
Шаг №4. Страница регистрации.
Форму регистрации делаем аналогично форме логина, с одним лишь исключением. Ошибки мы будем выводить над полями для ввода данных.
resources/js/pages/Registration.vue
<template> <div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8"> <div class="max-w-md w-full space-y-8"> <div> <h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900"> Вход в учетную запись </h2> </div> <ValidationObserver ref="form" v-slot="{ handleSubmit }"> <form class="mt-8 space-y-6" @submit.prevent="handleSubmit(sendForm)"> <div class="rounded-md shadow-sm -space-y-px"> <div> <label for="name" class="sr-only">Имя</label> <ValidationProvider :rules="{ required: true }" name="name" v-slot="{ errors }"> <span class="font-medium text-sm ml-3 text-red-700" v-if="errors[0]">{{errors[0]}}</span> <input id="name" name="name" type="text" v-model="form.name" autocomplete="email" required class="block border border-grey-light w-full p-3 rounded mb-4" :class="[errors[0] ? 'border-red-500' : 'border-gray-300']" placeholder="Имя"> </ValidationProvider> </div> <div> <label for="email" class="sr-only">Email</label> <ValidationProvider :rules="{ required: true, email: true }" name="email" v-slot="{ errors }"> <span class="font-medium text-sm ml-3 text-red-700" v-if="errors[0]">{{errors[0]}}</span> <input id="email" name="email" type="email" v-model="form.email" autocomplete="email" required class="block border border-grey-light w-full p-3 rounded mb-4" :class="[errors[0] ? 'border-red-500' : 'border-gray-300']" placeholder="Email"> </ValidationProvider> </div> <div> <label for="password" class="sr-only">Пароль</label> <ValidationProvider :rules="{ required: true }" name="password" v-slot="{ errors }"> <span class="font-medium text-sm ml-3 text-red-700" v-if="errors[0]">{{errors[0]}}</span> <input id="password" name="password" type="password" v-model="form.password" autocomplete="current-password" required class="block border border-grey-light w-full p-3 rounded mb-4" :class="[errors[0] ? 'border-red-500' : 'border-gray-300']" placeholder="Пароль"> </ValidationProvider> </div> <div> <label for="password_confirmation" class="sr-only">Подтверждение пароля</label> <ValidationProvider :rules="{ required: true }" name="password_confirmation" v-slot="{ errors }"> <span class="font-medium text-sm ml-3 text-red-700" v-if="errors[0]">{{errors[0]}}</span> <input id="password_confirmation" name="password" type="password" v-model="form.password_confirmation" autocomplete="current-password" required class="block border border-grey-light w-full p-3 rounded mb-4" :class="[errors[0] ? 'border-red-500' : 'border-gray-300']" placeholder="Подтверждение пароля"> </ValidationProvider> </div> </div> <div> <button type="submit" class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> <span class="absolute left-0 inset-y-0 flex items-center pl-3"> <svg class="h-5 w-5 text-indigo-500 group-hover:text-indigo-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> <path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"/> </svg> </span> Регистрация </button> </div> </form> </ValidationObserver> </div> </div> </template> <script> import axios from 'axios'; import {API_REGISTRATION_URL} from '../api/auth'; import {ValidationProvider, ValidationObserver} from 'vee-validate'; export default { name: 'Registration', components: { ValidationProvider, ValidationObserver }, data() { return { pending: false, registered: false, form: { name: null, email: null, password: null, password_confirmation: null, } } }, methods: { sendForm() { if (this.pending === false) { this.pending = true; axios.post(API_REGISTRATION_URL, this.form) .then(response => { this.registered = true; }) .catch(errors => { switch (errors.response.status) { case 422: this.$refs.form.setErrors(errors.response.data.errors) break; } }) .then(() => { this.pending = false; }); } } } } </script>
Аналогично странице логина, мы сделали страницу регистрации. Отличие только в том, что выводим текст ошибки сразу над полем ввода.
Теперь можете проверить страницы, если вы все правильно сделали, у вас должен быть примерно такой результат:


Кода получилось довольно много, но в то же время он очень простой. Не забывайте, что в процессе разработки нужно держать постоянно запущеной команду npm run watch
, а для «боевого» окружения использовать npm run prod
.
Для удобства, я добавил все файлы в репозиторий, чтобы вы могли подсмотреть, если что-то не получается.
https://gitlab.com/oneshkip/finmanager
И напоминаю, если у вас есть замечания, пожелания или на чем-то застряли, оставляйте комментарии.
npx tailwind init…. npx это опечатка?
нет)