Используйте FAB, диалоговые окна и элементы управления формами, чтобы добавлять новые задачи в приложение списка дел.

Привет и добро пожаловать обратно в нашу продолжающуюся серию руководств, в которой мы собираемся создать приложение todo от начала до конца, используя Vuetify и Vue JS. В нашем последнем эпизоде мы научились отображать пример данных списка дел с помощью компонента Vuetify v-list. Сегодня мы добавим функциональность, позволяющую пользователям заполнять список новыми задачами. Когда мы закончим с этим, нам не понадобятся данные фиктивной задачи, которые мы создали в прошлый раз. Приступим к делу немедленно!

Только начинаете работать с Vuetify? Ознакомьтесь с этой статьей.

Показ FAB

Мы будем использовать плавающую кнопку действия, чтобы пользователи могли добавлять новые задачи, поскольку это должно быть основным действием в нашем приложении. Мы можем создать FAB, установив свойство fab в компоненте Vuetify v-btn.

src/App.js
<template>
  <v-app>
    <v-card>
      <v-toolbar color="primary" elevation="3" dark rounded="0">
        <v-toolbar-title>Tasks</v-toolbar-title>
      </v-toolbar>
    </v-card>
    <v-card class="ma-4">
      <v-list>
        <v-list-item
          v-for="(task, index) in tasks"
          :key="index"
          v-bind:class="{ 'task-completed': task.isCompleted }"
          two-line
        >
          <v-checkbox
            hide-details
            v-model="task.isCompleted"
            class="mt-0 mr-2"
          ></v-checkbox>
          <v-list-item-content>
            <v-list-item-title>{{ task.title }}</v-list-item-title>
            <v-list-item-subtitle>{{ task.note }}</v-list-item-subtitle>
          </v-list-item-content>
        </v-list-item>
      </v-list>
    </v-card>
    <v-btn fab fixed right bottom color="primary">
    </v-btn>
  </v-app>
</template>
...

Пропсы fixed, right и bottom должны говорить сами за себя — вместе они привязывают FAB к правому нижнему углу области просмотра:

Кнопка будет отображать значок плюса Material Design, чтобы указать, что она предназначена для добавления задач:

src/App.js
<template>
  <v-app>
    <v-card>
      <v-toolbar color="primary" elevation="3" dark rounded="0">
        <v-toolbar-title>Tasks</v-toolbar-title>
      </v-toolbar>
    </v-card>
    <v-card class="ma-4">
      <v-list>
        <v-list-item
          v-for="(task, index) in tasks"
          :key="index"
          v-bind:class="{ 'task-completed': task.isCompleted }"
          two-line
        >
          <v-checkbox
            hide-details
            v-model="task.isCompleted"
            class="mt-0 mr-2"
          ></v-checkbox>
          <v-list-item-content>
            <v-list-item-title>{{ task.title }}</v-list-item-title>
            <v-list-item-subtitle>{{ task.note }}</v-list-item-subtitle>
          </v-list-item-content>
        </v-list-item>
      </v-list>
    </v-card>
    <v-btn fab fixed right bottom color="primary">
      <v-icon>mdi-plus</v-icon>
    </v-btn>
  </v-app>
</template>
...

Получите исходный код этого приложения

Зарегистрируйтесь здесь, чтобы получить последний исходный код этого замечательного приложения!

Отображение диалогового окна для добавления новых задач

Теперь воспользуемся компонентом v-dialog для создания диалога:

src/App.js
<template>
  <v-app>
    ...
    <v-btn fab fixed right bottom color="primary" @click="showNewTaskDialog = true">
      <v-icon>mdi-plus</v-icon>
    </v-btn>
    <v-dialog v-model="showNewTaskDialog" width="500">
      <v-card>
        <v-card-title class="text-h5 grey lighten-2">Add New Task</v-card-title>
      </v-card>
    </v-dialog>
  </v-app>
</template>
<script>
export default {
  name: 'App',
  data: () => ({
    tasks: [...Array(10)].map((value, index) => ({
      id: `task${index + 1}`,
      title: `Task ${index + 1}`,
      note: `Some things to note about task ${index + 1}`,
      isCompleted: false,
    })),
    showNewTaskDialog: false,
  }),
};
</script>
...

С помощью директивы v-model мы устанавливаем двустороннюю привязку между текущим состоянием открытия/закрытия диалога и новой созданной нами переменной showNewTaskDialog.

Мы используем v-card для создания тела диалогового окна и устанавливаем его заголовок с помощью v-card-title, стиль которого мы создали с помощью некоторых классов Vuetify.

Вместо ручной установки showNewTaskDialog в true в обработчике событий @click FAB, как мы сделали выше, мы могли бы сделать это автоматически, поместив FAB в слот activator v-dialog. Это дает FAB доступ к двум свойствам слота, on и attrs, которые устанавливают свойства и события FAB (используя v-on и v-bind):

