Базовый класс контрола

Мы руководствуемся концепцией представления интерфейса как набора контролов.

Контрол — это изолированная часть функционала, которую можно использовать повторно при разработке интерфейса. В файловой структуре он представляет собой минимум два файла: JS-модуль, где описана логика, и WML-шаблон, где описано представление.

Контролы можно вкладывать друг в друга, образуя дерево, где есть родительские и дочерние элементы. Создать новый контрол можно через композицию других контролов.

Создание класса контрола

Разработка контрола начинается с описания класса, который должен наследоваться от базового класса UI/Base:Control. Описание класса нужно поместить в отдельном JS-модуле. В основе работы с модулями мы применяем стороннюю OpenSource библиотеку.

В следующем примере продемонстрировано описание класса для контрола MyControl.

// JavaScript
define("MyControl", ["UI/Base"], function(Base) {
   var ModuleClass = Base.Control.extend({
      // логика работы контрола
   });
   return ModuleClass;
});

Также модуль контрола можно описать на TypeScript, о чем подробнее вы можете прочитать в разделе Лучшие практики.

Подключение шаблона

Неотъемлемой частью любого контрола является его визуальное отображение, которое описывают в шаблонах. В Wasaby для шаблонов создано собственное расширение файла — WML (Wasaby Markup Language). Для импорта шаблона используют RequireJS-плагин wml, и импортированный шаблон передают в свойство _template.

<!-- Template.wml -->
<div>Hello Wasaby</div>
// MyControl.js
define("MyControl", ["UI/Base", "wml!Template"], function(Base, template) {
   var ModuleClass = Base.Control.extend({
      _template: template
   });
   return ModuleClass;
});

Отображение данных

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

Ниже показано: в классе контрола объявлено свойство text, значение которого выводится в WML-шаблон.

<!-- MyControl.wml -->
<div title="{{ text }}">{{ text }}</div>
// JavaScript
define("MyControl", ["UI/Base", "wml!MyControl"], function(Base, template) {
   var ModuleClass = Base.Control.extend({
      _template: template,
      text: "Hello Wasaby!"
   });
   return ModuleClass;
});

В шаблонах можно использовать различные конструкции шаблонизатора, подробнее о которых читайте в разделе Синтаксис шаблонов.

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

Опции контрола

Опциями мы называем параметры, переданные при инициализации контрола. В шаблоне опции доступны в переменной _options, а в JS-модуле — в переменной this._options. Опции доступны только на чтение и менять их можно только через родительский контрол

В следующем примере мы инициализируем контрол MyControl и передаем в него опцию color.

<!-- Parent/Template.wml -->
<div>
   <Demo.Module color="{{ someColor }}" />
</div>
<!-- Demo/Template.wml -->
<div style="color: {{ _options.color }};">
   Hello Wasaby!
</div>
// Module.js
define("Demo/Module", ["UI/Base", "wml!Demo/Template"], function(Base, template) {
   var ModuleClass = Base.Control.extend({
      _template: template
   });
   return ModuleClass;
});

См. демо-пример на CodeSandbox

Подробнее о работе с опциями контрола можно прочитать в разделе Опции.

Жизненный цикл

Жизненным циклом мы называем процесс функционирования контрола начиная от его создания и заканчивая уничтожением его экземпляра и удаление элемента из DOM-дерева.

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

В области видимости любого хука указатель this возвращает экземпляр класса контрола. Через указатель можно получить доступ к дочерним html-элементам или контролам, вызывать методы контрола.

Схема жизненного цикла

Ниже представлен полный список хуков жизненного цикла. Зелёным цветом обозначены переопределяемые хуки, а серым — непосредственно процессы контрола.

Фаза "Создание"

_beforeMount()

Хук _beforeMount(options) вызывается до построения вёрстки контрола, что происходит единожды в рамках жизненного цикла. Хук является изоморфным: вызывается как на клиенте, так и на сервере. Построение верстки на сервере будет рассмотрено в отдельной статье.

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

Хук beforeMount() поддерживает асинхронные операции. Подробнее о передаче данных между границами процессов (при рендеринге на сервере и инициализации контрола в браузере) читайте в статье Received State. Передача сериализованного состояния.

