Создание диаграммы Ганта с помощью React с использованием Next.js

Диаграмма Ганта — это инструмент управления проектами для координации сложных графиков для больших или нескольких команд. Использование диаграммы Ганта на основе JavaScript имеет несколько преимуществ по сравнению с диаграммами Ганта на основе электронных таблиц, например:

  • Простая интеграция в существующие панели управления проектами.
  • Поделитесь своей диаграммой в Интернете.
  • Настройка диаграммы в соответствии с вашими потребностями.

В предыдущей статье мы описали, как создать базовый компонент диаграммы Ганта с возможностью перетаскивания с помощью ванильного JavaScript. В этой статье мы создадим такую ​​же базовую диаграмму Ганта с React, используя Next.js.

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

Базовая диаграмма Ганта React, которую мы создадим с помощью Next.js, будет иметь следующие функции:

  • Пользователь может выбрать период планирования, выбрав дату начала и дату окончания.
  • Мы сможем добавлять, удалять и редактировать задачи.
  • Продолжительность задач можно добавлять и удалять, и мы сможем редактировать их с помощью перетаскивания.

Когда вы закончите, у вас будет диаграмма Ганта, похожая на показанную ниже.

Вы можете найти код для заполненной диаграммы Ганта Next.js в нашем репозитории GitHub.

Начиная

Мы начнем этот проект с клонирования следующего репозитория GitHub для запуска диаграммы Ганта Next.js.

Папка .vscode содержит некоторые настройки VS Code и рекомендации по расширению, а также настройки для ESLint и Prettier. ESLint и Prettier были добавлены и настроены для форматирования кода при вставке и сохранении. Если вы не используете VS Code в качестве IDE, вам необходимо настроить их для своей IDE. Как только вы это сделаете, вы можете удалить эту папку.

Стартовый репозиторий содержит все компоненты, необходимые для создания диаграммы Ганта. Вам нужно будет создать некоторое состояние и завершить код для компонентов. Используемый код аналогичен нашей предыдущей статье, в которой объясняется, как создать стандартную диаграмму Ганта на JavaScript.

Если вы установите зависимости npm install и запустите локальный сервер разработки с помощью npm run dev, вы увидите страницу с заголовком «Gantt Tracker».

Давайте посмотрим, как устроен начальный код диаграммы Ганта.

Добавление стилей с помощью CSS

CSS, который мы будем использовать для диаграммы Ганта, включен в начальный код. Мы использовали Styled JSX, который поставляется с Next.js, для написания компонентов с ограниченным стилем. В файле styles/globals.js есть несколько глобальных стилей. Эти глобальные стили добавляются в наше приложение в компоненте Макет в папке компонентов. Все приложение упаковано в этот компонент макета в файле _app.js, который находится в папке pages.

Теги ‹style jsx› в компонентах содержат CSS, ограниченный компонентом. Styled JSX делает это, делая имена классов уникальными. В компоненте GanttChart есть несколько переменных CSS для цвета, радиуса границы и высоты ячейки, которые мы будем использовать для диаграммы Ганта.

Вспомогательные функции, помощники, константы и данные

Файл fetchWrapper.js в папке utils содержит функцию-оболочку fetch API, которую мы будем использовать для выполнения HTTP-запросов к нашей конечной точке API. Эта функция-оболочка взята из статьи Кента С. Додда Замените Axios простой пользовательской оболочкой выборки. Мы будем использовать эту функцию-оболочку выборки для получения примера данных диаграммы Ганта, которые находятся в файле data.json в общей папке.

Файл dateFunctions.js в папке helpers содержит вспомогательные функции, которые мы будем использовать для вычисления и форматирования дат. Наконец, файл Constants.js в корневой папке содержит массив месяцев, который мы будем использовать для создания столбцов диаграммы Ганта и для заполнения выбранных элементов ввода для выбора диапазона дат нашей диаграммы Ганта.

Теперь приступим к созданию компонента диаграммы Ганта.

Добавление состояния

Во-первых, мы добавим состояние в компонент GanttChart. Мы добавим состояние для задач, продолжительность задач и временной диапазон, которые будут отображаться на диаграмме Ганта. Мы будем использовать синие горизонтальные полосы по ячейкам, чтобы показать продолжительность задач.