src/App.js
<template>
  <v-app>
  ...   
    <v-dialog v-model="showNewTaskDialog" width="500">
      <template v-slot:activator="{ on, attrs }">
        <v-btn fab fixed right bottom color="primary" v-on="on" v-bind="attrs">
          <v-icon>mdi-plus</v-icon>
        </v-btn>
      </template>
      <v-card>
        <v-card-title class="text-h5 grey lighten-2">Add New Task</v-card-title>
      </v-card>
    </v-dialog>
  </v-app>
</template>
...

Прямо сейчас вот что появляется, когда пользователь нажимает FAB:

Создание формы

Мы создадим форму, используя компонент Vuetify v-form. Чтобы получить входные данные для заголовка, мы настроим еще одну двустороннюю привязку, на этот раз между новым компонентом v-text-field и свойством title нового объекта, который мы создали для временного хранения новых данных задачи (newTask):

src/App.js
<template>
  <v-app>
    ...
    <v-dialog v-model="showNewTaskDialog" width="500">
      <template v-slot:activator="{ on, attrs }">
        <v-btn fab fixed right bottom color="primary" v-on="on" v-bind="attrs">
          <v-icon>mdi-plus</v-icon>
        </v-btn>
      </template>
      <v-card>
        <v-card-title class="text-h5 grey lighten-2">Add New Task</v-card-title>
        <v-form class="mx-4 mt-4">
          <v-text-field
            v-model="newTask.title"
            label="Title"
            required
          ></v-text-field>
        </v-form>
      </v-card>
    </v-dialog>
  </v-app>
</template>
<script>
export default {
  name: 'App',
  data: () => ({
    ...
    newTask: {
      title: '',
    },
  }),
};
</script>
...

Давайте воспользуемся v-textarea для создания текстовой области для ввода примечания к задаче, так как мы ожидаем, что она будет довольно большого размера.

src/App.js
...
      <v-card>
        <v-card-title class="text-h5 grey lighten-2">Add New Task</v-card-title>
        <v-form class="mx-4 mt-4">
          <v-text-field
            v-model="newTask.title"
            label="Title"
            required
          ></v-text-field>
          <v-textarea label="Note" v-model="newTask.note"></v-textarea>
        </v-form>
      </v-card>
    </v-dialog>
  </v-app>
</template>
<script>
export default {
  name: 'App',
  data: () => ({
    ...
    newTask: {
      title: '',
      note: '',
    },
  }),
};
</script>
...

Мы можем изменить дизайн текстовых областей и текстовых полей в Vuetify. Существует 3 варианта ввода текста: вариант без блока (по умолчанию), вариант с заполнением и вариант с контуром (вариант без блока устарел и больше не упоминается в официальных руководствах по дизайну материалов, тем не менее такие фреймворки дизайна материалов, как Vuetify продолжает поддерживать его). Давайте настроим это:

src/App.js
    
...
      <v-card>
        <v-card-title class="text-h5 grey lighten-2">Add New Task</v-card-title>
        <v-form class="mx-4 mt-4">
          <v-text-field
            v-model="newTask.title"
            label="Title"
            required
            outlined
          ></v-text-field>
          <v-textarea label="Note" v-model="newTask.note" outlined></v-textarea>
        </v-form>
      </v-card>
    </v-dialog>
  </v-app>
</template>
...

Создание кнопок формы

В форме потребуются две кнопки: одна для отмены добавления новой задачи и закрытия диалога, а другая для отправки ввода формы и создания задачи. Кнопка Cancel будет простого варианта, а кнопка Add будет приподнята:

src/App.js
...
      <v-card>
        <v-card-title class="text-h5 grey lighten-2">Add New Task</v-card-title>
        <v-form class="mx-4 mt-4 pb-4">
          <v-text-field
            v-model="newTask.title"
            label="Title"
            required
            outlined
          ></v-text-field>
          <v-textarea label="Note" v-model="newTask.note" outlined></v-textarea>
          <div class="d-flex justify-end">
            <v-btn plain class="mr-2">Cancel</v-btn>
            <v-btn color="primary">Add</v-btn>
          </div>
        </v-form>
      </v-card>
    </v-dialog>
  </v-app>
</template>
...

Обратите внимание, что мы использовали вспомогательные классы гибкости Vuetify, чтобы выровнять две кнопки вправо. Мы также добавили пространство между ними с помощью вспомогательного класса mr-2:

Сброс формы ввода

В дополнение к закрытию диалогового окна при нажатии кнопки Cancel нам также потребуется очистить все входные данные формы и сбросить все ошибки проверки. Для этого мы можем использовать метод reset(), доступ к которому можно получить, установив ссылку на компонент формы:

src/App.js
...
      <v-card>
        <v-card-title class="text-h5 grey lighten-2">Add New Task</v-card-title>
        <v-form class="mx-4 mt-4 pb-4" ref="form">
          <v-text-field
            v-model="newTask.title"
            label="Title"
            required
            outlined
          ></v-text-field>
          <v-textarea label="Note" v-model="newTask.note" outlined></v-textarea>
          <div class="d-flex justify-end">
            <v-btn plain class="mr-2" @click="cancelButtonClick">Cancel</v-btn>
            <v-btn color="primary">Add</v-btn>
          </div>
        </v-form>
      </v-card>
    </v-dialog>
  </v-app>
</template>
<script>
export default {
  name: 'App',
  data: () => ({
    ...
  }),
  methods: {
    cancelButtonClick() {
      this.showNewTaskDialog = false;
      this.$refs.form.reset();
    }
  }
};
</script>
...

Проверка ввода формы при отправке

Теперь давайте удостоверимся, что данные нашей формы верны перед созданием задачи. Мы устанавливаем свойство lazy-validation в форме, что означает, что ее ввод не будет проверяться на достоверность, пока мы не вызовем метод validate() в форме ref. Кроме того, мы прослушиваем событие submit и используем модификатор события .prevent из Vue, чтобы предотвратить перезагрузку страницы при отправке.

src/App.js
...
        <v-form
          class="mx-4 mt-4 pb-4"
          ref="form"
          @submit.prevent="newTaskFormSubmit"
          lazy-validation
        >
          <v-text-field
            v-model="newTask.title"
            label="Title"
            required
            outlined
            :rules="titleRules"
          ></v-text-field>
          <v-textarea label="Note" v-model="newTask.note" outlined></v-textarea>
          <div class="d-flex justify-end">
            <v-btn plain class="mr-2" @click="cancelButtonClick">Cancel</v-btn>
            <v-btn color="primary" type="submit">Add</v-btn>
          </div>
        </v-form>
      </v-card>
    </v-dialog>
  </v-app>
</template>
<script>
export default {
  name: 'App',
  data: () => ({
    ...
    newTask: {
      title: '',
      note: '',
    },
    titleRules: [(value) => Boolean(value) || 'Enter a title'],
  }),
  methods: {
    cancelButtonClick() {
      this.showNewTaskDialog = false;
      this.$refs.form.reset();
    },
    newTaskFormSubmit() {
      if (this.$refs.form.validate()) {
        // add new task
      }
    },
  },
};
</script>
...

Компоненты ввода текста в Vuetify выполняют проверку с помощью реквизита rules. Это свойство принимает смешанный массив, элементами которого могут быть функции, строки или логические значения. Каждый из элементов представляет собой правило. Единственная функция, которую мы здесь создаем в массиве, принимает текущее введенное значение и возвращает логическое значение или строку. Если возвращаемое значение функции равно true (что в данном случае будет означать, что заголовок был введен, а входная строка больше не соответствует действительности), то проверка считается успешной. Но если функция возвращает значение false или строку (содержащую сообщение об ошибке), то проверка завершается неудачно, и ввод формы переходит в состояние ошибки.

Итак, когда пользователь не вводит заголовок и нажимает Add :

Добавление задачи при успешной проверке

Осталось добавить новый элемент в tasks, закрыть диалог и сбросить ввод формы после успешной проверки:

src/App.js
<template>
  <v-app>
    <v-card>
      <v-toolbar color="primary" elevation="3" dark rounded="0">
        <v-toolbar-title>Tasks</v-toolbar-title>
      </v-toolbar>
    </v-card>
    <v-card class="ma-4" v-show="tasks.length > 0">    <!-- Hide card if there are no tasks -->
      ...
    </v-card>
    ...
  </v-app>
</template>
<script>
import { v4 } from 'uuid';    // Add the "uuid" module for random task ID generation
export default {
  name: 'App',
  data: () => ({
    tasks: [], // We don't need sample data anymore
    showNewTaskDialog: false,
    newTask: {
      title: '',
      note: '',
    },
    titleRules: [(value) => Boolean(value) || 'Enter a title'],
  }),
  methods: {
    cancelButtonClick() {
      this.showNewTaskDialog = false;
      this.$refs.form.reset();
    },
    newTaskFormSubmit() {
      if (this.$refs.form.validate()) {
        this.tasks.push({
          id: v4(),
          title: this.newTask.title,
          note: this.newTask.note,
          isCompleted: false,
        });
        this.showNewTaskDialog = false;
        this.$refs.form.reset();
      }
    },
  },
};
</script>
...

Продолжение следует…

Сегодня мы узнали, как использовать FAB, диалоги, формы, ввод текста и кнопки, чтобы пользователи могли создавать свои собственные задачи. Мы также столкнулись с некоторыми полезными методами формы от Vuetify для проверки и сброса ввода.

Оставайтесь с нами для нашего следующего эпизода, так как вместе мы создаем это приложение со списком дел от начала до конца, используя эту фантастическую структуру Material Design.

Больше контента на plainenglish.io. Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Получите эксклюзивный доступ к возможностям написания и советам в нашем сообществе Discord.