Синтаксис шаблонов

Синтаксис шаблонов Wasaby является XHTML-совместимым. Используя шаблоны, вы можете в декларативной форме связывать данные контрола с элементами DOM. Для шаблонов мы используем собственное расширение файла — WML (Wasaby Markup Language). Необходимым условием валидности шаблона является наличие единственного корневого элемента.

<!-- WML -->
<div>
   <!-- контент шаблона -->
</div>

Конструкция шаблонизатора

Шаблонизатор Wasaby распознаёт конструкции, созданные с помощью синтаксиса Mustache (двойных фигурных скобок). Выражение этих конструкций можно составлять из переменных, находящихся в области видимости шаблона, а также из синтаксических конструкций языка JavaScript.

<!-- WML -->
<div>
   {{ выражение }}
</div>

При обработке шаблона вычисляется выражение конструкции, его значение экранируется и, как следствие, выводится на странице как простой текст (не HTML). Таким образом обеспечивается защита от XSS-уязвимостей.

Существуют и другие конструкции, которые будут разобраны далее в рамках руководства.

Вывод текста

По умолчанию значение конструкций выводится на страницу как текст. В следующем примере показано как вывести в шаблон значение переменной text.

<!-- WML -->
<div title="{{ text }}">
   {{ text }}
</div>

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

Вывод JsonML

Значение конструкции можно вывести как HTML. Для этого необходимо использовать Controls/decorator:Markup. Контрол принимает данные формата JsonML, а перед вставкой выполняет санитизацию небезопасных тегов.

<!-- WML -->
<Controls.decorator:Markup value="{{ json }}" />
// TypeScript
import { Control, TemplateFunction } from 'UI/Base';
import * as template from 'wml!My/Control';

export default class extends Control {
    protected _template: TemplateFunction = template;
    json: '[["p", {"version":"2.0"}, ["a", {href: "https://saby.ru"}, "https://saby.ru"]]]';
}

Примечание

Для добавления пользовательской верстки используйте только Controls/decorator:Markup. Если данное решение не удовлетворяет потребностей, то необходимо обратиться к ответственному.

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

<!-- WML -->
<div>&#8381;
</div>

Результат:

<div>
   ₽
   ₽
</div>

Использование JavaScript

Конструкции шаблонизатора поддерживают работу с выражениями языка JavaScript: арифметические операции, операторы сравнения и условные операторы, использование переменных из области видимости шаблона.

<!-- WML -->
<div>
   <div>{{true}}</div>
   <div>{{ 25 }}</div>
   <div>{{ 24 + 8 }}</div>
   <div>{{ 'hello' }}</div>
   <div>{{ "Hello" + ", " + "Wasaby!" }}</div>
   <div>{{ ({'a': 5, 'b': 'blue'}).a }}</div>
   <div>{{ ['days','color'][1] }}</div>
   <div>{{ number + 1 }}</div>
   <div>{{ bar && number }}</div>
   <div>{{ bar ? "hidden" : "visible" }}</div>
   <div>my-class {{ bar ? number }}</div>
   <div>{{ number > 20 }}</div>
</div>

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

В примере выше использован тернарный оператор:

<!-- WML -->
<div>my-class {{ bar ? number }}</div>
В JavaScript такой синтаксис оператора считается некорректным, правильно использовать выражение вида test ? consequent : alternate.
Однако в шаблонах Wasaby Framework тернарный оператор может использовать без alternate-части, тогда в качестве alternate будет использоваться:
  • пустая строка, если оператор используется в тексте, атрибуте тега, атрибуте компонента с префиксом attr; или атрибуте/опции с именами class и style.
  • undefined, для всех остальных случаев (опций компонента).
