Мне всегда было интересно, почему кажется, что так много разработчиков любят преподавать то, что они знают, в обучающих статьях или видео. Я подумал: Разве их время не было бы лучше потратить на работу с клиентами или на создание одного из собственных побочных проектов. Теперь я пишу здесь, по сути, учебное пособие. Так зачем я это делаю? Я делаю это, потому что (1) акт артикуляции кода в учебной статье поможет мне закрепить концепции в моем собственном мозгу, (2) возможно, эта информация может быть полезна кому-то другому, (3) потенциальная поучительная критика (не быть застенчивым) в разделе комментариев, и (4) я хочу уйти от фрилансера и перейти на постоянную работу (мое портфолио здесь), люди, работающие со специалистами по трудоустройству, говорят, что ведение блога помогает.

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

Пожалуйста, соберите эту канбан-доску с нуля без предварительно созданных компонентов, таких как «react-trello»…

  1. Создайте список из 4 досок с 2 элементами по умолчанию в каждом списке.
  2. Стилизуйте доски в соответствии с предоставленным png и улучшайте его.

3. Создайте функционал для добавления задачи в каждый список.

4. Создайте кнопки и функции для перемещения карточек из одного списка в другой.

5. Сохраняйте карточки в браузере.

Хватит преамбулы, поехали.

create-react-app dnd-example

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

Избавьтесь от шаблонного кода. Вы можете отказаться от следующего:

App.js
App.css 
App.test.js 
logo.svg

В React даже безумно простые вещи будут иметь несколько компонентов, состояний и т. Д. Таким образом, при проектировании простой функции вы «рисуете рамки вокруг каждого компонента (и подкомпонента) в макете и даете им все имена». ¹ Итак, что-то нравится…

Итак, зеленый - это <Board /> компонент, оранжевый - <List />, желтый - <TaskCard />, а зеленый - <AddTaskCard />. Ваши компоненты должны следовать принципу единой ответственности в том смысле, что они должны делать только одно. Кроме того, выяснение иерархии компонентов не является загадкой, компоненты, которые находятся внутри других компонентов в макете, являются потомками тех компонентов, в которых они находятся. Начнем с <Board />

Чтобы следовать принципу «подъема состояния», мы сохраним состояние нашей Канбан-доски у ближайшего общего предка (в данном случае <Board />) всех дочерних компонентов, которым необходим доступ к состоянию. Любой компонент может иметь состояние и отслеживать собственное состояние локального компонента, но вы можете себе представить, как это может быстро усложниться. Предпочтительный способ сделать что-то - использовать компоненты с сохранением состояния, когда это необходимо, и оставлять компоненты без состояния, когда это возможно, поскольку их легче рассуждать. Сохраняя состояние в нашем <Board />component, мы сохраняем единый источник истины в самом ближайшем компоненте общего предка. Посмотрите, как мы начинаем настраивать <Board />, и мы это обсудим.

Вы помните, что мы делаем доску Канбан, которая сохраняется при обновлении, поэтому здесь на помощь приходит localStorage. Если мы не будем ее использовать, каждый раз, когда мы обновляем браузер, все карточки задач, добавленные в список, исчезнут. localStorage тоже довольно полезен, потому что вы можете использовать его как поддельную базу данных при создании прототипа приложения. В строках 9–13 вы проверяете, действительно ли объект localStorage имеет элемент данных (данные, которые будут заполнять все ваши списки / карточки задач) с именем «списки», и, если он есть, затем анализирует его (потому что localStorage данные должны храниться в строковом формате) и использовать его для установки состояния <Board />. Если его не существует, установите состояние по умолчанию (с картами по умолчанию в соответствии с требованиями), а затем установите localStorage на основе этого.

Теперь вам действительно нужно что-то отрендерить. Глядя на наше состояние, у нас есть lists массив с 4 объектами в нем, каждый из которых представляет собой список, который вы увидите при рендеринге в DOM. Вот общий шаблон:

Метод render() требуется во всех компонентах класса (компонентах с отслеживанием состояния), он читает this.props и this.state и что-то возвращает. Он чист в том, что он не изменяет состояние компонента и не взаимодействует напрямую с DOM, как это предполагается в таких методах жизненного цикла, как componentDidMount(). В любом случае, вы можете видеть прямо под рендером, который мы отображаем через массив lists, вынимая данные для каждого компонента списка, строим их и вставляем в <li>.

Единственное, что может (а может и не показаться) странным, - это синтаксис <List {...list} />. Это просто сокращение для <List somePropName=this.props.someProp />. Посмотрите вверх на объекты, которые задают начальное состояние. Возьмем, к примеру, первый:

Написание <List {...list} /> аналогично написанию <List title={this.state.lists.title} id={this.state.lists.id} card={this.state.lists.cards} />, поэтому вы можете видеть, как он устанавливает имена свойств, которые вы передаете ключам пар ключ / значение объектов. Чтобы повторить, мы отображаем массив в state, называемый lists, захватывая каждый объект в массиве, и вы можете видеть, что каждый объект list имеет три пары значений ключа (или свойства), причем ключи имеют имена title, id и cards. Теперь, когда вы используете оператор распространения при отображении на state платы для заполнения <List /> компонентов, подобных этому <List {...list} />, вы просто распределяете (таким образом, оператор распространения) в компонент списка, например <List title={list.title} id={list.id} cards={list.cards} />, поэтому вы просто сохраняете некоторый синтаксис. Это более чистый способ написания, если вы знаете, что происходит.

Теперь, когда у вас есть массив из <List /> компонентов с правильными значениями, переданными в них через реквизиты, все, что вам нужно сделать, это вернуть их. А это могло выглядеть так:

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

Во-первых, давайте посмотрим, как мы добавляем карточку задачи, которая еще не находится в состоянии по умолчанию.

addTaskCard принимает два параметра (taskText, listNumber), потому что мы должны знать как содержание задачи, так и список, которому она будет назначена. Затем мы берем версию state, которая отражает состояние реагирующего компонента, но сохраняется в localStorage, чтобы она оставалась при обновлении или при закрытии вкладки браузера и повторном открытии. Мы JSON.parse его, чтобы вывести его из строкового формата, и в строках с 6 по 10 мы создаем новый newTask объект, чтобы перейти к правильному списку. Вам может быть интересно, что такое timeId: new Date().valueOf(). Это всего лишь хитрость, позволяющая получить строку даты / времени с уникальным идентификатором, который будет выглядеть так 1556889138703. Знайте, что это всего лишь хитрость для грубого прототипирования, и он работает только в том случае, если вы создаете его одним щелчком мыши. Если бы мы сгенерировали набор newTask объектов с помощью цикла for, не получилось бы сгенерировать набор уникальных идентификаторов, но по щелчку и для приблизительного прототипа у нас все хорошо.

Чтобы завершить добавление задачи, мы помещаем ее в массив, представляющий список, из которого возникла задача, устанавливаем state и устанавливаем localStorage, чтобы они совпадали.

Теперь перейдем к перетаскиванию. (Между прочим, есть отличный пакет npm под названием react-beautiful-dnd, который по уважительной причине получает 150 тысяч загрузок в неделю. Несмотря на это, иногда полезно писать что-то с нуля для собственных способностей к работе с кодом.)

Подобно тому, как HTML встроил для вас события DOM, в React есть синтетические события, которые их используют. Введите события мыши, такие как onDragStart, onDragOver и т. Д. Каждая карточка задачи может выглядеть примерно так…

Сосредоточьтесь на onDragStart, и вы заметите, что он будет запускать метод, переданный ему через реквизиты, называемые (не очень творчески) onDragStart, поэтому давайте взглянем на этот метод, чтобы увидеть, что происходит, когда он запускается.

Вы можете видеть, что мы передаем объект event, чтобы мы могли использовать его вместе с currentTarget для получения идентификатора задачи, мы также можем получить значение fromList, потому что каждая карточка имеет свойство fromList={whatever list # here}, если вы посмотрите назад на state, вы увидите что-то вроде listNumber: 2 в качестве пары ключ / значение в каждом объекте, представляющем карту, и это было «разложено» (помните ...card)) в fromList={2}, например, когда мы сопоставили часть карты state в компоненте <List /> вот так ...

Хорошо, небольшой объезд, вернемся к onDragStart. После создания объекта dragInfo мы устанавливаем его (сохраняем) в localStorage. Так что выходит на onDragOver и onDrop. Метод onDragOver необходим (почему вы можете прочитать здесь), но он не очень интересен, поэтому мы не будем его рассматривать. Прочтите onDrop.

Помните, что метод onDrop передается от <Board /> и присутствует в каждом списке, потому что именно в него вы помещаете задачу. В верхней части onDrop вы берете из localStorage как объект, представляющий задачу, которая была перетащена из исходного списка, так и объект, представляющий текущее состояние списков. Затем в строках 10–14 мы используем немного ванильного JS, чтобы найти карту, которую вытащили из определенного списка (и удалить ее), и добавить ее в список, который она перетаскивала и отбрасывала. Это хорошо. Реагируйте, еще достаточно места для написания ванильного JS. Строки 10–14, вероятно, можно было бы написать более кратко, но это была всего лишь быстрая проверка, не стесняйтесь размещать лучшее решение в разделе комментариев, если кто-нибудь это прочитает!

Вот и все. Помните, если вы хотите поиграть с живой версией, я поместил ее на хобби-сервер Heroku, и вы также можете проверить код репо.