Импортируйте хуки useState и useEffect в GanttChart.js:

Добавьте следующие переменные состояния и сеттеры в функцию GanttChart():

Мы получим данные о задачах и задачах из файла data.json, выполнив запрос на выборку с помощью клиентской оболочки выборки. Импортируйте клиентскую функцию:

Добавьте следующий хук useEffect для получения данных:

После получения данных будут установлены задачи и состояние taskDurations. Вы сможете увидеть состояние на вкладке Компоненты React Developer Tools.

Создание компонента диаграммы Ганта

Компонент диаграммы Ганта будет состоять из семи компонентов, которые вы найдете в папке components/GanttChart. В файл домашней страницы index.js, расположенный в папке pages, мы импортируем основной файл GanttChart.js. Существует также компонент AddButton, который используется в компонентах AddTask и AddTaskDuration.

Импортируйте семь компонентов в GanttChart.js:

Теперь добавьте их в ‹div› с идентификатором контейнера Ганта:

Компоненты Grid и Settings — это оболочки, которые применяют некоторые стили CSS к своим дочерним элементам. Для каждого из компонентов деструктурируйте переданный реквизит. Например, в компоненте «Задачи» деструктурируйте задачи, setTasks и setTaskDurations следующим образом:

А также в компоненте TimeTable:

Сделайте то же самое для компонентов AddTask, AddTaskDuration и TimeRange.

Если вы сейчас запустите свой локальный сервер, вы должны увидеть следующий базовый скелет нашего приложения в окне браузера:

Создание строк задач

Компонент Tasks в настоящее время отображает три пустых строки div. Под последней строкой ‹div className="gantt-task-row"›‹/div› добавьте следующий код для создания входных данных задачи:

Сначала мы проверяем, не являются ли задачи нулевыми, потому что Next.js выполняет рендеринг на стороне сервера. Состояние задач добавляется после монтирования компонента.

Мы создаем элемент input для каждой задачи со значением, равным имени задачи. Для удаления элемента есть кнопка удаления. Мы будем использовать атрибуты данных для обновления состояния задач при редактировании или удалении задачи.

Делаем задачи удаляемыми

Теперь давайте заставим кнопки удаления работать. Сначала добавьте к кнопке удаления следующее свойство:

Теперь добавьте следующую функцию handleDelete:

Мы передаем функцию setTaskDurations, чтобы получить предыдущее значение состояния из аргумента функции. Мы получаем идентификатор задачи из атрибута данных на кнопке. Затем мы обновляем состояние задач, отфильтровывая задачу по идентификатору из состояния задач, переданного в качестве реквизита, а затем устанавливая состояние задач с помощью setTasks. Мы также обновляем состояние tasksDurations, удаляя все длительности задач, связанные с удаленной задачей, а затем обновляя состояние. Это приводит к повторному рендерингу компонента Tasks, который переходит в новое состояние.

Теперь, если вы нажмете на кнопку удаления, задача будет удалена.

Делаем задачи редактируемыми

Входные данные задачи не редактируются, давайте это исправим. Добавьте следующее свойство onChange в задачу ‹input›:

Теперь добавьте следующую функцию onChange:

Мы получаем значение ввода и идентификатор задачи. Затем мы создаем новое состояние задач, отфильтровывая отредактированную задачу, создавая новый объект задачи и затем обновляя состояние задач. Так же сортируем задачи по id, по порядку их создания. Когда мы добавим новые задачи позже в этом руководстве, мы увидим, что данный идентификатор является числом: самые последние созданные задачи имеют более высокий номер.

Теперь попробуйте отредактировать задачи на сервере разработки. Вы заметите, что поле ввода теряет фокус после добавления или удаления из него символа. Состояние задач обновляется, как вы можете видеть в своих инструментах разработки React. Ввод теряет фокус, потому что компонент Tasks перерисовывается каждый раз, когда обновляется состояние задач.

Мы исправим это с помощью двух рефов, которые позволят нам сфокусировать правильный ввод при каждом повторном рендеринге. Добавьте следующие объекты ссылки:

inputRef будет содержать массив наших входных элементов. indexRef будет содержать индекс последнего измененного ввода. Значения ref будут сохраняться после каждого повторного рендеринга.