Пример:
<!-- WML -->
<UIModule.Component 
       
      <!-- В качестве alternate-части использется undefined -->
      option="{{ hasOption ? someOption }}" 
       
      <!-- В качестве alternate-части используется пустая строка -->
      attr:class="{{ hasClass ? 'class-for-component' }}">
   <ws:content>
       
      <!-- В качестве alternate-части используется пустая строка -->
      <div class="pre--{{ divClass ? 'class-for-div' }}--post">{{ hasText ? someText }}</div>
   </ws:content>
</UIModule.Component>

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

<!-- WML -->
<div>
    {{ var name = 'Wasaby'; "Hello" + name }}
    {{ console.log('debug') }}
    {{ if (bar) number }}
</div>

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

Условия

Директива ws:if

Директива <ws:if> позволяет добавлять html-разметку при выполнении условия, описанного в атрибуте data. Значением атрибута является конструкция шаблонизатора. Когда атрибут принимает значение true, в DOM вставляется содержимое блока <ws:if>.

<!-- WML -->
<div>
   <ws:if data="{{ someOpt === 'saby' }}">
      <a href="https://saby.ru/">saby.ru</a>
   </ws:if>
</div>

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

Не рекомендуется к использованию

Директива имеет короткую форму записи — атрибут if, который можно назначить любому html-тегу в шаблоне. Смысл атрибута идентичен атрибуту data. Когда атрибут принимает значение true, в DOM вставляется и содержимое тега, и сам тег.

<!-- WML -->
<div>
   <a href="https://saby.ru/" if="{{ someOpt === 'url' }}">saby.ru</a>
</div>

Директива ws:else

Директиву <ws:if> можно дополнить блоками <ws:else>. Для всех блоков, кроме последнего, допускается создание условия в атрибуте data.

<!-- WML -->
<div>
    <ws:if data="{{ someOpt === 'saby.ru' }}">
        <a href="https://saby.ru/">saby.ru</a>
    </ws:if>
    <ws:else data="{{ someOpt === 'wi' }}">
        <a href="/">wi.sbis.ru</a>
    </ws:else>
    <ws:else>
        <a href="https://online.sbis.ru.ru/">online.sbis.ru</a>
    </ws:else>
</div>

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

Между блоками <ws:if> и <ws:else> запрещается встраивание разметки. Это приводит к синтаксической ошибке.

Область видимости директивы

Директивы <ws:if> и <ws:else> не создают новую область видимости переменных (см. Подробнее о шаблонах), поэтому переменные родительского шаблона доступны к использованию в любом из блоков директивы.

Циклы

Директива ws:for

С помощью директивы <ws:for> можно реализовать добавление фрагментов разметки в цикле. Для описания выражения цикла используют атрибут data, а тело цикла описывают в содержимом блока <ws:for>.

В атрибуте data допускается организовывать итерации по целочисленному диапазону, объектам, массивам и экземплярам классов Types/entity:Model или Types/entity:Record. Для каждого типа данных используется собственная форма записи.

Итерация по целочисленному диапазону или элементам массива:

<!-- WML -->
<div>
   <ws:for data="i in number">
      {{i}};
   </ws:for>
</div>

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

Итерация по свойствам объекта или элементам массива:

<!-- WML -->
<div>
   <ul>
      <ws:for data="key, value in food">
         <li>{{ key }} is {{ value }};</li>
      </ws:for>
   </ul>
</div>

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

Не рекомендуется к использованию

Директива <ws:for> имеет короткую форму записи — атрибут for, который можно назначить любому html-тегу в шаблоне. Смысл атрибута идентичен атрибуту data.

<!-- WML -->
<div>
   <ul>
      <li for="goods in food">{{ goods.name }} is {{ goods.color }};</li>
   </ul>
</div>
См. демо-пример на CodeSandbox

Область видимости директивы

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

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

По имени

Для добавления в шаблон контрола можно применять специальный синтаксис, где имя контрола — это имя тега. Если в имени встречается символ "/", тогда при добавлении в шаблон его заменяют на символ точки.

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

