Я не сторонник велосипедов. Прежде чем начать разрабатывать свое решение тривиальной на мой взгляд задачи, всегда трачу уйму времени на поиски уже существующих библиотек или модулей. И далеко не потому, что мой код заведомо будет хуже стороннего. Просто, зачем придумывать то, что уже создано, проверено и отлажено. Гораздо лучше потратить время на создание чего-нибудь нового, непридуманного доселе. Однако в этот раз мне все-таки пришлось садиться за разработку самому. В статье речь пойдет об удобной js-библиотеке, позволяющей «связывать» данные.
С чего все началось или чего не хватает Angular
Прежде всего перечислю задачи, с которыми мне пришлось столкнуться и которые не получилось решить существующими библиотеками. Примеры будут приводиться для Angular, но тоже самое в полной мере относится и к Angular Light, и к другим многочисленным библиотекам. Итак, поехали…
01) Одностороннее связывание данных
Это странно, но вот как реализовать одностороннее связывание данных я понять так и не смог. В своей работе в основном использую фреймворк Yii2, и вот вам простейшая задача: сгенерировать поле ввода (input) и связать его с блоком (div). Казалось бы, проще-простого, ан-нет: при генерации inputa, хотя в нем и установлено его значение, но angular данное значение не учитывает и берет значение «модели», которое при загрузке страницы является пустым.
Как вариант, можно было бы использовать директиву «ng-init» (или «al-init» для Angular Light), и для простейших случаев проблема, действительно, будет решена. Но есть одно но: если вы используете компоненты динамического обновления данных (такие как Editable) получается следующее: вы изменяете значение, сохраняете его => требуется обновить привязку => а при обновлении связывания, вновь берется значение из «ng-init», которое уже не является актуальным. Беда…
02) Модель-Представление-Контроллер
Сейчас в меня полетят камни, но я против использования MVC-схемы при отображении html-страниц.
Для больших и крупных проектов со сложной структурой — да, для проектов связанных с отображением графики, где для одного элемента может потребоваться несколько видов — да, но вот использование этого механизма при отображении простейших страниц — зачем? Ведь в итоге получается следующее: html-код элемента находится в одном месте, шаблон его отображения в другом, а контроллер вообще где-то в скриптах в начале страницы. В итоге вместо простой наглядной структуры получается нечто громоздкое и неповоротливое, где для того, чтобы скопировать какой-либо элемент для повторного использования в другом проекте, приходится перелопатить всю страницу, вникнуть во всю логику ее функционирования.
По моему скромному мнению, если у вас на странице есть, скажем, прикольные часики, то вся логика этого элемента, включая скрипты и стили, должна находиться в одном месте. Тогда при желании вы без труда сможете его выделить/скопировать/вставить — и вуа-ля, он точно также работает в вашем другом проекте, без всяких рысканий по скриптам и контроллерам.
03) Динамическая загрузка содержимого
Именно данная проблема и стала решающим фактором для начала разработки собственного решения. На самом деле предыдущие пункты, в принципе, можно решить через костыли и груды ненужного кода. А вот с обновлением связей после загрузки содержимого все печально.
Связывание данных в Angular, как вы знаете, производится лишь при загрузке страницы. Но что делать, если содержимое генерируется динамически или загружается через ajax-запросы, т.е. если требуется выполнить связывание уже после первоначальной загрузки? Единственный способ, который удалось обнаружить — это использование $scope.$apply() в контроллере. Казалось бы, выход найден? Отнюдь. Дело в том, что в содержимом, пришедшем с сервера и загруженном на страницу, также могут быть (и будут) элементы, требующие связывания данных. Как быть в этом случае? Не пихать же все возможные алгоритмы сайта в один большой мега-контроллер.
Или вот еще пример: как поступать, когда в загруженном содержимом присутствуют блоки, содержимое которых также загружается или генерируется динамически? Я далеко не спец в Angular, и возможно, есть способы выполнить данные задачи. Но скорей всего это будет далеко не просто и очень громоздко.
RainyJs — установка и быстрый старт
Библиотека является полностью автономной и в сжатом виде весит всего около 7Кб.
Для использования просто скачайте файл rainy.js и подключите его на свою страницу.
Первоначальное связывание данных, как и в Angular, выполняется автоматически при завершении загрузки страницы. Но также связывание данных выполняется и при ajax-загрузке содержимого, причем в этом случае обрабатывается только непосредственно загруженный фрагмент страницы.
Для «прямого» обновления связей (например, после динамического создания элемента) можно вызвать функцию rainy(elem), где в качестве параметра можно передать селектор, dom-элемент, либо даже jQuery-объект. Если вызвать данную функцию без параметров, то будет выполнено обновление связей на всей странице.
Базовые директивы связывания данных
Сейчас библиотека содержит всего 12 директив, что делает ее достаточно простой в освоении.
Основными директивами являются «rxname» и «rxdata». Первая устанавливается в элемент-источник и задает имя переменной, вторая устанавливается в элемент-приемник и указывает имя переменной, из которой требуется брать данные. Если данные требуется получать из нескольких источников, то в «rxdata» помещается список имен переменных через пробел.
Пример использования приведен ниже:
<label>Ваше имя:</label><br>
<input rxname="var01" value="World"><br>
Hello, <span rxdata="var01"></span>!
Префиксы и постфиксы в отображении
Довольно часто веб-мастерам требуется немного подкорректировать результирующее отображение (например, добавить постфикс «руб.» для введенной суммы). Для такого форматирования можно воспользоваться директивой «rxview», внутри которой можно использовать следующие шаблоны (шаблоны записываются в двойных фигурных скобках):
- {{rxname}} — наименование переменной источника, которая была изменена
- {{value}} — результирующее значение, которое было бы отображено без форматирования
- {{var01}} — значение переменной с именем «var01» (используется, если несколько источников)
Пример использования приведен ниже:
<label>Укажите процент ставки:</label><br>
<input rxname="var02" style="width:150px;" type="range">
<div rxdata="var02" rxview="Вы выбрали: {{value}}%"></div>
Отображение/Скрытие блока данных
Еще одной часто встречающейся задачей является отображение/скрытие текстового блока по флажку или по некоторому условию. Для этих целей отведена отдельная директива «rxshow». В качестве ее значения можно поместить либо наименование переменной, либо js-код условия срабатывания.
Пример использования приведен ниже:
<input rxname="var03" type="checkbox">
<label>Установи флажок для отображения блока</label><br>
<div rxdata="var03" rxshow="value">
Этот текст будет виден только при установленном флажке.
</div>
Выполнение любого кода над элементом
Следующей директивой, о которой пойдет речь, является «rxcode». Она предназначена для более сложных вариантов связывания данных и представляет из себя js-код, который выполнится в элементе-приемнике перед обновлением его содержимого. Здесь можно как переопределить устанавливаемое значение, просто вернув новое, так и вообще отменить изменение, вернув undefined.
Также данная директива используется для выполнения абсолютно любого кода над элементом. Например, изменить класс элемента или сделать его недоступным можно именно через нее. Знаю-знаю, в angular для этого отведены отдельные директивы и для изменения класса достаточно лишь установить значение атрибута. Признаюсь, и у меня тоже были мысли по этому поводу, но в итоге решил все же не плодить лишних параметров, ведь простота — залог успеха.
Внутри «rxcode» доступны следующие переменные:
- sender — dom-элемент, из которого пришло событие
- rxname — наименование переменной, значение которой было изменено
- values — массив значений переменных, указанных в rxdata
- value — значение первой переменной из массива [values]
- self — текущий dom-элемент (приемник события)
Пример использования приведен ниже:
<label>При отрицательном значении текст будет красным:</label><br />
<input rxname="var05" value="2" type="number"><br>
<div rxdata="var05" rxcode="
if(value < 0){ self.classList.add('red'); }
else { self.classList.remove('red'); }
return 'Введено значение: ' + (value||0);
"></div>
Ajax-загрузка содержимого элемента
И вот, наконец, мы подошли к самому главному, для чего, собственно, библиотека и создавалась. Здесь также всего лишь две директивы, которые можно установить в приемник: «rxajax» и «rxload».
В [rxajax] следует указать путь к скрипту, возвращающему новое содержимое, т.е. шаблон запроса. Данный запрос отправляется методом GET, а для передачи данных используется JSONP-формат. В тексте запроса можно использовать те же подстановочные шаблоны, что и в директиве «rxview».
Директива «rxload» может содержать js-код, который будет выполнен после загрузки в элемент нового содержимого. Стоит заметить, что данная директива может использоваться не только при ajax-загрузке контента, но также и для любых других элементов. Это может быть очень полезно, например, при использовании jQuery-плагинов, поскольку большинство из них «подключают себя к элементам» только при первоначальной загрузки страницы.
Пример использования «rxajax» приведен ниже:
<select rxname="var06">
<option disabled="">Выбери браузер...</option>
<option selected="" value="1">Mozilla Firefox</option>
<option value="2">Google Chrome</option>
<option value="3">Internet Explorer</option>
<option value="4">Opera ReMix</option>
</select>
<div class="bold" rxdata="var06"
rxajax="http://x-rainy.org/getajax.php?select=browser&value={{value}}"
rxview="Загрузка...">
</div>
Еще несколько примеров использования
Данная библиотека предназначена для одностороннего связывания данных, однако организовать двустороннее связывание также не является особой проблемой:
Двустороннее связывание данных:<br />
<input rxname="var51a" rxdata="var51b" /><br />
<input rxname="var51b" rxdata="var51a" /><br />
Порой флажок «Выделить все» бывает просто необходим. Теперь это проще простого:
<input type="checkbox" rxname="var44"> Выделить все<br />
<input type="checkbox" rxdata="var44"> Флажочек №1<br />
<input type="checkbox" rxdata="var44"> Флажочек №2<br />
<input type="checkbox" rxdata="var44"> Флажочек №3<br />
И еще один довольно интересный пример: при изменении цены или количества автоматически пересчитывается сумма, а при изменении суммы автоматически устанавливается цена:
<div><label>Кол-во: </label><input type="number" rxname="m_count" value="0" /></div>
<div><label>Цена: </label><input type="number" rxname="m_price" value="0"
rxdata="m_count m_summa"
rxcode="
if(rxname.toLowerCase() !== 'm_summa')return undefined;
return ((values['m_count']||0) != 0)
? (values['m_summa']||0) / values['m_count'] : 0;
"/>
</div>
<div><label>Сумма: </label><input type="number" rxname="m_summa" value="0"
rxdata="m_count m_price"
rxcode="return (values['m_count']||0) * (values['m_price']||0);"/>
</div>
Вместо заключения
Я сторонник динамично развивающихся проектов, поэтому буду рад любым советам и предложениям по улучшению кода. Единственное, учитывайте, что затраты должны быть соизмеримы с добавляемыми возможностями. Например, писать кучу строк кода для поддержки IE6 смысла особого я не вижу. Просто потому, что если потенциальный клиент до сих пор пользуется таким старьем — это точно не наш клиент.
Ссылки на внешние ресурсы
x-rainy.org — официальный сайт библиотеки
Автор: kroshanin
Очень хорошая библиотека! Всё делается быстро и красиво. Прикрутил за 20 минут и всё заработало и без ангуляра. Хотелось бы понять как создать калбэк на собственную функцию, после того как формулы из rxcode были посчитаны и результат помещён в DOM. А то без этого ajax получается недоделаным, так как невозможно проконтролировать изменение и выполнить дальнейшие обработки. как минимум например сложение табличных полей “сумма” для получения “ИТОГО” по всей таблице. Сейчас приходится запускать функцию прям в rxcode через setTimeout но это очень кривой костыль, так как нет уверенности что библиотека уже записала данные в DOM:/ ну и задержки эт уже не по феншую ajax.