Добавьте следующее свойство ref к задаче ‹input›:

Это установит текущее значение inputRef. Массив будет содержать ссылку на каждый входной элемент.

В функцию onChange добавьте следующую строку:

Это установит indexRef в индекс последнего отредактированного ввода.

Теперь мы можем использовать эти две ссылки, чтобы сфокусировать ввод, который редактирует пользователь. Добавьте следующий хук useEffect в компонент Tasks:

Этот useEffect будет работать при каждом рендеринге. Он установит фокус на вход, который был отредактирован последним. Теперь попробуйте отредактировать задачи на сервере разработки. Ввод будет поддерживать фокус, пока вы редактируете задачу.

Создание сетки диаграммы Ганта

Мы создадим сетку диаграммы Ганта в компоненте TimeTable. В верхней части файла TimeTable.js есть несколько объектов, в том числе ganttTimePeriod, которые мы будем использовать для динамического стиля CSS. Мы создадим сетку, используя код, аналогичный предыдущей статье, в котором использовался ванильный JavaScript. Мы будем создавать сетку построчно. Мы добавим полосы продолжительности задачи в правильную начальную ячейку, проверив свойство задачи продолжительности задачи и дату ее начала.

Существует некоторый начальный код для создания строк. Мы заполним массивы monthRows, dayRows и weekRows элементами React для сетки диаграммы Ганта, а затем отобразим их.

Замените импортированные модули следующими модулями:

Над возвратом компонента добавьте следующий цикл for:

Это перебирает количество месяцев в диапазоне периода времени и создает строки месяца, недели и дня. Значение numMonths равно двум. Мы сможем изменить это значение, когда завершим код компонента TimeRange позже в этом руководстве. Мы используем переменную месяца и метод setMonth для увеличения значения месяца в каждом цикле. Мы используем это значение и массив месяцев из нашего файла констант, чтобы добавить стилизованные элементы React с правильным названием месяца в массив monthRows.

Мы используем вложенный цикл для создания ячеек dayRow и weekRow. Мы используем методы JavaScript Date, а также функции getDaysInMonth и getDayOfWeek из нашего файла dateFunctions.

Если вы просмотрите свое приложение сейчас, вы увидите строки года и месяца, дня месяца и дня недели. Они будут расположены правильно, как только мы добавим ячейки даты для каждой задачи.

Теперь давайте создадим ячейки строки задачи и добавим продолжительность задачи. Убедитесь, что следующие функции импортированы из dataFunctions.js:

Добавьте следующий цикл for ниже того, который вы добавили:

Мы прокручиваем каждый месяц, как мы это делали в первом цикле for, который мы добавили. Мы используем вложенный цикл for для создания ячеек. Атрибуты data-task и data-date добавляются в каждую ячейку. Мы будем использовать их, когда добавим функцию перетаскивания к длительности задач. Для каждой ячейки мы сопоставляем состояние taskDurations и добавляем элемент ‹div› продолжительности задачи в качестве дочернего элемента ячейки, если идентификатор задачи ячейки и дата начала соответствуют свойствам продолжительности задачи и начала. Ширина элемента длительности задачи определяется путем вычисления продолжительности задачи с помощью функции dayDiff и последующего умножения ее на 100% ширины ячейки с помощью CSS.

Теперь вы увидите строки задач и продолжительность задач в своем приложении.

Возможность удаления длительности задач