<!-- WML -->
<div>
   <!-- Встраивание контрола -->
   <Spoiler.Module />
    
   <!-- Встраивание контрола из JS библиотеки-->
   <Demo.foo:Module />
</div>

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

Директива ws:partial

Для добавления в шаблон контрола или другого шаблона можно применять директиву <ws:partial>. Директиве в атрибут template передают имя контрола или шаблона (с указанием плагина wml!).

<!-- WML -->
<div>
   <!-- Встраивание шаблона -->
   <ws:partial template="wml!Demo/OtherTemplate"/>
    
   <!-- Встраивание контрол -->
   <ws:partial template="Spoiler/Module"/>
</div>

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

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

<!-- WML -->
<div>
   <!-- Значение атрибута вычисляется динамически -->
   <ws:partial template="{{ name }}"/>
</div>

Передача опций

Каждый контрол или шаблон в исходном wml-файле создает изолированную область видимости, в которой недоступны переменные родительского шаблона. Чтобы иметь возможность работы с родительскими переменными, необходимо передать их в качестве опций нужному контролу или шаблону.

Сделать это можно двумя способами:

  1. Опция описывается на атрибуте тега контрола или шаблона:
<!-- WML -->
<UIModule.Control option="Значение">
  1. Опция описывается отдельным тегом внутри тега контрола или шаблона. Имя данного тега обязательно должено начинаться с префикса ws:
<!-- WML -->
<UIModule.Control>
   <ws:option>
      <!-- Значение -->
   </ws:option>
</UIModule.Control>

В качестве значения опции допустимо использовать текст (всё, что не является тегом и комментарием), конструкцию локализации ( {[ ... ]} ) и/или mustache-выражение ( {{ ... }} ). Для опции на атрибуте тип по умолчанию — строка, для опции отдельным тегом — контентная опция. В случае, если значение состоит только из mustache-выражения, то значение будет иметь тот тип, который получился в результате вычисления содержимого mustache-выражения.

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

Описывать опцию двумя способами одновременно недопустимо, компилятор WML выдаст соответствующее предупреждение.

Задание типа опции

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

  1. Задать атрибут type на теге опции:
<!-- WML -->
<UIModule.Control>
   <ws:option type="array|boolean|function|number|object|string|value">
      <!-- Значение -->
   </ws:option>
</UIModule.Control>
  1. Значение опции обернуть в директиву типа данных:
<!-- WML -->
<UIModule.Control>
   <ws:option>
      <ws:Type>
         <!-- Значение -->
      </ws:Type>
   </ws:option>
</UIModule.Control>

Где ws:Type — одна из доступных директив типов данных: ws:Array, ws:Boolean, ws:Function, ws:Number, ws:Object, ws:String, ws:Value.

Явное задание одного и того же типа двумя способами является избыточным — компилятор WML выдаст соответствующее предупреждение.

Явное задание разных типов двумя способами является конфликтным — компилятор WML выдаст соответствующую ошибку.

Директивы типов данных

Директивы типов данных позволяют контролировать тип опций при описании опций контрола или шаблона. Директивы ws:Boolean, ws:Number, ws:String используют соответствующие стандартные обертки типов JavaScript — Boolean, Number, String.

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

ws:Array

С помощью данной директивы возможно декларативное описание массива.

<!-- WML -->
...
   <ws:option>
      <ws:Array>
         <!-- Элементы массива -->
      </ws:Array>
   </ws:option>
...

Элементами массива могут быть только директивы типов данных, в том числе ws:Array.

Задание атрибутов на директиве недопустимо!

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

Пример. Следующий фрагмент:

<!-- WML -->
...
   <ws:option>
      <ws:Array>
         <ws:Array>
             <ws:String>first</ws:String>
         </ws:Array>
         <ws:Array>
             <ws:String>second</ws:String>
         </ws:Array>
      </ws:Array>
   </ws:option>
...

эквивалентен JS-коду:

option = [
   ["first"],
   ["second"]
];
ws:Boolean