// Module.js
define("Module", ["UI/Base", "wml!Template", "Types/source"], function(Base, template, source) {
   var ModuleClass = Base.Control.extend({
      _template: template,
      _dataSource: null,
      _beforeMount: function(options) {
         this._dataSource = options.dataSource || new source.Memory({
            idProperty: 'id',
            data: [{ id: 0 }]
         });
      }
   });
   return ModuleClass;
});

_afterMount()

Хук _afterMount() вызывается после инициализации контрола и его монтирования в DOM-дерево. Хук вызывается один раз в рамках жизненного цикла. Опции, с которыми был инициализирован контрол, доступны в переменной this._options.

В данном хуке можно выполнять манипуляции с DOM-деревом, например для измерения размеров, работать с дочерними контролами, создавать подписки на серверные события, оповещать о собственных событиях и т.д.

_afterMount: function(options) {
   console.log(this._children.myDiv.style.width);
}

Фаза "Обновление"

_beforeUpdate()

Хук _beforeUpdate(newOptions) вызывается непосредственно перед обновлением DOM-дерева. Хук является синхронным. Новые опции, с которыми контрол будет обновлён, доступны в первом аргументе хука. Старые опции контрола доступны в переменной this._options.

В данном хуке можно изменить состояние контрола до его перестроения и обновления в DOM.

_beforeUpdate(newOptions) {
   this._counters = newOptions.counters;
}

_afterRender()

Хук жизненного цикла контрола. Вызывается синхронно после применения измененной верстки контрола. На этом этапе вы получаете доступ к отрисованной верстке.

Жизненный хук используется в случае, если не подходит _afterUpdate для некоторых ускорений.

Например, если после отрисовки необходимо выполнить корректировку положения скролла (возврат на прежнее положение), это нужно делать синхронно после отрисовки, чтобы не было видно прыжка.

Control.extend({
...
   _afterRender() {
// Accessing DOM elements to some fix after render.
      this._container.scrollTop = this._savedScrollTop;
   }
...
});

_afterUpdate()

Хук _afterUpdate(oldOptions) вызывается непосредственно после обновления DOM-дерева. Первый аргумент хука хранит опции, которыми обладал контрол до обновления. Опции, с которыми был обновлён контрол, доступны в переменной this._options.

В данном хуке можно выполнять манипуляции с обновлённым DOM, например для измерения размеров, работать с дочерними контролами, оповещать подписчиков о событиях и передавать им данные.

_afterUpdate(oldOptions) {
   console.log(this._children.myDiv.style.width);
}

Фаза "Уничтожение"

_beforeUnmount()

Хук _beforeUnmount() вызывается до уничтожения контрола, что происходит единожды в рамках жизненного цикла.

В данном хуке нужно высвобождать ресурсы, которые были заняты контролом и нуждаются в закрытии или удалении. Например, здесь можно удалить ссылки или обработчики событий, отписаться от серверных событий, остановить таймеры (setTimeout, setInterval), удалить callback’и.

_beforeUnmount() {

   // Удаляем значение свойства, хранимое в памяти.
   this._headConfig = null;

   // Уничтожаем экземпляр класса, созданный в JS-коде.
   this._rangeModel.destroy();

   // Отписываемся от серверных событий.
   ServerEventBus.serverChannel('LastActivity.UserStatus').unsubscribe('onMessage', this._statusSelectedHandler);

   // Удаляем обработчик события.
   this.container.removeEventListener('beforeunload');
}

Доступ к элементам вёрстки и дочерним контролам

Важно

Задание динамически-генерируемого имени не поддерживается. То есть в опцию name можно передавать только строчку. В случае определения значения опции name с использованием оператора {{ }} например так:

<Controls.buttons:Button name="{{myChildControlName}}" />
возникнут ошибки с очищением ссылок на контролы в объекте _children.

В Wasaby изменение состояния дочерних html-элементов и контролов следует выполнять через обновление свойств родительского контрола. Значительно реже вам может потребоваться манипулировать дочерними элементами напрямую. Для этого в JS-модуле используйте объект this._children, содержащий дочерние элементы (html-элементы, контролы), которым задан атрибут name. В этом объекте название свойства соответствует имени элемента, а значением является сам элемент.

