С момента запуска Figma API, многие люди размышляли о возможности автоматического превращения документов Figma в компоненты React. Некоторые из вас создали рабочие прототипы, а Pagedraw даже создал целый продукт!

Нам понравился ваш энтузиазм, и мы поделимся с вами собственным вариантом конвертера React, обсудим тонкие дизайнерские и технические решения, принятые при его разработке, и изложим причины, мотивировавшие его создание. Мы открываем код на GitHub. (Слишком взволнованы, чтобы прочитать эту статью хотите поиграть с нашим API прямо сейчас? Посмотрите нашу страницу разработчиков!)

Мы хотели решить две основные проблемы при создании Figma to React. Одна из них заключалась в том, чтобы дизайн для компонентов, которые мы генерируем, по возможности обитал в Figma. Как замечательно было бы обновить дизайн в Figma, а затем нажать кнопку, чтобы синхронизировать эти изменения дизайна с вашим сайтом? Мы должны были быть уверены, что обновление вашего дизайна не перезаписывало пользовательский код, написанный нами, чтобы сделать веб-сайт или приложение функциональным, из-за чего естественно сгенерированный Figma дизайн код и функциональный код, живут отдельно в аккуратных модулях.

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

Как замечательно было бы обновить дизайн в Figma, а затем нажать кнопку, чтобы синхронизировать эти изменения дизайна с вашим сайтом?

Как замечательно было бы обновить дизайн в Figma, а затем нажать кнопку, чтобы синхронизировать эти изменения дизайна с вашим сайтом?

От Figma к CSS

Первым препятствием для Figma to React было создание компонентов React, которые выглядят как дизайны, в противном случае в этом нет никакого смысла. Давайте рассмотрим пример сортируемого списка, указанный выше:

От Figma к CSS
Исходный файл Figma
Существует много разных способов воспроизвести внешний вид этого списка в HTML. Например, мы могли бы отобразить изображение всего артборда и создать компонент, который просто отрисовывает это изображение. Этот подход прост, но очень ограничен. Например, здесь практически невозможно сделать что-то интерактивное. Например, клик по кнопке для сортировки списка.

Первым препятствием для Figma to React было создание компонентов React, которые выглядят как дизайны, которые они представляют — в противном случае в этом нет никакого смысла.

Используйте абсолютное позиционирование для размещения узлов

Лучшим подходом было бы разбить дизайн на его составные части, преобразовать каждую часть в DOM-элемент, например, <div> или <img>, а затем скомпоновать эти DOM-элементы. Составление этих элементов — это процесс, называемый макетом, в котором мы указываем, где должен размещаться каждый элемент, и какого размера он должен быть по отношению к другим элементам.

Figma API обеспечивает надежную основу для определения макета. Каждый узел в документе имеет свойство absoluteBoundingBox. Используя absoluteBoundingBox, мы можем точно определить, где каждый узел в настоящее время расположен на экране, и сколько места он занимает. Исходя из этого, идея заключалась бы в том, чтобы взять каждый узел, отобразить его как элемент, а затем использовать абсолютное позиционирование CSS, чтобы разместить его на странице. Поступая таким образом, мы можем получить что-то вроде этого (обратите внимание, что фрейм все еще прикреплен к верхнему левому углу):

Абсолютное позиционирование Figma to React

Это выглядит примерно так же, как если бы мы отображали весь фрейм как изображение, однако мы уже добились большого прогресса. Теперь мы можем выбрать текст в списке, мы можем заменить текст чем-то динамическим, и мы можем присоединить события по нажатию на кликабельные элементы в сцене (например, стрелки сортировки). Несмотря на это, многие недостатки остаются. Если мы добавим много текста в это поле элемента списка, текст выйдет за пределы прямоугольника. Подобные абсолютные границы — чрезмерное ограничение. Нам нужно позволить элементам динамически изменять размер в соответствии с дизайном.

Используйте ограничения Figma для динамического изменения размера элементов

Используйте ограничения Figma для динамического изменения размера узлов

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

"constraints": {
  "vertical": "TOP",
  "horizontal": "LEFT_RIGHT" 
}

Это говорит о том, что этот конкретный текстовый элемент должен располагаться относительно верхней части родительского фрейма и должен растягиваться с ним по горизонтали, чтобы левое и правое поля сохранялись. Родительский элемент в этом случае — прямоугольник, окружающий «Элемент списка 1». Применение ограничений из поля в абсолютное позиционирование — это прямое сопоставление с атрибутами left, right, top и bottom в CSS. Это касается изменения размера и формы контейнера списка, но не изменения самого списка. Например, как мы адаптировались, если бы захотели динамически добавить другой элемент списка или, если текст, который входит в этот элемент списка, превышает размеры исходного поля?

Макет: сверху-вниз или снизу вверх?

Вышеупомянутые проблемы — это то, как сам HTML умеет решать проблемы. Если вы складываете два div-элемента один поверх другого, изменение высоты первого div автоматически приведет к отступу второго div. Такое поведение не работает в нашей ситуации, потому что все абсолютно позиционируется, и поэтому они закреплены на месте!

Укладка div элементов один поверх другого в зависимости от того, сколько контента в каждом из них — это подход, который мы будем называть макет снизу-вверх (bottom-up), то есть мы начинаем с самых нижних строительных блоков (скажем, фрагментов текста) и создаем структуру, составляя эти блоки вместе, чтобы получить форму слоев более высокого уровня. Напротив, то, что мы до сих пор делали, это макет сверху-вниз (top-down), в котором мы указываем, сколько места должен занимать самый верхний слой, а затем помещаем в него элементы, которые составляют этот высший уровень. Что, если мы создадим комбинацию подходов сверху-вниз и снизу-вверх?

Давайте рассмотрим, как мы хотим реагировать, когда контент внутри одного из наших элементов изменяется. В некоторых случаях ответ очевиден:

Макет: сверху-вниз или снизу вверх?

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

Макет: сверху-вниз или снизу вверх? Пример-1 Макет: сверху-вниз или снизу вверх? Пример-2

Что мы делаем в случаях A и B выше, если захотим добавить текст в первое поле? Решение, к которому я пришел, было прагматичным, но во многих случаях несовершенным. Основная идея заключается в том, что большинство веб-сайтов упорядочены по вертикали, поскольку соглашение о веб-сайте заключается в том, что вы скролите сверху вниз, чтобы просмотреть весь доступный контент. Учитывая это, я решил обработать дочерние элементы узла, как если бы они имели линейный порядок сверху вниз.

Если мы применим это к первому приведенному выше примеру, порядок может выглядеть примерно так:

Макет: сверху-вниз или снизу вверх? Пример-3

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

Макет: сверху-вниз или снизу вверх? Пример-4

Обратите внимание, что в приведенном выше примере результат странный, но погранично разумный. А это результат B:

Макет: сверху-вниз или снизу вверх? Пример-5

Скорее всего, это не то, что мы хотели: большинство людей ожидало бы, что изображение и другие текстовые поля останутся в верхней части. Есть много способов решить эту проблему. Мы могли бы добавить эвристику типа «элементы, которые вертикально выровнены, должны оставаться вертикально выровненными» или отметить узлы в Figma определенным образом, чтобы аннотировать, что они должны сохранять свое вертикальное положение. Попробуйте свой собственный подход, изменив код Figma to React и поделитесь своими идеями!

3-уровневый подход

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

Линейный, основанный на интервалах макет
Линейный, основанный на интервалах макет
Обратите внимание, что этот отступ может быть отрицательным. Затем мы просто устанавливаем свойство CSS margin-top для текущего элемента для этой разницы. Теперь, если один элемент изменится, другие переместятся вверх и вниз по странице, как и следовало ожидать. То же самое можно сделать с выровненными по низу элементами, рассматривая их, как отдельную группу. В результате мы можем разделить дочерние элементы любого узла на три группы:

  • Группа элементов, выровненных по верху TOP, с макетом снизу-вверх
  • Группа элементов, которые расположены в центре, SCALE или TOP_BOTTOM, расположены с абсолютным позиционированием (или макетом сверху-вниз)
  • Группа элементов, выровненных по низу BOTTOM, опять же с макетом снизу-вверх

3-уровневый подход
3-уровневый макет
Если мы применим эти идеи к сортируемому списку, мы получим полностью активный компонент, который изменяет размеры для соответствия контейнеру и адаптируется к изменению содержимого внутри себя, как показано ниже.
3-уровневый подход, прмиер
Ограничения в действии
Одна вещь, на которую надо обратить внимание в этом видео — это то, что нижний колонтитул перестает двигаться в определенном месте — другими словами, документ перестает становиться короче в фиксированной точке. Это важно для того, чтобы, например, нижний колонтитул не попадал в основное содержимое компонента. Однако это не может быть реализовано как минимальная высота элемента: по мере изменения размера текста минимальная высота должна увеличиваться, чтобы разместить дополнительный текст. Решением здесь является добавление нижнего поля к элементам, выровненным по верхнему краю (и верхнего поля к элементам, выровненным по нижнему краю), так что они прижаты к противоположному концу их родительского элемента и «поддерживают» свой родительский элемент, открытый до определенного размера.

Комбинированный подход

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

Визуализация ограничений
Визуализация ограничений
Связь между этими элементами заключается в следующем: текстовый элемент и прямоугольная область являются дочерними элементами фрейма. Текстовый элемент имеет ограничения фрейма LEFT_RIGHT и TOP, а прямоугольник имеет ограничения LEFT_RIGHT и TOP_BOTTOM. Когда фрейм сужается, текст должен сжиматься на второй строке, увеличивая его высоту. Поскольку его поля остаются постоянными, это приводит к тому, что содержащий фрейм также увеличивается, что приводит к сдвигу следующего в списке фрейма. В то же время, поскольку прямоугольник (опять же, синий) имеет ограничение TOP_BOTTOM для этого фрейма, он также должен изменять размер и становиться больше, чтобы удовлетворить это ограничение. Таким образом, мы имеем ограничение снизу-вверх, когда внутренний текст делает внешний фрейм больше, а затем ограничение сверху-вниз, когда внешний фрейм делает внутренний прямоугольник больше. Я думаю, что это одно из тех взаимодействий, где результат ничем не примечателен (это именно то поведение, которое вы ожидаете), но его реализация достаточно креативная.

Заставить список вести себя, как список

Теперь, когда у нас есть нечто похожее на список, как мы можем заставить его вести себя, как список? Например, как мы можем заставить его загружать произвольные данные? Или сортировать эти данные?

Наш конвертер React должен будет выдавать файлы с компонентами React в них. Каждый раз, когда мы меняем дизайн, мы должны по крайней мере изменить часть содержимого некоторых из этих файлов, чтобы представить изменения в дизайне. Чтобы заставить компоненты делать что-то, мы, вероятно, захотим написать код, который может взаимодействовать с сгенерированными компонентами. Наконец, код, который мы пишем, никогда не должен быть перезаписан генератором и в идеале может быть переносимым из одного компонента в другой.

Возможно, вы уже заметили, что предыдущий раздел о макете вообще не зависит от React. Вы правы — мы могли бы заставить наш конвертер генерировать чистый HTML и CSS, и он работал бы так же хорошо. Это связано с тем, что чистое преобразование фрейма Figma приводит к статическому компоненту, и React не дает никаких существенных преимуществ, когда речь заходит о создании статического сайта, кроме возможности компоновки кода.

Теперь, когда у нас есть нечто похожее на список, как мы можем заставить этот список вести себя, как список?

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

Гаджеты — куски повторно используемого кода, которые придерживаются дизайна

Первая концепция, которую я хочу представить — это гаджет (Gadget), называемый «компонентами» в коде. Гаджет — это оболочка, которая окружает любой узел в дизайне Figma и добавляет к нему часть функциональности — любой функциональности вообще. Вы можете создать или прикрепить гаджет к узлу Figma, поместив хеш-символ ('#') перед его именем.

Например, если вы хотите, чтобы фрейм вел себя как часы, вы можете назвать его #Clock. Это не сделает ничего особенного в самом файле, но приведет к тому, что этот узел станет гаджетом в нашем конвертере — это создаст файл гаджета CClock. js, который вы сможете заполнить функциональностью. Возможно, самый простой способ объяснить это — показать пример:

Пример сгенерированного кода
В этой сцене у нас есть контейнер с двумя фреймами внутри. Один фрейм содержит изображение круга, а другой — изображение квадрата. Скажем, мы хотим, чтобы оба фрейма демонстрировали какое-то обычное (но одинаковое) поведение, например, мы хотим анимировать оба вращением.

Мы можем обернуть каждый фрейм в тот же гаджет (называемый #Spinner). Это создаст настраиваемый файл кода CSpinner.js. Сгенерированная левая сторона будет ссылаться на этот компонент гаджет каждый раз, когда узел, аннотированный как #Spinner, появляется в дереве узлов. Код также передает идентификатор узла (nodeID) в гаджет, который он может использовать для поиска своего содержимого в каждом случае. Сила гаджетов заключается в том, что они могут применяться к любому узлу, поэтому содержимое узла может варьироваться от случая к случаю.

Гаджет — это оболочка, которая окружает любой узел в дизайне Figma и добавляет к нему часть функциональности — любой функциональности вообще.

Это означает, что, если мы создадим CSpinner. js, чтобы сделать контент анимированным и вращающимся, мы можем заставить любой узел вращаться, назвав его #Spinner и тем самым присоединяя к нему код гаджета. Если мы применим код анимации к CSpinner, мы получим следующее:

#Spinner в действии
Обратите внимание, что в своей функции рендера компонент CSpinner просто ссылается на компонент, который получается из getComponentById. CSpinner не знает, что он окружает — полное отделение функции от дизайна. Обратите внимание также, что после создания CSpinner. js мы никогда не перезапишем его: любые внесенные в него изменения сохраняются независимо от того, сколько раз вы регенерируете проекты.

Переменные: замена текста плейсхолдера динамическим значением

Переменные — это вторая концепция, которую мы вводим. Переменная — это просто текстовый узел, имя которого начинается с $. Такой узел по умолчанию отображает текст в дизайне, но может быть переопределен реквизитами React для отображения произвольного текста. Свойство, которое переопределяет текст, совпадает с именем узла, только без $. Например, если у меня есть узел под названием $chicken, а реквизит, входящий в этот элемент, выглядит как {chicken: «poptarts"}, тогда текст этого узла будет заменен на строку «poptarts». Вы можете отправить эти свойства вниз, обернув узлы переменными в гаджете.

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

CListItems extends React. PureComponent {
render () {
const Component = getComponentFromId (this.props.nodeId);
if (this.props.listItems) {
return this.props.listItems.map ((item) =>
<div style={{position: «relative"}} key={item}>
<Component {…this.props} item={item} />
</div>)
} else {
return <Component {…this.props} />
}
}
}

Несколько примечаний об этом фрагменте кода:

  • Мы не читаем напрямую из источника данных. Скорее мы ожидаем, что будет передан список элементов, которые уже обработаны. Причина этого станет очевидна позже.
  • Имя гаджета начинается с C, что верно для всех шаблонов гаджета, сгенерированных с помощью конвертера. Это делается для того, чтобы мы всегда могли начинать с заглавной буквы, которая представляет собой соглашение об использовании имен компонентов React.
  • Мы по умолчанию показываем, что находится в документе Figma, если нет listItems. Это рекомендуется, чтобы страница могла функционировать без необходимости предоставления каких-либо источников данных.
  • Мы можем использовать компонент — узел, который гаджет обертывает, несколько раз в функции рендеринга! Таким образом мы можем дублировать элемент списка.
  • Мы должны обернуть каждый компонент в div. Это относится к position: relative стилю, который необходим в нашем случае. Детали не так важны, но приятно, что мы можем это сделать. Обратите внимание, что вы можете так же легко прикрепить класс и стилизовать его в CSS. React фактически препятствует встроенным стилям в руководстве по стилю. Вы можете представить себе реализацию конвертера, который дает файл CSS без лишних трудностей.

Итак, почему мы не можем просто загружать источник данных непосредственно в этот компонент? Причина в том, что мы хотим создать сортируемый список, а элементы управления сортировки находятся вне этого компонента. Поскольку компонент ListItems и кнопки для сортировки по возрастанию и убыванию находятся на совершенно разных поддеревях, они могут общаться друг с другом только через общий родительский элемент. Они должны общаться через него, чтобы он был каноническим источником истины для элементов списка.

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

Загрузка пользовательских данных в список

Далее создадим общий родительский компонент:

export class CSortableList extends React. PureComponent {
state = {};

constructor (props) {
super (props);
if (!props.listSource) return;
const req = new XMLHttpRequest ();
req.addEventListener («load», () => {
const listItems = JSON. parse (req.responseText);
this.setState ({listItems});
});
req.open («GET», props. listSource);
req.send ();
}

sortAscending = () => {
if (this.state.listItems) this. setState ({listItems: this.state.listItems.slice ().sort ()});
}
sortDescending = …

render () {
const Component = getComponentFromId (this.props.nodeId);
return;
}
}

Если мы передадим listSource этому гаджету, он попытается загрузить URL-адрес, хранящийся в listSource, и сохранить полученный обработанный объект JSON в listItems. Мы также определяем две функции сортировки и передаем их, как свойства в Component. Теперь любой узел, являющийся потомком CSortableList, может вызывать эти функции сортировки, и, если мы поместим в него гаджет CListItems, он сможет отобразить список из источника данных!

Наконец, мы кратко покажем гаджет, который запускает сортировку:

export class CSortAscending extends React. PureComponent {
sort = () => {
this.props.sortAscending && this.props.sortAscending ();
}

render () {
const Component = getComponentFromId (this.props.nodeId);
return;
}
}

Этот гаджет обернут вокруг кнопки, которая запускает сортировку списка в порядке возрастания. Поскольку один из его родительских элементов представляет собой CSortableList, мы можем вызвать функцию props. sortAscending (), которая приведет к изменению состояния в CSortableList, вызову переиздания гаджета CListItems и перестановке элементов списка в нем. Давайте прикрепим все эти гаджеты к нашему оригинальному дизайну, создадим компонент CSortableList с параметром listSource, установленным в /shapes.json, и посмотрим, что произойдет:

Сортировка пользовательских данных
Сортировка пользовательских данных

Повторное использование кода

Этот конкретный пример теперь работает! Теперь, когда у нас есть этот код, легко присоединить его ко всему, что мы хотим превратить в сортируемый список, назвав узлы в Figma аналогично названиям гаджетов. Нам удалось вложить функциональность в эти гаджеты, которые можно произвольно применить к любому узлу Figma. Разве не так должны создаваться интерфейсы? Наверное, не совсем. Есть ли уроки, которые можно извлечь для изменения наших мыслей о взаимодействии дизайна и кода? Мы надеемся на это.

Разве не так должны создаваться интерфейсы? Наверное, не совсем.

Будущая работа: прототипирование, CSS Grid, Layout Grids

Несколько идей по расширению Figma to React:

  • Учитывать прототипирование ссылок, чтобы клик по элементу переводил приложение в другое состояние
  • Реализовать состояния при наведении курсора
  • Создать таблицу стилей, которая использует сетку CSS для компоновки элементов
  • Учитывать столбцы и строки макета в Figma
  • Реализация поддержки вращающихся узлов (прямо сейчас любой узел с поворотом или наклоном не будет корректно отображаться)

Заключение

Мы надеемся, что представили вам не огранённый алмаз. Мы наметили стратегию по привязке ограничений к HTML и прикреплению многократно используемого кода к дизайну. Если вы не заметили ссылку в начале статьи, мы открыли код для Figma To React на Github.

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

Не терпится построить что-то свое с нашим API? Посетите нашу страницу разработчиков для вдохновения, а также Show & Tell канал на Spectrum. Будущее за вами.

Спасибо Valerie Veteto и Carmel DeAmicis.