Теперь давайте сделаем длительность нашей задачи удаляемой, нажав кнопку удаления, когда длительность задачи находится в фокусе. Добавьте следующее свойство к элементу длительности задачи, который представляет собой ‹div›, который возвращается, когда сопоставляются taskDurations (taskDurations.map((el, i) =› {):

Теперь давайте создадим функцию deleteTaskDuration ниже двух циклов for:

React SyntheticEvent и идентификатор длительности задачи передаются в функцию deleteTaskDuration. Если нажата клавиша Удалить или Возврат, новый объект состояния создается путем фильтрации удаленной задачи из состояния taskDurations, и состояние обновляется.

Добавление функции перетаскивания к длительности задач

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

Состояние taskDurationElDraggedId будет отслеживать идентификатор длительности задачи, которая в данный момент перетаскивается. Теперь добавьте следующую функцию ниже функции deleteTaskDuration:

Эта функция будет обрабатывать установку состояния taskDurationElDraggedId.

Теперь измените возвращаемый элемент длительности задачи ‹div› на следующее:

Мы устанавливаем для свойства draggable элемента ‹div› длительность задачи значение true. Конечно, нам не нужно это свойство для работы перетаскивания, но оно полезно для UX, поскольку создает «призрачное» изображение элемента длительности задачи, прикрепленного к курсору мыши при его перетаскивании. Затем мы вызываем функцию handleDragStart и передаем идентификатор длительности задачи в качестве аргумента, когда начинается перетаскивание длительности задачи. Мы также сделали уменьшение непрозрачности длительности задачи при перетаскивании.

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

Добавьте следующее свойство в ячейку строки задачи, которая имеет свойство data-task:

Мы добавляем атрибут onDrop, чтобы определить обработчик события drop для каждой ячейки в зоне перетаскивания сетки, которая является областью ячеек задачи. Удаление элемента в ячейке вызовет вызов функции onTaskDuration. Давайте определим функцию onTaskDurationDrop. Добавьте следующие строки ниже функции handleDragStart:

Мы предотвращаем добавление длительности задачи поверх длительности другой задачи, проверяя, имеет ли целевая ячейка атрибут перетаскивания. Затем мы обновляем состояние taskDuration. Сначала мы находим задачу, которую перетащили, и удаляем ее из копии состояния taskDurations. Затем мы создаем новую продолжительность задачи и добавляем ее в скопированное состояние. Мы используем это скопированное и измененное состояние для обновления состояния.

Если вы сейчас попытаетесь перетащить продолжительность задачи, вы увидите, что курсор превратится в значок «не разрешено». Это связано с тем, что перетаскивание элемента внутри другого элемента по умолчанию отключено. Чтобы удалить это поведение по умолчанию в нашей зоне перетаскивания, добавьте следующее свойство к элементу ‹div› с идентификатором gantt-time-period-cell-container:

Теперь, когда перетаскивание работает, давайте запустим форму «Добавить задачу».

Делаем форму «Добавить задачу» функциональной

В компоненте AddTask форма имеет элемент ввода с обработчиком события onChange, который обновляет задачу с локальной переменной состояния. Отправка формы вызовет функцию handleSubmit. Нам нужно завершить эту функцию, обновив состояние задач с помощью переданного реквизита setTasks.

Установите новое состояние задач, добавив следующий установщик состояния над setTask(‘’):

Мы создаем новую задачу и добавляем ее в копию состояния, newState, а затем возвращаем ее из setState, чтобы заменить состояние новым состоянием.

Чтобы определить новый идентификатор задачи, мы используем метод сокращения, чтобы найти наибольший текущий идентификатор задачи, а затем добавляем к нему единицу, чтобы создать идентификатор для новой задачи. Значения идентификатора задачи — это числа, представляющие порядок, в котором они были созданы. Задачи отсортированы по их идентификатору.

Теперь вы сможете добавлять задачи на диаграмму Ганта. Когда вы добавляете задачу, диаграмма Ганта перерисовывается, отображая новое состояние задач.

Доводим до ума форму «Добавить продолжительность задачи»

Компонент AddTaskDuration частично создан для вас, как и компонент AddTask. Нам нужно перебрать состояние переданных задач и заполнить поле «Какие задачи» ‹select›. Нам также необходимо обновить состояние taskDuration с помощью переданного в setTaskDurations установщика состояния.

Форма имеет три ввода: ввод «выбрать» для выбора задачи, для которой предназначена продолжительность задачи, и два ввода даты для выбора даты начала и окончания продолжительности задачи. Состояние формы управляется с помощью трех локальных переменных состояния: task, startDate и endDate, а также их установщиков состояния. Функция onChange обрабатывает обновление локального состояния при изменении входного значения.

Во-первых, давайте заполним задачи ‹select› input элементами ‹option› для каждой задачи. Добавьте следующий код внутрь ввода ‹select› с идентификатором select-task:

Теперь вы увидите, что меню «выбрать» заполнено всеми названиями задач. Если вы добавите задачу, вы увидите, что она добавлена ​​в меню «выбрать» по мере повторного рендеринга компонента GanttChart.

Чтобы функция handleSubmit заработала, добавьте к ней следующие строки ниже e.preventDefault():

Если задание не выбрано, возвращаемся. Затем мы создаем новую продолжительность задачи, используя локальное состояние и отметку времени для идентификатора. Затем мы устанавливаем состояние `taskDurations`, используя `setTaskDurations`, возвращая новый массив из переданной функции.

Теперь вы сможете добавлять новые длительности задач на диаграмму Ганта.

Делаем «Трекер периода» функциональным

Чтобы завершить код для компонента TimeRange, нам нужно задать каждому элементу ‹select› правильное значение, используя переданное состояние timeRange. Затем нам нужно обновить это состояние в функции-обработчике onChange.

Замените реквизиты значений четырех элементов ‹select› следующими реквизитами значений:

Теперь добавьте следующие строки в функцию onChange после строки const {value, id} = e.target;:

Когда значение одного из входов ‹select› изменяется, мы обновляем состояние timeRange. Это приводит к повторному отображению компонента GanttChart, в котором отображается обновленный временной диапазон.

Мы завершили нашу базовую диаграмму Ганта, но процесс был немного утомительным и, возможно, не без ошибок. Теперь давайте создадим диаграмму Ганта, используя наш компонент Bryntum Gantt для сравнения.

Использование Бринта Ганта с Next.js

Чтобы установить Bryntum Gantt с помощью npm, вам просто нужно установить две библиотеки без внешних зависимостей. Если вы не знакомы с продукцией Bryntum, вы можете следовать руководству здесь.

Давайте импортируем компонент Bryntum Gantt. В папке компонентов создайте файл Gantt.js и добавьте следующие строки:

Мы будем использовать импортированный компонент BryntumGantt. Мы передадим файл конфигурации в качестве реквизита, чтобы определить, как выглядит диаграмма Ганта и откуда она получает данные. Вы также можете передать реф, если это необходимо.

Нам нужно импортировать компонент BryntumGantt динамически. Создайте еще один компонент с именем GanttInstance.js и добавьте следующий код:

В корневой папке проекта создайте файл ganttConfig.js и добавьте следующий код:

Мы передаем конфигурацию из ganttConfig.js в компонент Ганта. Сюда входит свойство столбцов для создания столбцов диаграммы Ганта для задач. Вы можете узнать больше об опциях конфигурации в Документации Bryntum.

Мы настраиваем проект со свойством транспорт для заполнения хранилищ данных диаграммы Ганта. Мы настроили URL-адрес для загрузки данных с помощью свойства load. Источник данных URL — это файл JSON в общей папке начального репозитория. Вы также можете настроить транспорт для синхронизации изменений данных с определенным URL-адресом. Для получения дополнительной информации вы можете прочитать следующее руководство в нашей документации: Привязка данных Bryntum Gantt.

Мы динамически импортируем компонент Ганта с ssr, установленным в false. Пока компонент загружается, мы отображаем загружаемый компонент, который возвращает тег ‹p› с сообщением «Загрузка…».

Чтобы заполнение данных диаграммы Ганта работало, нам нужно отключить [строгий режим](https://reactjs.org/docs/strict-mode.html). Установите для reactStrictMode значение false в файле next.config.js, который находится в корневой папке проекта:

Давайте создадим страницу для просмотра нашей диаграммы Бринтума-Ганта. В папке pages создайте файл с именем bryntum.js и добавьте в него следующие строки:

Мы импортируем наш компонент GanttInstance и CSS для темы Bryntum Gantt Stockholm, которая является одной из пяти доступных тем. Вы можете увидеть демонстрацию, показывающую различные темы здесь. Вы также можете создавать собственные темы. В этой демонстрации вы также можете узнать о различных компонентах диаграммы Ганта, нажав кнопку Узнать в правом верхнем углу.

Компонент GanttInstance заключен в ‹div› с классом контейнера. Мы даем ему высоту 100vh, чтобы диаграмма Ганта заполнила весь экран.

В своем приложении перейдите на маршрут bryntum, перейдя по следующему адресу: https://localhost:3000/bryntum. Теперь вы увидите диаграмму Бринтума-Ганта.

Этот базовый пример компонента Bryntum Gantt по умолчанию имеет больше функций, чем созданная нами диаграмма Ганта. Особенности включают в себя:

  • Сворачиваемые группы задач.
  • Перетаскивание задач.
  • Добавляйте, редактируйте, копируйте и удаляйте задачи. Щелкните правой кнопкой мыши задачу, чтобы увидеть всплывающее меню с этими действиями.
  • Перетаскиваемые длительности задач.
  • Изменяемая продолжительность задач. Наведите указатель мыши на левую или правую сторону длительности задачи, которая имеет зеленый цвет, пока курсор не изменится на значок изменения размера. Затем нажмите и удерживайте, перемещая мышь влево или вправо.

Попробуйте некоторые из этих функций в своем приложении.

Сравнение ванильных диаграмм JS, React и Bryntum Gantt

Давайте сравним созданную нами диаграмму Ганта с базовым примером диаграммы Ганта Бринтума, а также с ванильным JavaScript, который мы создали в предыдущей статье.

Разбить диаграмму Ганта React на разные компоненты, упорядочить код и управлять состоянием было проще, чем при использовании ванильного JavaScript. React управляет состоянием декларативно, что упрощает управление им. Стандартная диаграмма Ганта на JavaScript требовала большого количества прямых манипуляций с DOM, выбора элементов DOM и добавления к ним событий. Код обычной диаграммы Ганта на JavaScript и диаграммы Ганта в React очень похож, однако, поскольку мы использовали Next.js, нам пришлось учитывать SSR.

Диаграмму Бринтума-Ганта было легко настроить, она имеет очень настраиваемый API, о котором вы можете узнать в API docs. Вы можете увидеть все доступные функции здесь и просмотреть расширенную демонстрацию диаграммы Ганта React здесь. Расширенная демонстрация включает в себя такие функции, как выделение критических путей, выбор темы и отмена или повтор действий.

Диаграмма Бринтума-Ганта построена на чистом JavaScript/ES6+ и использует очень быстрый движок рендеринга. Его производительность велика даже с большими наборами данных. Вы можете проверить это самостоятельно: вернитесь на свой локальный сервер разработки и установите максимальный диапазон периода отслеживания в специально созданном диаграмме Ганта и добавьте новую задачу. Вы заметите, что добавление новой задачи занимает пару секунд. Это связано с тем, что нужно отобразить много ячеек. Попробуйте это и на живом примере ванильного JavaScript Ганта из предыдущей статьи. Вы заметите то же самое.

Теперь измените диапазон дат проекта на диаграмме, созданной с помощью компонента Bryntum Gantt. Сделайте диапазон дат 1000 лет, а затем добавьте задачу. Заметной разницы в производительности нет.

Следующие шаги

Есть много способов добавить или улучшить компонент диаграммы Ганта, как описано в разделе Следующие шаги нашей предыдущей статьи, где мы использовали обычный JavaScript для создания диаграммы Ганта. В отличие от стандартной диаграммы Ганта для JavaScript, вам не нужно беспокоиться о очистке данных о задаче и продолжительности задачи с помощью React.

Вы можете улучшить диаграмму Ганта, добавив проверку ввода. Управление состоянием также можно улучшить, предотвратив ненужную повторную визуализацию. Компонент TimeTable отображает таблицу, используя множество вложенных циклов. Минимизация количества вложенных циклов улучшит производительность.

Строить или покупать?

Эта статья дает вам отправную точку для создания собственной диаграммы Ганта React с использованием Next.js. Если вместо этого вы хотите купить готовое, проверенное в боевых условиях решение, которое просто работает, взгляните на некоторые примеры нашего Бринтума Ганта. Компонент Bryntum Gantt позволяет:

  • Планируйте задачи, используя зависимости и ограничения.
  • Используйте календари для проектов, задач и ресурсов.
  • Используйте повторяющиеся и фиксированные интервалы времени.
  • Настройка рендеринга и стилей.
  • Настройте взаимодействие с пользователем с помощью множества различных типов столбцов и редакторов задач.
  • Работа с обширными наборами данных и настройкой производительности.

У нас также есть форум поддержки и мы предлагаем профессиональные услуги.