Поиска помощи и фидбека пост.
Работа с формами, обрабатываемыми на клиенте, начала доставлять немало боли, особенно на фоне аналогичного опыта с Angular/Vue.js.
Появилась мысль облегчить решение таких задач. Основная идея была в том, чтобы сделать подход к формам более декларативным. Упростить добавление типового контрола в форму. Сделать возможность легко мапить форму на неплоские модели данных. Ну и это должно более-менее безболезненно встраиваться в существующее приложение (что-нибудь вроде bem-core + bem-components) без тотального переписывания всего и вся.
Прикручивание того же Vue.js в качестве дополнения, (если не замены) i-bem выглядело прикольно, но как-то не зашло, ибо появляются намёки на неконсистентность методологий.
Оставалось одно — свой велосипед. Важное требование — максимально простое решение. И никаких angular-like дайджестов. Идея с сеттерами во Vue ок, но лень всё это писать, можно проще.
В итоге пришёл даже не к какой-то программной единице, решающей проблемы, а, скорее, к подходу к их решению.
Идея в том, чтобы выделить три очевидных составляющих:
- Контроллер состояний.
- «Форма».
- Компоненты «формы».
Контроллер состояний хранит стейты и общается с компонентами.
Компоненты — это блоки, непосредственно работающие с данными. Инпут — компонент. Он может и отражать изменения данных, и сам их инициировать. Блок с текстом также может быть компонентом, отображающим какую-то часть стейта.
Форма описывает стейт и привязывает компоненты к нему. Причём привязывается на уровне BEMHTML, пробрасывая в компонент имя стейта и путь к нужным данным в нём. Никаких поисков компонентов для установки значений или подписки на их события в форме нет.
В качестве шины событий с данными — каналы (events__channels
). Именно каналы. У каждого стейта свой канал. В каждом канале два типа событий: read и write. В идеале компоненты подписываются на первое и генерируют второе. Контроллер состояний — наоборот. В данных события летит путь (dot-separated), по которому произошло изменение и новое значение. Компонент подписывается на все read-события своего стейта. Он сам решает, на какие реагировать, а какие дропать (по пути изменения в стейте). Два типа событий заметно всё упрощают за счёт того, что в общем случае компоненты не общаются между собой. Всё общение между компонентами происходит через контроллер стейтов.
До сих пор это всё дозревало в голове. Наконец удалось выкроить вечер и на коленке проверить жизнеспособность идеи. Всё это вылилось в пока ещё очень сырой контроллер стейтов и пару примеров.
https://github.com/gwer/e-state
В example.blocks есть форма (my-form
), обёртка для инпутов (my-input
) и составной контрол (editable-list
). В данном случае составной контрол сделан проксирующим: не имеет собственного стейта и лишь правильно привязывает свои контролы к стейту внешней формы. Но можно писать и аналогичные контролы с собственным стейтом. Единственное, что следует помнить — если форма имеет несколько экземпляров, привязанных к разным стейтам, имя стейта для такой формы должно быть уникально для каждого экземпляра (например, генерить на основе this.generateId()
).
То есть один раз описывается обёртка над инпутом, а дальше для добавления в форму новых полей требуется минимум изменений.
Клиентский JS формы:
eState.init(this.stateName, {
f1: '1111',
f2: '2222',
nested: {
first: '',
second: '',
}
})
Разметка формы:
{
block: 'my-input',
js: {
stateName,
model: 'f1',
},
},
{
block: 'my-input',
js: {
stateName,
model: 'f2',
},
},
{
block: 'my-input',
js: {
stateName,
model: 'f2',
},
},
{
block: 'my-input',
js: {
stateName,
model: 'nested.first',
},
},
{
block: 'my-input',
js: {
stateName,
model: 'nested.second',
},
},
Можно заметить, что к полю f2
привязываются сразу два инпута. При изменении содержимого одного из них содержимое будет меняться и в другом.
Написать аналогичные обёртки для основных контролов — не должно быть проблемой.
Компоненты могут быть более сложными. Можно завязывать их на несколько путей в стейте и вешать на эти пути любую логику. Сейчас примеры представляют собой эдакий аналог ng-model
, но можно без проблем сделать, например, аналоги ng-bind
, ng-show
/ng-hide
.
Так вот, к чему всё это. Наверняка кто-то уже подходил к этой проблеме. Есть ли похожие подходы или решения? Какие незамеченные мной проблемы есть у описанного подхода? Может, есть идеи, как это можно улучшить?