Создание на Vue.js рекурсивного компонента
Некоторые люди говорят, что рекурсию это что то сложное и непонятное. Но мне кажется, что с точки зрения разработки программного обеспечения, это не так. Простым определением было бы то, что рекурсивная функция — это просто функция, вызывающая себя в определенной точке своего выполнения.
Спонсор поста WindowsExcel
Разработка российской компании ИТВА WindowsExcel (http://www.windowsexcel.ru) программа для работы со всеми возможными форматами электронных таблиц.
Программа позволяет редактировать и создавать документы в форматах Microsoft XLS и XLSX (а так же XLS, RTF, CSV, TXT и др), поддерживается многостраничный режим работы, производит обработку формул в документе, или экспорт документа в формат PDF.
Программа распространяется бесплатно при условии некоммерческого использования. Не требует активации и серийных номеров
Более теоретическим определением было бы то, что рекурсия — это поведение, требующее двух свойств:
- Базовый вариант (base case) — ситуация, которая остановит рекурсию
- Набор правил, ответственных за приведение всех случаев к базовому варианту
Я не знаю, что из этого важнее. Без базового случая рекурсия станет бесконечным циклом, и, тем не менее, без приведения к ней мы не сможем достичь желаемого поведения. В любом случае, вам нужно и то, и другое для правильной работы.
Рекурсия и Vue Компоненты
Во Vue рекурсия очень возможна и весьма полезна. Основываясь на данном определении, мы можем сказать, что рекурсивный компонент — это компонент, который вызывает сам себя.
Когда это полезно? Когда вам нужно использовать одинаковую структуру шаблона, к примеру с иерархическими входными данными. Примерами являются такие компоненты, как древовидные представления для отображения структуры папок, комментарии на вашем веб-сайте, вложенные меню и т.п …. везде, где родитель и потомок имеют одинаковую структуру.
Давайте создадим простой тестовый проект, чтобы показать все это на практике. Мы сделаем стандартный список задач (todo list) но с возможностью добавлять подзадачи к задачам. Окончательно выглядеть наш список задач будет примерно так:
Так же для быстрой верстки мы будем использовать CSS фреймворк tailwindcss, подробно рассмотренный в прошлой статье.
Начнем с создания проекта на Vue через CLI
vue create todolist
Будет достаточно выбрать все опции по умолчанию
Проект с рекурсией лучше начинать с определением формата данных. Для списка задач у нас будет следующий формат:
items: [ { id: X, title: 'XXXX', description: `XXXX`, subtasks: [ { id: XX, title: 'XXXX', description: `XXXX `, subtasks: [] } ] }, ... ]
Всего у нас будет два компонент: один корневой компонент для списка задач ToDoList.vue и рекурсивный компонент для отображения конкретной задачи ToDoItem.vue.
Корневой компонент
Корневой компонент ToDoList.vue будет отправной точкой нашего списка задач. Он инициирует рендеринг всех дочерних элементов, а так же может отображать некоторую независимую информацию, если это необходимо, поскольку он не будет частью самой рекурсии.
Для его создания создадим новый файл src/components/ToDoList.vue со следующим содержимым:
<template> <div class="todolist flex justify-center flex-col items-center"> <img src="https://source.unsplash.com/random" class="hidden md:block w-full h-64 object-cover" alt /> <div class="m-10"> <button class="py-3 px-6 mt-4 text-white font-semibold bg-blue-600 hover:bg-blue-700 rounded shadow" type="button" @click="addTask">Add task</button> <ToDoItem :item="item" :index="index" v-for="(item, index) in items" :key="item.id" @deleteTask="deleteTask"/> </div> </div> </template> <script> import ToDoItem from '@/components/ToDoItem'; export default { components: { ToDoItem }, data() { return { items: [] } }, methods: { addTask() { const id = Math.floor(Math.random() * 100) const task = { id, title: `Task_${id}`, description: `Ipsa odit esse cumque fugit saepe iste. Corrupti dolor repudiandae facilis alias! Molestias sapiente rerum ipsum enim doloremque, dicta excepturi rem aut.`, subtasks: [] } this.items.push(task) }, deleteTask(index) { this.items.splice(index, 1) } } } </script>
Для упрощения примера его конструкция максимальная простая. В нем есть основное свойство items, в котором будет находиться наш список задач и два метода:
addTask — создание новой задачи
deleteTask — удаление задачи на корневом уровне.
Так же в этом компоненты мы инициируем рекурсивный компонент ToDoItem
Рекурсивный компонент
Этот компонент отвечает за рендеринг каждой задачи в нашем дереве. Он будет отображать информацию о текущей задачи и отображать ее дочерние подзадачи, если таковые имеются.
Для его создания создадим новый файл src/components/ToDoItem.vue со следующим содержимым:
<template> <div class="m-10"> <h3 class="text-2xl font-semibold">{{ item.title }}</h3> <div class="flex items-center"> <img class="h-16 w-16 object-cover" src="https://source.unsplash.com/random" alt /> <div class="px-10"> <p class="text-left text-xl">{{ item.description }}</p> <ul class="list-none mx-10" v-if="item.subtasks && item.subtasks.length > 0"> <ToDoItem v-for="(child, subIndex) in item.subtasks" v-bind:item="child" :index="subIndex" :key="child.id" :parentItem="item" @deleteSubTask="deleteSubTask" /> </ul> </div> </div> <button class="py-2 px-4 mt-4 text-white font-semibold bg-blue-900 hover:bg-blue-700 rounded shadow" v-if="parentItem" @click="deleteSubTask">Delete subtask</button> <button class="py-2 px-4 mt-4 text-white font-semibold bg-blue-900 hover:bg-blue-700 rounded shadow" v-if="!parentItem" @click="deleteTask">Delete task</button> <button class="py-2 px-4 mt-4 text-white font-semibold bg-blue-900 hover:bg-blue-700 rounded shadow mx-5" type="button" @click="addSubTask">Add subtask</button> </div> </template> <script> export default { name: "ToDoItem", props: { item: { type: Object, required: true }, index: { type: Number, required: true }, parentItem: { required: false } }, methods: { deleteSubTask() { this.parentItem.subtasks.splice(this.index, 1) }, deleteTask() { this.$emit('deleteTask', this.index) }, addSubTask() { const id = Math.floor(Math.random() * 100) const task = { id, title: `Task_${id}`, description: `Ipsa odit esse cumque fugit saepe iste. Corrupti dolor repudiandae facilis alias! Molestias sapiente rerum ipsum enim doloremque, dicta excepturi rem aut.`, subtasks: [] } this.item.subtasks.push(task) } } } </script>
Основное отличие рекурсивного компонента от любых других обязательное наличие свойство name: «ToDoItem» с именем компонента и вызов самого себя в шаблоне
... <ToDoItem v-for="(child, subIndex) in item.subtasks" v-bind:item="child" :index="subIndex" :key="child.id" :parentItem="item" @deleteSubTask="deleteSubTask" /> ... <script> export default { name: "ToDoItem", ...
Так же в этом компоненте реализованы методы:
deleteTask — удаления корневой задачи
deleteSubTask — удаление выбранной подзадачи
addSubTask — метод добавления новой подзадачи
Заключение
Рекурсия не так сложна, как кажется. Это простое выполнение одного и того же блока кода снова и снова с различными входными параметрами, пока не будет достигнут базовый вариант.
Я надеюсь, что эта статья упростит понимание рекурсии и того, как создать рекурсивный компонент во Vue.js.
Спасибо за чтение и до встречи в следующем посте.
Интересный подход. Но в ToDoItem в методе deleteSubTask удаляется параметр пропса, что является плохой практикой. Т.к изменение входных параметров легко затираются обновлением любого свойства данного пропа в родителе. Да и в целом нарушает абстракцию дочернего компонента.
Вопрос как можно вложенные данные выводить через vuex