<!-- Template.wml -->
<div name="myDiv">
   <MyControl name="myButton" />
</div>
// Module.js
define("Module", ["UI/Base", "wml!Template"], function(Base, template) {
   var ModuleClass = Base.Control.extend({
      _template: template,
      _afterMount: function(options) {
         console.log(this._children.myDiv.width);
         console.log(this._children.myButton.width);
      }
   });
   return ModuleClass;
});

Обращение к дочерним контролам возможно на всех фазах кроме хука _beforeMount.

Использование кастомных событий

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

Для объявления события контрола достаточно вызывать метод _notify() вот так:

define("Demo/Module", ["UI/Base", "wml!Demo/Template"], function(Base, template) {
   var ModuleClass = Base.Control.extend({
      _template: template,
      _afterMount: function(options) {
         this._notify("myEvent");
      }
   });
   return ModuleClass;
});

Подписку на событие задают в WML-шаблоне контрола с помощью атрибута on:<имя события>. В качестве значения атрибут принимает имя метода-обработчика.

<!-- WML -->
<div>
   <Demo.Module on:myEvent="eventHandler()" />
</div>
// JavaScript
define("Parent/Module", ["UI/Base", "wml!Parent/Template"], function(Base, template){
   var ModuleClass = Base.Control.extend({
      _template: template,
      eventHandler: function(event) {
         console.log(event);
      }
   });
   return ModuleClass;
});

См. демо-пример на CodeSandbox

Механизм событий можно использовать для передачи данных. Подробнее о работе с событиями можно прочитать в разделе Работа с событиями.

Подключение стилей

Для подключения стилей следует задать статическое свойство _styles. Значением свойства является массив с именами CSS-файлов.

В следующем примере показано создание CSS-файла и его подключение в контрол.

/* Demo/Style.css */
.myClass {
   border: 1px solid #ccc;
   padding: 5px;
}
<!-- Demo/Template.wml -->
<div class="myClass">
   <p>©1996-2019 Tensor. All rights reserved</p>
</div>
// Demo/Module.js
define("Demo/Module", ["UI/Base", "wml!Demo/Template"], function(Base, template) {
   var ModuleClass = Base.Control.extend({
      _template: template
   });
   ModuleClass._styles = ['Demo/Style'];
   return ModuleClass;
});

Встраивание шаблона в шаблон

Когда фрагмент разметки встречается в шаблонах несколько раз, его лучше вынести в отдельный шаблон и добавлять в требуемых местах. При таком подходе код получается более компактным. Более того, при редактировании содержимого отдельного шаблона изменения будут доступны сразу во всех местах его использования.

Для встраивания шаблона применяют директиву <ws:partial>.

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

<!-- Demo/Footer.wml -->
<div style="border: 1px solid #ccc; padding: 5px">
   <ws:if data="{{ showCopyright }}">
      <p>©1996-2019 Tensor. All rights reserved</p>
   </ws:if>
   <ws:if data="{{ showLink }}">
      <a href="https://tensor.ru/about/corporation">About company</a>
   </ws:if>
</div>
<!-- Demo/Template.wml -->
<div>
   <p>Tensor is a large holding company engaged in information technologies.</p>
   <ws:partial template="wml!Demo/Footer" showCopyright="{{ true }}" />
</div>
// JavaScript
define("Demo/Module", ["UI/Base", "wml!Demo/Template"], function(Base, template) {
   var ModuleClass = Base.Control.extend({
      _template: template
   });
   return ModuleClass;
});

Результат:

Tensor is a large holding company engaged in information technologies.
©1996-2018 Tensor. All rights reserved

См. демо-пример на CodeSandbox

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

Переменная showCopyright была передана в директиве <ws:partial> с помощью одноимённого атрибута (см. Demo/Template.wml). Переменная showLink отсутствует, и в результате приводится к значению false в условии для директивы <ws:if>.

Более подробно про области видимости мы поговорим в других разделах руководства.