Skip to main content
Шаблон и валидация

Менеджер финансов — Шаблон и валидация.

Продолжаем серию уроков по связке 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

И напоминаю, если у вас есть замечания, пожелания или на чем-то застряли, оставляйте комментарии.

2 thoughts to “Менеджер финансов — Шаблон и валидация.”

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *