Биндинги – это набор значений, которые доступны для использования в шаблоне. Они сохраняются в свойстве binding . Это авторасширяемое поле, поэтому при создании класса, производного от basis.ui.Node , или экземпляра этих классов, новое значение не перезаписывает старое, а дополняет.
Биндинг представляет из себя функцию, которая принимает единственный параметр node – владелец шаблона, для которого вычисляется значение. Результат выполнения такой функции передается шаблону. Функция не должна иметь побочного эффекта, то есть не должна что-то менять в объектах.
Функция биндинга вычисляется сразу после создания экземпляра шаблона, но только в том случае, если шаблон использует биндинг. По этой причине не важно количество биндингов: вычисляться будут только используемые.
В случае, если что-то поменялось, и нужно перевычислить значение биндинга, необходимо вызвать метод updateBind , которому передается имя биндинга. Если изменения сопровождаются событием (событиями), можно указать это в описании биндинга (для этого используется расширенная форма) и избавиться от самостоятельного вызова updateBind . В этом случае биндинг будет вычисляться (если используется шаблоном) при создании шаблона и при возникновении событий из указанного списка.
Так же в качестве описания биндига принимаются некоторые специальные значения:
объект, поддерживающий механизм binding bridge – значение отдается в шаблон как есть;
строка – используется, если возможно, преобразование сокращения, иначе значение оборачивается в basis.getter;
экземпляр basis.ui.Node – при первом вычислении экземпляр добавляется в список satellite узла, а в шаблон отдается значение его свойства element .
Сокращения
Для наиболее частых типов биндингов используется специальная запись в виде строки (сокращение), которая позволяет сократить количество кода для описания биндинга. Такие строки имеют некоторый префикс вида "название:" , а все что идет после него используется как значение.
В модуле basis.ui определены два типа сокращения:
data – предназначен для упрощения прокидывания полей из свойства data в шаблон;
satellite – предназначен для упрощения прокидывания корневого элемента сателлита в шаблон;
Управляет сокращениями объект basis.ui.BINDING_PRESET , который позволяет добавлять новые сокращения методом add при необходимости. Новые сокращения должны быть добавлены до первого объявления биндинга такого типа.
Биндинги по умолчанию
Для basis.ui.Node и basis.ui.PartitionNode определен ряд биндигов, которые доступны по умолчанию (в скобках указаны события):
state (stateChanged) – возвращает строковое значение свойства state ;
childNodesState (childNodesStateChanged) – возвращает строковое значение свойства childNodesState ;
childCount (childNodesModified) – возвращает число дочерних узлов;
hasChildren (childNodesModified) – возвращает true если есть дочерние узлы и false в ином случае;
empty (childNodesModified) – возвращает true если нет дочерних узлов и false в ином случае;
Для basis.ui.Node так же определены дополнительные биндинги:
selected (select, unselect) – возвращает true , если узел выбран ( selected == true ), иначе false ;
unselected (select, unselect) – возвращает true , если узел не выбран, иначе false ;
disabled (disable, enable) – возвращает true , если узел заблокирован ( node.isDisabled() == true ), иначе false ;
enabled (disable, enable) – возвращает true , если узел не заблокирован, иначе false ;
На досуге решил разобрать биндинг и валидацию в Java Beans Binding (JSR 295). Реализация предлагаемая JSR 295 показалась мне проще и удобнее, чем аналогичный функционал, предоставляемый в рамках JGoodies.
Ниже рассмотрю два небольших примера биндинга контролов и пример создания и использования валидатора.
Биндинг текстового поля (JTextField)
Определим простой класс, со свойствами которого будут связаны элементы формы
Процесс биндинга текстовых полей довольно прост и в комментариях практически не нуждается. Связывание двух свойств различных элементов осуществляется путем создания объекта класса AutoBinding. Этот класс синхронизирует свойства объектов, руководствуясь одной из трех стратегий:
1. READ_ONCE — синхронизация осуществляется только один раз, во время связывания полей объектов.
2. READ — целевой объект синхронизируется с источником.
3. READ_WRITE — синхронизация объектов друг с другом.
Процесс создания объектов класса AutoBinding может быть упрощен посредством использования фабрики классов Bindings
Все очевидно.
Биндинг комбобоксов (JComboBox)
Биндинг комбобоксов осуществляется в два этапа. Вначале комбобокс связывается со списком возможных значений, после чего осуществляется связывание свойства selectedItem с соответствующим свойством целевого класса.
Определим простой класс, представляющий элемент списка значений комбобокса
Добавим в класс MyBean поле, с которым будет связываться свойство selectedItem комбобокса
Создадим список значений комбобокса
Поддержку биндинга сложных Swing-компонентов осуществляет фабрика классов SwingBindings. Свяжем комбобокс со список значений
Осталось связать свойство selectedItem комбобокса с соответствующим свойством целевого класса
В качестве примера реализуем простой валидатор для рассмотренного выше элемента textField. Наш валидатор будет осуществлять две проверки — на непустоту и на максимальное количество символов.
Все валидаторы должны наследоваться от параметризованного класса Validator, где T — тип данных с которым будет осуществляться работа.
Проверкой значения занимается функция validate. В случае, если значение прошло валидацию, должен быть возвращен null. В противном случае следует возвратить объект класса Result, содержащий описание проблемы и код ошибки.
Для того, чтобы воспользоваться валидатором нужно немного имзенить код, осуществляющий биндинг
Осталось только отреагировать на события валидации
Содержание статьи
Двухсторонний биндинг данных прочно закрепился в современной фронтенд-разработке. Он позволяет избавиться от оверхеда работы с DOM’ом, сконцентрироваться на логике приложения и посильнее изолировать шаблоны от этой самой логики. Вокруг биндинга пляшет весь Angular и довольно большая часть Ember.js, да и для семейства Backbone подобные расширения стали плодиться как грибы после дождя. Но, несмотря на все удобства, и эта технология имеет свои проблемы, ограничения и, самое главное, особенности реализации в рамках конкретных фреймворков.
JavaScript позволяет построить интерактивное взаимодействие с пользователем за счет реакции на его действия визуальными событиями. Человек вводит данные в форму, нажимает кнопку «Отправить», на странице появляется индикатор загрузки, а после (предположим, что была допущена ошибка) неправильно заполненные поля подсвечиваются красным. В этот момент под капотом приложения происходит примерно следующее:
- значения полей записываются в какую-то переменную;
- переменная сериализуется в JSON и отправляется на сервер с помощью AJAX-запроса;
- DOM страницы модифицируется — добавляется (или делается видимым) индикатор загрузки;
- по окончании запроса мы видим, что статус отличен от 200, разбираем ответ;
- DOM страницы еще раз модифицируется — нужные поля отмечаются красным, а индикатор загрузки удаляется (или прячется).
Классический JS + jQuery код работал бы примерно следующим образом:
- На событие click кнопки вешается функция.
- Функция собирает значения полей и записывает в переменную.
- Далее переменная сериализуется и отправляется на сервер.
- Мы помечаем в какой-то переменной, что запрос на сервер в процессе (чтобы не реагировать на нажатия снова и снова).
- Модифицируем DOM на предмет индикатора.
- Ждем окончания запроса и разбираем ответ.
- Модифицируем DOM на предмет полей и удаляем индикатор.
WARNING
В пункте 7 есть огромный риск нарушить инкапсуляцию — мы начинаем модифицировать представление. Где должна храниться эта логика? Как избежать ее дублирования с тем, что сгенерировало страницу изначально? Обычно именно на этом месте большинство фронтенд-кода превращается в лапшу.
Двухсторонний биндинг данных избавляет нас от шагов 2, 5 и 7, попутно однозначно решая проблему инкапсуляции логики представления (или, как сейчас модно считать, избавляет представление от логики вовсе).
Представь, что у нас есть переменная entity , содержащая JS-объект. Каждое поле ввода в форме ассоциировано с атрибутом этого объекта (к примеру, с entity.name ). При этом переменная может содержать вложенный объект entity.errors , включащий список инвалидаций, который по умолчанию пуст. Таким образом, если мы хотим пометить, что поле entity.name невалидно, мы делаем entity.errors.name = ‘The field is too short’ . Также во время AJAX-запроса мы устанавливаем entity.loading в true .
Чтобы отобразить такой объект в нужную нам форму, понадобится примерно вот такой шаблон (нотация Underscore Template):
Теперь если каким-то чудом при любом изменении нашего input его значение будет автоматически попадать в entity.name и, наоборот, при изменении чего угодно в entity этот шаблон будет обновляться необходимым образом — это и будет двухсторонний биндинг данных. Наше представление полностью описывает логику отображения исходя из возможного состояния, а само приложение, не думая о DOM, это состояние меняет.
Все, что нам остается сделать, — повесить клик на кнопку и работать с переменной entity , а по ответу сервера положить ошибки валидации в подобъект errors . Гораздо проще и чище, не так ли?
К сожалению, пример выше — просто пример. В реальной жизни ему не хватает очень большого количества метаинформации, без которой подобный биндинг попросту не заработает. Чтобы все заработало в универсальном случае, нам надо разобраться хотя бы со следующим:
- Надо каким-то образом отследить изменение entity на любой глубине. А ты помнишь, что все, что у нас есть, — JavaScript?
- Мы не можем следить сразу за всеми изменениями всех переменных на странице и перерисовывать всю страницу целиком. В лучшем случае это будет просто тормозить. В худшем определенные куски DOM могут терять необходимое состояние. Изменения должны применяться максимально изолированно.
- Даже если мы как-то секционировали нашу страницу на куски, что будет, если нам нужно изменить кусок текста в DOM, не затрагивая теги? Или как, например, перерисовать (включая оборачивающие теги) два tr из десяти?
- DOM не всегда должен меняться мгновенно, что делать, если мы хотим разбавить все анимацией?
- У нас нет отображения input в entity.name . Мы вроде как представили, что он есть, но по факту — где он должен быть? В коде приложения через bind ? Или, может, его должен декларировать view, ведь обратная зависимость определяется именно там?
Для решения всех этих проблем каждый фреймворк предлагает свои костыли — уникальные решения, которые вносят ложку дегтя в такую красивую теорию. Самое время вставить наш микроскоп поглубже в каждый из них и понять, что же двухсторонний биндинг данных представляет собой на самом деле и откуда берутся некоторые, порой такие странные ограничения привычных нам инструментов.
Смотреть мы будем на три примера: * Angular — как канонический пример «нового лучшего HTML»; * Ember — как пример привязки более классической парадигмы JS к новому инструменту и, конечно, * Joosy — как живая демонстрация моего субъективного видения удобного двухстороннего биндинга.
К сожалению, никаких универсальных способов отследить любые изменения объекта в JS попросту нет. Все существующие решения накладывают ограничения на то, как с объектом производится работа. А решений существует целых два: работа через сеттеры/геттеры и внешний мониторинг.
Сеттеры/геттеры (Ember, Joosy)
Свойства, работающие через геттеры и сеттеры, — классика программирования. Все возможные типы данных оборачиваются в расширяющий их класс (тип) и дополняются методами get и set . При любом обращении к set объект генерирует событие «Я изменился!». Технически решение очень простое и, по понятным причинам, эффективное. Но вместо entity.field = ‘value’ теперь придется писать entity.set(‘field’, ‘value’) . Это хуже читается не только глазами, но и любыми средствами по работе с кодом (от Lint’а до банальной подсветки кода).
Ember
Геттеры и сеттеры являются центральным стержнем системы свойств Ember. Они позволяют не только оповещать об изменении объекта, но и подписываться на изменение конкретных полей. В базе все выглядит именно так, как было описано:
Правда, когда мы переходим к массивам, которые Ember приводит к более общим Enumerable , все становится чуть сложнее и запутаннее, так как в этом случае у нас не просто изменяются поля, но еще и изменяется размер массива. Кроме того, в Ember есть целый ряд метасвойств наподобие @each , позволяющих подписаться на вложенные поля каждого элемента массива ( @each.field ).
HISTORY
Если понаблюдать, как развиваются фреймворки, то мы увидим один забавный повторяющийся виток. Комплексные фреймворки а-ля Rails потихоньку разбиваются на множество независимых компонентов, которые можно использовать отдельно. Объекты с отслеживанием изменений — прекрасный кандидат на вынос из Ember/Joosy в независимую библиотеку с выделенным API.
Joosy
Геттеры и сеттеры для объектов в Joosy работают совершенно идентичным образом за тем лишь исключением, что в Joosy отсутствует подписка на изменение конкретного поля. Joosy не умеет следить за полями, он следит только за изменением сущности в целом (правда, событие об изменении все-таки содержит информацию о том, что же изменилось). За счет этого массивы организованы чуть проще:
Кроме этого, для хешей Joosy (мимикрируя под Ruby) дает возможность прямо объявить необходимые свойства.
В случае прямого объявления полей Joosy зарегистрирует все указанные атрибуты с помощью defineProperty, позволяя обращаться к ним напрямую, через entity.field1 . Таким образом, обращение к entity.set(‘field1’, ‘value’) будет полностью эквивалентно прямой установке entity.field1 = ‘value’ . Да, список полей придется поддерживать вручную, но конечный синтаксис будет гораздо ближе к базовому JS.
Внешний мониторинг (Angular)
Angular пошел своим путем. Вместо попыток поймать изменение атрибутов из самого объекта он ввел тотальную систему слежки за чем угодно — тот самый $watch , которым не злоупотребляет только ленивый.
Архитектура Angular вводит «цикл отрисовки», одним из шагов которого является проверка по указанному списку отслеживания — а не изменилось ли чего. К каждому элементу списка отслеживания можно прицепить одну или несколько функций, которые вызовутся, как только значение изменится. Такое решение прозрачно работает с любыми способами установки атрибута (нет нужды в set или поддерживании списка полей), но и это, увы, не серебряная пуля.
- Скорость. Если ты работал с Angular, то наверняка уже замечал, что после преодоления определенного рубежа этих самых $watch все начинает СИЛЬНО тормозить. И чем мобильнее твой клиент (а мы живем в веке мобильных технологий), тем быстрее этот рубеж случится. Производительность — первая плата за универсальность.
- Проблемы с нескалярными данными. По умолчанию проверка на изменение производится по ссылке. Это означает, что как бы ты ни менял массив или объект, изменений Angular не заметит. Одно из решений этой проблемы — дополнительный режим $watch , который пытается проверять разницу по значению. Но, во-первых, он еще больше тормозит, а во-вторых, все еще не всегда работает со сложными структурами. Здесь, однако, стоит отметить, что архитектура Angular всячески избегает отслеживания сложных структур и нескалярных данных в принципе. Насколько это в итоге удобно — решать тебе, но технических ограничений в систему работы с DOM (их мы обсудим позже) это добавляет предостаточно.
А вообще, просто попробуй поискать angular watch на StackOverflow.com, и все сразу станет на свои места.
Теперь, когда мы умеем ловить изменения объектов, самое время понять, как мы сегментируем страницу. Что именно обновляется, когда какой-то из них меняется? Понятно, что если мы просто выводим значение поля, entity.name , то поменять надо только это значение и ничего больше. Но что, если мы выводим таблицу в цикле?
Декларативные шаблоны (Angular, Ember)
Одна из причин, по которым Angular нужен «новый HTML», а Ember — Handlebars, именно в этом. Декларативное описание, которое они разбирают своим собственным внутренним парсером, дает им информацию о контекстах биндингов.
Когда мы выводим <% raw %><> <% endraw %>, Ember создает регион, который привязывается к обновлению поля first_name объекта person . Аналогичным образом работает и Angular: создает общий регион для массива и по вложенному региону для каждого элемента. Меняется массив — перерисовывается общий регион. Один из объектов внутренний.
По этой же логике работают условия ( ng_if и <% raw %><
Флагом такого подхода является девиз: «Шаблоны не должны содержать логику!» Хотя я, откровенно говоря, считаю, что это как раз следствие, а не правило. В подобную декларативную нотацию уж очень накладно было бы встраивать полноценный логический язык. А так и волки сыты, и овцы целы. И язык не нужен, и к высшей цели пришли — вроде как логика в шаблонах — это плохо?
В реальности все не так просто. Логика не может испариться из твоего приложения, где-то она все равно должна быть. И если ее нет напрямую в шаблонах, то она должна попадать в шаблон в виде состояния. Это значит, что на каждый чих нам понадобится виртуальное состояние. Обработка индикаторов загрузки, условий доступности, факта «выбранности», абсолютно каждого мелкого переключателя. К сожалению, понимание, насколько серьезна эта плата, приходит уже в конце, когда приложение начинает обрастать мелкими удобствами.
Ручное секционирование (Joosy)
Мне всегда очень хотелось остаться с любимым HAML (в вариации с CoffeeScript), но таким образом, чтобы сохранить основные возможности двухстороннего биндига. Для достижения этой цели в Joosy используется ручное секционирование. На место декларативных объявлений пришли классические хелперы, набор которых позволяет определить регион и объявить его локальные объекты (при их изменении весь регион будет обновлен).
Например, чтобы достичь поведения, похожего на ng_repeat Angular или each Ember, можно написать что-то такое:
Теперь, когда поменяется @projects или любой проект из их числа, все автоматически окажется в DOM. Обрати внимание, что смотрители регионов специально сделаны таким образом, чтобы мониторить коллекцию с полной вложенностью. Поэтому сегмент на весь блок всего один.
Кроме инлайн-блоков, Joosy умеет рендерить в виде региона классический partial (прямо как в Rails). Это даже куда более частый случай.
Такой подход приводит к тому, что регионов в Joosy обычно гораздо меньше, чем в Angular или Ember. Практика показывает, что производительности это не мешает. Зато дает возможность работать с абсолютно любым языком шаблонов, с абсолютно любой логической нотацией и вручную управлять динамической привязкой (включая возможность завязать перерисовку региона на объект, который в нем не используется), что иногда бывает ох как полезно.
Минус — обратная сторона плюса, вручную всем управлять не только можно, но и необходимо. Нет объявленных регионов — нет двухстороннего биндинга. Другая теоретическая проблема — работа с огромными регионами (1000 строк в таблице). Так как в Joosy каждый массив создает всего один регион, обновление любого объекта этого массива приведет к полной перерисовке всего региона. В этом edge-случае по умолчанию хорошо себя ведет только Ember. Joosy и Angular потребуют ручной оптимизации биндинга.
Теперь у нас есть регионы, которые ждут изменения своего набора объектов и автоматически перерисовываются. Жизнь вроде бы налаживается. Но есть еще одна проблема, которую надо решить:
Что, если наш регион не является полностью содержимым тега и его не обновить с помощью .innerHTML ?
Metamorph (Ember, Joosy)
Ember и Joosy используют одно решение. Изначально (Joosy писался параллельно с Ember) мы просто написали одно и то же. В итоге оказалось, что решение Ember очень удачно обернуто во внешнюю библиотеку, — и Metamorph, надо сказать, прекрасно справляется с поставленной задачей.
В современных браузерах Metamorph просто использует W3C Ranges, а вот в старых все куда интереснее. Регион оборачивается в два тега
Борис Сталь
Известный специалист в области фронтенд-разработки, постоянный докладчик на множестве конференций, тимлид в EM