С помощью данной директивы заданное значение приводится к логическому типу.

<!-- WML -->
...
   <ws:option>
      <ws:Boolean>
         <!-- Значение -->
      </ws:Boolean>
   </ws:option>
...

В качестве содержимого директивы допустимо использовать либо текст (логический литерал — true|false), либо mustache-выражение.

Задание атрибутов на директиве недопустимо!

Пример. Следующий фрагмент:

<!-- WML -->
...
   <ws:option>
      <ws:Boolean>
         {{ !_value }}
      </ws:Boolean>
   </ws:option>
...

эквивалентен JS-коду:

option = Boolean(!_value);
ws:Function

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

<!-- WML -->
...
   <ws:option>
      <ws:Function arg1="Аргумент1" arg2="Аргумент2">
         <!-- Путь к функции -->
      </ws:Function>
   </ws:option>
...

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

P:L

, где P — физический путь к библиотеке/модулю (разделитель — "/"), L — логический путь к функции (разделитель — ".").

Пример:

UIModule/directory/library:Helpers.func

На директиве допустимо задание атрибутов — аргументов, с которыми будет происходить вызов указанной функции.

ws:Number

С помощью данной директивы заданное значение приводится к числовому типу.

<!-- WML -->
...
   <ws:option>
      <ws:Number>
         <!-- Значение -->
      </ws:Number>
   </ws:option>
...

В качестве содержимого директивы допустимо использовать либо текст (числовой литерал), либо mustache-выражение.

Задание атрибутов на директиве недопустимо!

Пример. Следующий фрагмент:

<!-- WML -->
...
   <ws:option>
      <ws:Number>
         {{ _number }}
      </ws:Number>
   </ws:option>
...

эквивалентен JS-коду:

option = Number(_number);
ws:Object

С помощью данной директивы возможно декларативное описание объекта.

<!-- WML -->
...
   <ws:option>
      <ws:Object>
            <!-- Свойства объекта -->
      </ws:Object>
   </ws:option>
...

В качестве содержимого директивы допустимо указывать только другие опции — теги с префиксом ws:; описание и типизация свойств директивы ws:Object происходит аналогично опциям контрола и шаблона.

На директиве допустимо задание атрибутов — также свойств директивы.

Описывать одну опцию двумя способами одновременно недопустимо, компилятор WML выдаст соответствующее предупреждение.

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

Пример. Следующий фрагмент:

<!-- WML -->
...
   <ws:option>
      <ws:Object a="123">
          <ws:b>
              <ws:Number>456</ws:Number>
          </ws:b>
      </ws:Object>
   </ws:option>
...

эквивалентен JS-коду:

option = {
    "a": "123",
    "b": Number(456)
};
ws:String

С помощью данной директивы заданное значение приводится к строковому типу.

<!-- WML -->
...
   <ws:option>
      <ws:String>
         <!-- Значение -->
      </ws:String>
   </ws:option>
...

В качестве содержимого директивы допустимо использовать текст, конструкцию локализации и/или mustache-выражение.

Задание атрибутов на директиве недопустимо!

Пример. Следующий фрагмент:

...
   <ws:option>
      <ws:String>
         Hello, {{ _userName }}
      </ws:String>
   </ws:option>
...

эквивалентен JS-коду:

option = String("Hello, " + _userName);
ws:Value

С помощью данной директивы тип значения никак не модифицируется (аналог типа any).

...
   <ws:option>
      <ws:Value>
         <!-- Значение -->
      </ws:Value>
   </ws:option>
...

В качестве содержимого директивы допустимо использовать текст, конструкцию локализации и/или mustache-выражение.

Задание атрибутов на директиве недопустимо!

Приведение типов

В случаях, когда тип не задан явно, компилятор WML попытается вывести тип самостоятельно.

Приведение опции к типу контентной опции

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

