Создание на Vue.js рекурсивного компонента

Spread the love

Некоторые люди говорят, что рекурсию это что то сложное и непонятное. Но мне кажется, что с точки зрения разработки программного обеспечения, это не так. Простым определением было бы то, что рекурсивная функция — это просто функция, вызывающая себя в определенной точке своего выполнения.

Спонсор поста 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.

Спасибо за чтение и до встречи в следующем посте.

Была ли вам полезна эта статья?
[16 / 4.1]

Spread the love
Подписаться
Уведомление о
guest
2 Комментарий
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Андрей
Андрей
4 лет назад

Интересный подход. Но в ToDoItem в методе deleteSubTask удаляется параметр пропса, что является плохой практикой. Т.к изменение входных параметров легко затираются обновлением любого свойства данного пропа в родителе. Да и в целом нарушает абстракцию дочернего компонента.

Алибек
Алибек
2 лет назад
Reply to  Андрей

Вопрос как можно вложенные данные выводить через vuex