kate-form v1.2.4
Простое управление сложными интерфейсами на React/Redux
kate-form - простой способ управления данными форм и их поведением с разделением логики и отображения для React/Redux.
Полный пример использования библиотеки можно найти в репозитории kate-form-demo
Ограничения
Библиотека использует объект Proxy. Для браузеров без Proxy используется полифилл,
который ограничивает использование this.content теми элементами и их свойствами,
которые были определены при создании (kateFormInit или setData('', ...) ).
Браузеры, поддерживающие Proxy можно глянуть тут https://caniuse.com/#search=proxy
Описание
Область использования
При создании сложных интерфейсов на React для:
- админ. панелей,
- систем управления задачами/проектами,
CRM систем,
и аналогичных, необходимо разрабатывать множество форм.
Каждая форма в подобном интерфесе состоит из данных, элементов которые отображают и изменяют эти данные (input, select, checkbox и прочих), а также логики поведения самих элементов - изменении их атрибутов (видимость, доступность, визуальные характеристики и прочих) в зависимости от данных или действий пользователя.
Например:
- по клику на кнопку "Отправить уведомление" появляется поле ввода "e-mail"
- в зависимости от прав пользователя становится доступной кнопка "Редактировать"
при отличии значения поля "Пароль" от значения поля "Повторите пароль" выводится надпись "Пароли не совпадают"
Для управления данными формы и их вводом есть, например, популярное решение - redux-form, которое хранит лишь данные формы. При его использовании, все характеристики и логика поведения элементов формы описывается либо в самих элементах либо внутри render. При таком подходе и отображение и логика идут вперемешку. В больших формах со сложной логикой это может приводить к неудобствам как разработки, так и поддержки.
Данная бибилиотека создана для удобства описания поведения форм, с помощью разделения логики и отображения:
помимо данных (значения) элемента формы, в
reduxstoreхранится и набор параметров, описывающих его визуальные характеристики,- за отображение элемента формы отвечает отдельный компонент,
- предоставляется простой способ манипулирования как данными, так и характеристиками элементов.
Пример использования
Рассмотрим два примера, предложенных выше:
- по клику на кнопку "Отправить уведомление" появляется поле ввода "e-mail" или скрывается, если оно уже отображено
- при отличии значения поля "Пароль" от значения поля "Повторите пароль" выводится надпись "Пароли не совпадают"
Элементы формы определеяются простым массивом, где мы указыаем их тип и связываем с нужными обработчиками событий:
const elements = [
{
type: Elements.BUTTON,
title: 'Send notification',
onClick: this.showEMail,
},
{
id: 'email',
type: Elements.INPUT,
placeholder: 'e-mail',
hidden: true,
},
{
id: 'password',
type: Elements.INPUT,
placeholder: 'Password',
inputType: 'password',
onChange: this.checkPasswords,
},
{
id: 'password2',
type: Elements.INPUT,
placeholder: 'Retype password',
inputType: 'password',
onChange: this.checkPasswords,
},
{
id: 'passwordsMatchText',
type: Elements.LABEL,
title: 'Passwords match',
},
];А обработчики определяем как методы компонента и используем в них поле content
для доступа к свойствам элементов формы, используя заданные в списке
id элементов как имена полей.
showEMail = () => {
this.content.email.hidden = !this.content.email.hidden;
}
checkPasswords = () => {
if (this.content.password.value !== this.content.password2.value) {
this.content.passwordsMatchText.title = 'Passwords do not match';
} else {
this.content.passwordsMatchText.title = 'Passwords match';
}
}Просто, быстро, удобно и читаемо!
При таком подходе возникает вопрос о расположении различных элементов на странице. В примере выше - они описаны простым массивом, а значит будут выводится единообразно, друг за другом, что, в некоторых случаях недостаточно.
Для решения этого вопроса можно сделать вспомогательные элементы, которые будут регулировать расположение других элементов на станице.
Код обработчиков остается таким-же, т.к. content рекурсивно находит
нужные элементы по id.
const elements = [
{
type: Elements.GROUP,
layout: 'horizontal',
elements: [
{
type: Elements.BUTTON,
title: 'Send notification',
onClick: this.showEMail,
},
{
id: 'email',
type: Elements.INPUT,
placeholder: 'e-mail',
hidden: true,
},
],
},
{
type: Elements.GROUP,
elements: [
{
id: 'password2_1',
type: Elements.INPUT,
placeholder: 'Password',
inputType: 'password',
onChange: this.checkPasswords,
},
{
id: 'password2_2',
type: Elements.INPUT,
placeholder: 'Retype password',
inputType: 'password',
onChange: this.checkPasswords,
},
{
id: 'passwordsMatchText',
type: Elements.LABEL,
title: 'Passwords match',
},
],
},
];Концепция
Принцип работы kate-form очень простой:
- есть набор
компонентов- компонентов элементов, которые используются в формах, с уникальным именами для каждого - в самой форме определяется набор элементов, с указанием имени нужного компонента в поле type
- Компонент
KateFormвыполняет рендер элементов, подставляя нужный компонент в соответствии с типом, передавая значение остальных полей какprops.
Таким образом достигается разделение отображения и логики:
- за отображение отвечают
компоненты, - за логику формы отвечают набор элементов и их взаимодействие в классе компонента формы.
Работа с библиотекой
Полный пример использования библиотеки можно найти в репозитории https://github.com/romannep/kate-form-demo
Установка
npm install kate-form --saveПодключение приложения
Для работы kate-form требуется redux.
Для хранения состояния необходимо подключить reducer из kate-form
в корневом reducer-e:
import { reducer } from 'kate-form';
const rootReducer = combineReducers({
...
'kate-form': reducer,
...
});Набор компонентов передается через KateFormProvider в корневом для использующих
их форм компоненте.
import { KateFormProvider } from 'kate-form';
...
<KateFormProvider components={components} t={t} [logRerender]>
<App />
</KateFormProvider>
...Без передачи компонентов kate-form будет использовать свой минимальный набор
встроенных.
Помимо компонентов в KateFormProvider передается функция перевода t которая будет
доступна внутри каждого компонента. Без указания функции будет использована
функция по умолчанию, просто возвращающая полученный параметр.
const t = param => param;Для отладки производительности можно указать опциональный параметр logRerender.
При этом в console будут выводится сообщения о рендере каждого элемента.
Подключение компонента формы
Компонент формы необходимо подключить к kate-form, используя HOC функцию
withKateForm(FormComponent, formPath, subElementsPath= 'elements', kateFormPath = 'kate-form'), где
FormComponent- подключаемый компонентformPath- путь к данным формы вreduxstoreотносительно всех данныхkate-formsubElementsPath- имя поля вложенных элементов в групповых компонентах. Необязательный параметр, по умолчанию'elements'.kateFormPath- путь к даннымkate-formвreduxstore. Необязательный параметр, по умолчанию'kate-form'
import { withKateForm } from 'kate-form';
const kateFormPath = 'formLayout';
class FormLayout extends Component {
...
}
export default withKateForm(FormLayout, kateFormPath);Фукнция withKateForm передает в props следующие параметры:
kateFormInit(formElements)- метод первоначальной установки элементов формыkateFormContent- объектcontentдля работы с элементами формыsetData(path, value)- метод прямого изменения данных формы, гдеpath- строка - путь к данным - индексы массива, имена полей объектов через.data- данные формыsetValues(obj)- метод, для каждого{ key: data }в переданом в параметре объекта ищет элемент сid==keyи устанавливает полеvalueравномуdatagetValues()- метод, который переберает все элементы формы имеющие полеvalueи возвращает объект где ключами будутidэлементов, а значениями - значения полейvaluekateFormPath- полученный в параметре путь к форме
При первоначальном определении данных формы их необходимо установить в redux store.
Для этого используется метод kateFormInit.
Для удобства работы с данными формы объект kateFormContent можно сохранить в
свойство класса
Для вывода формы в render необходимо использовать компонент KateForm с параметром
path - пути к данным формы относительно всех данных kate-form
import { ..., KateForm } from 'kate-form';
class FormLayout extends Component {
constructor(props) {
super(props);
const { kateFormInit, kateFormContent } = this.props;
const elements = [
{
type: 'button',
title: 'Show/hide email',
onClick: this.showEMail,
},
{
id: 'email',
type: 'input',
placeholder: 'e-mail',
hidden: true,
},
...
];
kateFormInit(elements);
this.content = kateFormContent;
}
showEMail = () => {
this.content.email.hidden = !this.content.email.hidden;
}
...
render() {
const { kateFormPath } = this.props;
return (
<KateForm path={kateFormPath} />
);
}
}Жизненный цикл
Работать с объектом content для доступа к элементам формы
прямо в конструкторе не получится: метод kateFormInit устанавливает
элементы формы условно в следующем цикле событий javascript (см детали реализации redux).
Отследить момент инициализации данных, а следовательно
момент начала возможной работы с content можно с помощью
метода react компонента componentDidUpdate.
В общем случае (при изменении state родительских компонентов)
этот метод может быть вызван не один раз, поэтому логично дополнить
компонент функцией shouldComponentUpdate.
shouldComponentUpdate(nextProps) {
return this.props.data !== nextProps.data;
}
componentDidUpdate() {
// do some after init stuff
}При обновлении данных корневого элемента будет естественно перерисована и этот компонент - т.е. метод componentDidUpdate будет вызан повторно. При неохоимости выолнить некоторые действия только разово, можно прибегнуть к setTimeout:
constructor() {
...
...
setTimeout(() => {
// do somethng with content
}, 0);
}Компоненты
Компоненты это набор React компонентов, которые представляют собой элементы формы, которые используются для рендера.
const label = ({ title, ...props }) => (
<span {...props}>{title}</span>
);
const components = {
...
'label': label,
}В форме мы можем использовать данный копмонент следующим образом:
constructor(props) {
...
const elements = [
...
{
type: 'label',
title: 'Some label',
style: { color: '#FF0000'}
}
];
...
}kate-form выполняет рендер компонента по его имени, указанном в поле type,
передавая остальные поля как props.
Данный пример будет эквивалентен
<span style={{ color: '#FF0000' }} >Some label</span>В props компонента, помимо данных элемента передаются еще следующие параметры:
setData(subPath, value)- метод изменения данных, гдеsubPath- путь к данным относительно самого элемента иvalue- значение, которое нужно установить.path- полный путь к самому элементу.t- функция для переводов
Компонент для поля ввода в минимальном ввиде может выглядеть так:
const input = ({ setData, value, ...props }) => {
const change = (e) => {
setData('value', e.target.value);
};
return (
<input onChange={change} value={value || ''} {...props} />
);
};Компонент для вывода группы элементов, где элементы группы находятся в поле elements
const elements = [
...
{
type: 'group',
elements: [
{
type: 'button',
title: 'Send notification',
onClick: this.showEMail,
},
{
id: 'email',
type: 'input',
placeholder: 'e-mail',
hidden: true,
},
]
}
...
]в минимальном ввиде может выглядеть так
const group = ({ path, elements, ...props }) => {
return (
<div>
{
elements.map((item, index) => (
<div key={index}>
<KateForm path={`${path}.elements.${index}`} />
</div>
))
}
</div>
);
};Для исключения использования строк в качестве идентификаторов компонентов, а также для исключения кофликтов имен при использовании разных наборов компонентов логично использовать набор констант:
Компоненты:
const label = ({ title, setData, t, ...props }) => (
<span {...props}>{t(title)}</span>
);
const Elements = {
LABEL: Symbol('label'),
...
};
const components = {
[Elements.LABEL]: label,
...
};Форма
import { ..., Elements } from 'kate-form';
...
constructor(props) {
...
const elements = [
...
{
type: Elements.LABEL,
title: 'Some label',
style: { color: '#FF0000'}
}
];
...
}Детали устройства библиотеки
Компонент формы
Данные каждой формы подключаются в redux по определенному пути относительно
всех данных kate-form, чтобы можно было использовать несколько различных форм.
Для доступа к данным формы и методу их изменения компонент формы подключается к
redux следующим образом
import { getSetData, ... } from 'kate-form';
const kateFormPath = 'formLayout';
class FormLayout extends Component {
...
}
const mapStateToProps = (state) => {
return {
...
data: state['kate-form'][kateFormPath],
};
};
export default connect(mapStateToProps, {
...
setData: getSetData(kateFormPath),
})(FormLayout);При таком подключении в props в поле data у нас актуальное состояние данных формы,
а в поле setData - метод изменения данных.
В чистом виде, метод для изменения данных принимает путь к данным
и значние для установки.
Путь - path - строка, с именами полей объекта или индексами массива через ..
Для примера в описании, установка в объекте с id == email поля hidden равным false
вызов этого метода будет иметь вид:
setData('0.elements.1.hidden', false)Структура элементов может быть сложной, со множеством уровней вложенности,
поэтому для удобства kate-form предоставляет объект content,
где можно удобно обратитья к нужному элементу по его id:
content.email.hidden = falseДанные формы и логика
При первоначальном определении данных формы их необходимо установить в redux store,
т.к. kate-form для рендера берет данные от туда. Для этого используется метод
setData;
Если компонент предоставляет собой только форму, это можно сделать в
методах constructor или componentWillMount.
constructor(props) {
super(props);
const { setData } = this.props;
const elements = [
...
];
setData('',elements);
}Для получения объекта content для удобной работы с элементами формы используется
функция getContent. Фнукция создает Proxy объекты для доступа к данным,
поэтому ей необходимо передать два метода - получения и установки данных.
import { ..., createContent } from 'kate-form';
...
class FormLayout extends Component {
constructor(props) {
super(props);
const { setData } = this.props;
...
this.content = createContent(this.getData, setData);
}
getData = () => this.props.data;
...
}Функция getContent подразумевает, что в элементах - группах их вложенные элементы
хранятся в массиве в поле elements. Если вложенные элементы хранятся в поле с
другим названием, его необходимо передать третьим параметром.
this.content = createContent(this.getData, setData, 'subElements');Для обработки значений формы можно использовать объект-массив data, а также
функцию getValues, которая переберет все элементы формы имеющие поле value
и вернет объект где ключем будет id элемента, а значением - значение его поля value
import { ..., getValues } from 'kate-form';
...
class FormLayout extends Component {
...
logValues = () => {
console.log(getValues(this.props.data));
}
}Функция getValues подразумевает, что в элементах - группах их вложенные элементы
хранятся в массиве в поле elements. Если вложенные элементы хранятся в поле с
другим названием, его необходимо передать вторым параметром.
getValues(this.props.data, 'subElements')Рендер формы
Для рендера формы используется компонент KateForm, которому в качестве параметра
передается путь к redux store относительно данных kate-form по которому находятся данные формы.
import { ..., KateForm } from 'kate-form';
const kateFormPath = 'formLayout';
...
class FormLayout extends Component {
...
render() {
return (
<KateForm path={kateFormPath} />
);
}
}Компонент KateForm работает следующим образом:
- если по переданному
pathнаходится массив, вызывается рендерKateFormдля каждого элемента массива с добавлением вpathего индекса - если по переданному
pathнаходится объект, вызывается рендер компонента согласно значению поляtype.