Контентной опцией является:

  1. Опция контрола или шаблона, тип которой не задан явно, а содержимое — текст (!);
  2. Опция контрола или шаблона, тип которой не задан явно, а содержимое — текст, HTML-теги, контролы или шаблоны;

Контентная опция content может быть задана явно с соответствующим именем:

<!-- WML -->
<UIModule.Control>
   <ws:content>
        <div>text<div>
   </ws:content>
</UIModule.Control>

или неявно, что эквивалентно примеру выше:

<!-- WML -->
<UIModule.Control>
    <div>text<div>
</UIModule.Control>

Смешивать неявно заданную контентную опцию content и явно заданные опции недопустимо:

<!-- WML -->
<UIModule.Control>
   <div>text<div>
   <ws:option>
      <span>text<span>
   </ws:option>
</UIModule.Control>

Подробнее о контентных опциях контрола читайте здесь.

Приведение опции к типу ws:Array

В случае, когда внутри опции используется несколько директив типов данных, опция приводится к типу ws:Array, а его элементы — содержимое опции.

Пример:

<!-- WML -->
...
   <ws:option>
      <ws:Boolean>
            ...
      </ws:Boolean>
      <ws:Number>
            ...
      </ws:Number>
   </ws:option>
...

эквивалентен следующему:

<!-- WML -->
...
   <ws:option>
      <ws:Array>
          <ws:Boolean>
                ...
          </ws:Boolean>
          <ws:Number>
                ...
          </ws:Number>
      </ws:Array>
   </ws:option>
...
Приведение опции к типу ws:Object

Одним из непростых на первый взгляд является приведение типа опции к ws:Object. Рассмотрим пример с полной записью свойств:

<!-- WML -->
...
   <ws:option>
      <ws:Object a="aaa">
          <ws:prop1>
              <ws:Object b="bbb">
                  <ws:prop2>
                        ...
                  </ws:prop2>
              </ws:Object>
          </ws:prop1>
          <ws:prop3>
                ...
          </ws:prop3>
      </ws:Object>
   </ws:option>
...

Данный фрагмент аналогичен следующему JS-коду:

option = {
    a: "aaa",
    prop1: {
        b: "bbb",
        prop2: "..."
    },
    prop3: "..."
}

Директиву ws:Object можно отбросить, а атрибуты, находящиеся на директиве, можно задать на опции:

<!-- WML -->
...
   <ws:option a="aaa">
      <ws:prop1 b="bbb">
          <ws:prop2>
                ...
          </ws:prop2>
      </ws:prop1>
      <ws:prop3>
            ...
      </ws:prop3>
   </ws:option>
...

Следует понимать, какую структуру будет иметь результирующий объект в таком формате записи, который приведен в примере выше.

Передача атрибутов

Для встроенного контрола/шаблона используется другой синтаксис для обозначения атрибутов — префикс attr. Описанные таким образом атрибуты будут добавлены на корневой DOM-элемент.

В следующем примере показано, что на корневой DOM-элемент будет добавлен атрибут class со значением myClass.

<!-- WML -->
<Spoiler.Module attr:class="myClass"/>
/* CSS */
.myClass {
   color: red;
}

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

Встроенный шаблон

С помощью директивы <ws:template> вы можете объявить встроенный шаблон — фрагмент вёрстки, предназначенный для локального использования в рамках родительского шаблона. Встроенный шаблон объявляют до момента его использования, обычно в начале файла. В атрибуте name задают имя.

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

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

<!-- WML -->
<div>
   <ws:template name="myTemplate">
      <p>Список разделов</p>
      <ul>
         <ws:for data="key, value in links">
            <li><a href="{{value}}">{{key}}</a></li>
         </ws:for> 
      </ul>
   </ws:template>
   ...
   <ws:partial template="myTemplate" />
</div>

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

Обработчик события внутри встроенного шаблона и добавление шаблона через цикл ws:for

В настоящий момент при использовании директивы ws:for есть ограничение. Не поддерживает вывод встроенного шаблона, где используется обработчик события.