Подробнее о шаблонах

Теоретическая справка

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

Понятие контекста выполнения

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

Понятие области видимости переменных

Область видимости переменной — это часть контекста выполнения, в которой эта переменная существует. Область видимости определяет, есть ли у вас доступ к переменной в данном контексте выполнения.

В отличие от большинства языков программирования, Javascript (до ES-2015) не имеет области видимости уровня блока (область видимости, окруженная фигурными скобками), переменные, объявленные внутри блоков принадлежат области видимости функции.

Переменные, объявленные в функции, являются локальными переменными и доступны только в этой функции или в функциях, определяемых внутри этой функции.

Понятие замыкания

Замыкания в Javascript используются довольно часто, и вы, наверняка, уже сталкивались с ними. Они позволяют делать код более выразительным и лаконичным.

Замыкание — это функция, объявленная внутри другой функции и имеющая доступ к переменным внешней (вмещающей) функции.

Замыкание имеет доступ сразу к трем областям видимости:

  • к своей собственной области видимости (переменные, объявленные внутри замыкания);
  • к области видимости внешней функции (переменные, объявленные внутри внешней функции);
  • к глобальной области видимости.

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

Простыми словами, замыкание — это функция, описанная внутри другой функции, вместе со всеми внешними переменными, которые ей доступны. Хотя иногда программисты позволяют себе терминологическую двойственность и, например, говорят «переменная берется из замыкания». Это означает – из внешнего набора переменных.

Пример замыкания в Javascript:

function showName(firstName, lastName){
   var nameIntro = "Your name is ";

   function makeFullName() {
      return nameIntro + firstName + " " + lastName;
   }

   return makeFullName();
}

// Your name is Michael Jackson.
showName("Michael", "Jackson");

Переменные, объявленные во вложенной функции, будут перебивать объявление одноименной переменной снаружи.

Следующий пример отражает этот принцип:

var name = "John";

function showName() {
   var name = "Jack";

   // Выведет Jack.
   console.log(name);
}

// Выведет John.
console.log(name);
showName();

Если имена не пересекаются, из внутренней функции будет доступна как переменная, объявленная внутри, так и переменная, объявленная снаружи. Таким образом вся программа разбивается на набор контекстов выполнения, выраженных функциями, и при выполнении программы в каждый момент времени будет доступен ровно тот набор переменных, который принадлежит данной области видимости.

Как именно технически устроены замыкания можно почитать тут.

Правила замыканий

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

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

Замыкания хранят ссылки на переменные внешней функции, а не фактические значения.

Такая интересная особенность позволяет описывать приватные переменные.

Например:

function user() {
   var name = 'Unknown';
   return {

      setName: function(value) {
         name = value;
      }, 

      getName: function() {
         return name;
      }
   }
}

var testUser = user();

// Unknown.
testUser.getName();

// Изменяем значение приватной переменной.
testUser.setName('John Smith');

// John Smith.
testUser.getName();

Побочные эффекты, связанные с замыканиями

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

Например:

function userIdGenerator(users) {
   var i;
   var uniqueId = 100;

   for (i = 0; i < users.length; i++) {
      users[i]['id'] = function()  {
         return uniqueId + i;
      }
   }

   return users;
}
var testUsers = [{ name: "Smith", id:0 }, { name: "Johnson", id:0 }, { name: "Thompson", id:0 }]
var testUsersIds = userIdGenerator(testUsers);
var firstId = testUsersIds[0];
console.log(firstId.id());

// 103.

В предыдущем примере, к тому времени, когда вызывается анонимная функция, значение i становится равно 3 (длина массива). Число 3 было прибавлено к значению переменной uniqueId, тем самым для всех элементов массива testUsers значение id стало равно 103, вместо предполагаемых 100, 101, 102.

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

Мотивация введения областей видимости в шаблоне

Задача доступа к переменным в теле шаблона

WML-шаблонизатор позволяет встраивать конструкции вида {{ выражение }} в шаблон. Внутри этих конструкций можно использовать переменные, доступ к которым есть в месте использования конструкции в шаблоне.

Шаблон:

<div class="{{ myClass }}">
   {{ myValue }}
</div>

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

Шаблон:

<ws:template name="myTemplate">
   <div class="{{ myClass }}">
      Текст
   </div>
</ws:template>
<ws:partial template="myTemplate" myClass="class1"/>

В данном примере переменная myClass определяется атрибутом в месте использования внутреннего шаблона. Внутренний шаблон myTemplate создает новую область видимости, и доступ к переменной myClass разрешается только внутри области myTemplate. Но в области тела шаблона может быть определена и своя переменная myClass. Появляется конфликт переменных с одинаковым названием.

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

Задача доступа к экземпляру компонента в его шаблоне

Шаблон может быть частью компонента, и тогда он может использоваться через компонент. Например, <Controls.list:View … /> в шаблоне будет вставлять в это место при построении результат выполнения шаблона компонента ListView.

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

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

Задача доступа к переменным в шаблоне контентной опции компонента

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

Шаблон 1:

<Controls.list:View>
   <ws:itemTemplate>
      <div >
      ...
      </div>
   </ws:itemTemplate>
</Controls.list:View>

Шаблон ListView:

<ws:partial template="{{itemTemplate}}" item="{{item}}"/>

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

Задача передачи набора переменных по месту использования шаблона

Данная задача относится к использованию паттерна HOC. Если вы не знаете, что такое паттерн HOC (компонент высшего порядка). Для дальнейшего понимания материала полезно ознакомиться с этим понятием.

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

Шаблон без HOC:

<Controls.TextBox value="{{ myValue }}"/>

Шаблон с HOC:

<Controls.NumberHOC value="{{ myValue }}">
   <Controls.TextBox/>
</Controls.NumberHOC>

Хорошо если HOC знает, какие именно опции ему передают, тогда в крайнем случае он может передать их атрибутами при использовании контентной опции:

<ws:partial template="{{ _options.content }}" value1="{{ _options.value1 }}" value2="{{ _options.value2 }}"  />

Но названия опций как правило неизвестны.

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

Введение понятия области видимости в шаблоне

Стремление уподобить область видимости как в функциях

WML-шаблонизатор генерирует из шаблонов функции, выполнение которых строит html. Внутренние шаблоны генерируются во внутренние функции. Структурно и по смыслу шаблоны и обычные js-функции очень похожи, поэтому есть стремление уподобить доступ к переменным в шаблонах доступу к переменным в Javascript.

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

Конструкции шаблонизатора как набор областей видимости

Шаблонизатор предоставляет ряд конструкций, которые выделяют свою область видимости. Эти конструкции представляют собой шаблоны как отдельные самостоятельные единицы. Момент построения верстки по каждому шаблону имеет свой контекст выполнения, и, соответственно, доступ к определенным переменным.

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

WML-файл

Строго говоря файл — это не совсем конструкция. Все тело файла является телом шаблона. Именно wml-файл является основным способом создать шаблон. Такой шаблон можно указать как шаблон для компонента, задать его в качестве контентной опции компонента, или использовать отдельно в другом шаблоне через тег ws:partial.

Контентная опция

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

Например:

<Controls.list:View>
   <ws:itemTemplate>
      <div class="myItem">
         Text of item
      </div>
   </ws:itemTemplate>
</Controls.list:View>

В данном случае itemTemplate — это опция ListView, и внутри располагается шаблон. Это полноценный шаблон, в котором можно использовать все то же самое, что и в шаблоне wml-файла, просто он объявлен как часть компонента по месту его использования.

Конструкция ws:for

Шаблонизатор позволяет выполнять цикл по данным.

Например:

<ws:for data="item in items">
   {{ item.title }}
</ws:for>

Данная конструкция проитерирует циклом переменную items и для каждого элемента items отдельно построит верстку. Внутри тега ws:for располагается отдельный шаблон, по которому и будет строиться верстка.

Дополнительно читайте здесь.

Конструкция ws:template

Шаблонизатор позволяет объявить внутренний шаблон и задать ему имя для дальнейшего использования в том шаблоне, в котором внутренний шаблон объявляется.

Например:

<ws:template name="innerTemplate">
   <div class="innerClass">
      Text
   </div>
</ws:template>
<ws:partial template="innerTemplate" />

Дополнительно читайте здесь.

Основные принципы областей видимости в шаблоне

Эти принципы должны действовать всегда и без исключений.

Доступ к переменным внутри области

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

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

Например, в области тела шаблона доступна переменная value:

{{ value }}
<div class="{{ value }}"></div>
<ws:for data="item in items">
   {{ value }}
</ws:for>
<ws:template name="myTemplate">
   {{ value }}
   <ws:for data="item in items">
      {{ value }}
   </ws:for>
</ws:template>
<Controls.list:View resultsText="{{ value }}">
   <ws:itemTemplate>
      {{ value }}
   </ws:itemTemplate>
</Controls.list:View>

Везде в данном примере value доступен, везде будет использовано одинаковое значение value.

Таким образом внутренние шаблоны обретают более весомый смысл.

Например:

<Controls.list:View>
   <ws:headerTemplate>
      Заголовок
      Общая часть: {{ value }}
   </ws:headerTemplate>
   <ws:footerTemplate>
      Заключение
      Общая часть: {{ value }}
   </ws:footerTemplate>
</Controls.list:View>

Данный пример можно переписать так:

<ws:template name="myTemplate">
   Общая часть: {{ value }}
</ws:template>
<Controls.list:View>
   <ws:headerTemplate>
      Заголовок
      <ws:partial template="myTemplate"/>
   </ws:headerTemplate>
   <ws:footerTemplate>
      Заключение
      <ws:partial template="myTemplate"/>
   </ws:footerTemplate>
</Controls.list:View>

И доступ к переменной value сохранится.

Отсутствие доступа к переменным вне области

Переменные должны быть доступны только в той области видимости, в которой определены.

Таким образом обретает смысл само понятие области видимости переменной.

Например:

<ws:for data="item in items">
   <div class="myClass">
      <ws:template name="innerTemplate">
         <div class="innerClass">
            {{ text }} and {{ value }}
         </div>
      </ws:template>
      <ws:partial template="innerTemplate" text="Текст"/>
   </div>
</ws:for>

В данном примере объявляется и используется внутренний шаблон innerTemplate, он должен быть доступен только в той области видимости, в которой объявляется. Это значит, что вне шаблона конструкции ws:for он не должен быть доступен, а внутри — доступен. Переменная value доступна из замыкания, а переменная text передана в качестве аргумента и будет доступна только в шаблоне innerTemplate.

Проводя аналогию, функции в Javascript выглядят так:

items.forEach(function(item) {
   var value = "Текст";
   function log(text) {
      console.log(text);
      console.log(value);
   }

   // Отработает без ошибок.
   log("Текст");
});

// Упадет ошибка ReferenceError: log is not defined.
log();

Перекрытие переменных в областях

Переменная во внутренней области перекрывает переменную из внешней области.

Согласно принципам замыкания:

function makeCounter() {
   var currentCount = 1;

   return function() {
   var currentCount;

   // Здесь нельзя вывести currentCount из внешней функции (равный 1).
   };
}

Переменная, объявленная во вложенной области, перекрывает переменную с таким же названием из внешней области.

Такое же правило работает и в шаблонах:

{{ value }}  <!-- выведется value0 -->
<ws:template name="template1">
   {{ value }}  <!-- выведется value1 -->
   <ws:template name="template2">
      {{ value }}  <!-- выведется value2 -->
   </ws:template>
   <ws:partial template="template2" value="value2"/>
</ws:template>
<ws:partial template="template1" value="value1"/>

Переменная value, доступная во внешней области, равна value0, но переменные во вложенных областях перекроют это значение и будут равны value1 и value2 соответственно.

Замыкание по месту объявления шаблона

Область видимости, созданная телом шаблона, замыкается на область, в которой этот шаблон был объявлен.

Предложим поясняющий пример.

Шаблон Template1:

{{ value }}

<ws:for data="count in [1,2,3,4,5]">
<Controls.InputRender>
   <ws:inputTemplate>
      <span>
         <ws:if data="{{ inputTemplate.isExtended }}">
            {{ count }} : <input type="text"/>

            {{ value }} е <!-- Числовая переменная, доступная в шаблон -->
         </ws:if>
         <ws:else>
            <input type="text"/>
         </ws:else>
      </span>
   </ws:inputTemplate>
</Controls.InputRender>
</ws:for>

Шаблон InputRender:

<ws:partial template="{{inputTemplate}}" isExtended="{{ isExtended }}"/>

В компоненте поле isExtended по умолчанию false. Когда шаблонизатор строит верстку шаблона Template1, внутренний шаблон построится по пути else.

После построения в какой-то момент поле isExtended меняется на true, и шаблон компонента InputRender перестраивается.

Значения count будет взято по порядку итерирования — 1,2,3,4,5. Так произойдет, потому что тело цикла скопирует итерируемую переменную, как это происходит в [1,2,3,4,5].forEach(...) в Javascript.

На месте использования переменной value возьмется актуальное значение из замыкания. То есть будет взято не то значение, которое было на момент построения шаблона Template1, а то значение, которое будет на момент изменения флага isExtended. Переменная value не копируется в область видимости тела цикла, так же как и в Javascript.

Область видимости, созданная телом шаблона, не замыкается на область, в которой этот шаблон был использован.

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

Например, есть Template1:

<div>
   {{ value }}
</div>

И есть Template2, в котором используется Template1:

<div>
   {{ value }}
   <ws:partial template="{{ Template1 }}" />
</div>

Переменная value существует в области видимости Template2, но это не значит, что она автоматически попадет в область видимости Template1. Это разные шаблоны, которые связаны друг с другом по принципу вызова шаблона, а не по принципу объявления.

Вот как выглядит нечто подобное в Javascript:

function Template1() {

   // Упадет ошибка SyntaxError: Invalid or unexpected token.
   console.log(value);
}
function Template2() {
   var value = "text";

   // Будет выведен текст.
   console.log(value);
   Template1();
}

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

Таким же образом нужно поступить и в шаблоне Template2:

<div>
   {{ value }}
   <ws:partial template="Template1" value="{{ value }}"/>
</div>

Передача переменных по ссылке

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

В Javascript есть примитивные переменные (number, string, boolean, null, undefined), и они передаются в функцию аргументом по значению. Объекты же передаются по ссылке.

function clickHandler(event, item) {
   if (event.target.is('.arrowUp')) {
      item.count++;
   }
}

item в данном примере — объект, переданный по ссылке. Изменение его полей приведет к изменению полей переданного в функцию объекта.

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

В шаблонах это выглядит следующим образом:

<ws:template name="myTemplate">
   <Controls.buttons:Button caption="{{ title }}" click="clickHandler(item)" />
</ws:template>
<ws:for data="item in items">
   <ws:partial template="myTemplate" title="up" clickHandler="{{ clickUpHandler }}"
   item="{{ item }}" />
   <ws:partial template="myTemplate" title="down" clickHandler="{{ clickDownHandler }}"
   item="{{ item }}" />
</ws:for>

Обработчики clickUpHandler и clickDownHandler могут изменять передаваемые ей объекты item.

Источники переменных, доступных в области видимости

Определим, какие именно переменные доступны в текущем контексте выполнения, откуда они взялись и какие приоритеты были у источников переменных. Разные источники могут содержать переменные с одинаковыми названиями и необходимо определить, каким значениям стоит отдавать предпочтение при составлении набора доступных переменных.

Доступ из замыкания

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

Место использования шаблона

Шаблон может быть использован с помощью тега ws:partial: <ws:partial template="..." /> в составе компонента: <Controls.list:View .../>

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

Например:

<ws:template name="myTemplate">
   {{ value }}
</ws:template>
<ws:partial template="myTemplate" value="text"/>

переменная value будет доступна во внутреннем шаблоне. В функциях Javascript это похоже на передачу аргументов в функцию.

Итерируемая переменная в ws:for

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

В примере

<ws:for data="item in items">
   {{ item.title }}
</ws:for>

можно заметить, что область видимости шаблона пополняется переменной item.

Доступ к локальным переменным

Переменные, объявленные в области видимости как локальные, будут доступны в этой области.

Например:

<ws:template name="myTemplate">
   ...
</ws:template>

Переменная myTemplate становится локальной для того шаблона, в котором объявлена. На эту переменную накладываются жесткие ограничения, эту переменную можно объявить только через ws:template, а использовать только в теге ws:partial в поле template.

Еще один пример локальной переменной — именованная конструкция использования шаблона или компонента.

<ws:partial name="myPartial" .../>
<Controls.list:View name="myListView" .../>

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

Например:

<ws:partial name="myPartial" .../>
<Controls.list:View ws:itemTemplate="myPartial" />

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

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

Приоритеты источников переменных

Определим порядок перекрытия переменных при составлении набора переменных, доступных в данном контексте выполнения.

Рассмотрим пример на Javascript:

// Переменная из внешней области видимости, наименьший приоритет.
var value = '1';

// Аргумент — средний приоритет.
function template(value) {

   // Локальная переменная — наивысший приоритет.
   var value = '3';
}
template('2');

Показаны приоритеты в Javascript. Формально все не совсем так, но суть ясна. Аналогичная логика принята и в шаблонах:

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

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

Особенности области видимости шаблона компонента

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

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

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

Компонент и его значения свойств по умолчанию

На компоненте можно определить набор значений по умолчанию, это выглядит так:

InputRender.getDefaultOptions = function() {
   return {
      value: '',
      selectOnClick: false,
      style: 'default',
      inputType: 'Text'
   };
};

Они будут доступны в шаблоне компонента через объект _options, если не будут переопределены значениями из переменных, полученных по месту использования компонента.

Поля и методы компонента

Любые поля и методы, доступные через экземпляр класса компонента, также доступны и в шаблоне. Унаследованные от предков поля и методы — в том числе.

Например:

var PagingComponent = Base.Control.extend({
   ...
   stateBegin: 'normal',
   clickHandler: function(e, btnName) {...}
   ...
});

Шаблон такого компонента:

...
<span class="ws-Paging__arrow__state-{{stateBegin}}" on:click="clickHandler('Begin')">
   <i class="icon-First ws-Paging__icon"></i>
</span>
...

Использует и поля (stateBegin), и методы (clickHandler).

Объект _options строится каждый раз при построении верстки, экземпляр компонента при этом не пересоздается, то есть его состояние сохраняется. Состояние компонента можно изменять и использовать в шаблоне.

Таким образом доступными переменными становятся и те, которые принадлежат экземпляру компонента, и те, которые были объявлены по месту использования компонента. Эти переменные никак не пересекаются.

Свойства и методы экземпляра (включая поле _options) образуют слой локальных переменных, который становится на ступень наименее приоритетных в текущем контексте выполнения.

Доступ к опциям в фазах жизненного цикла

Опции компонента можно переопределить и дополнить в фазах жизненного цикла beforeMount и beforeUpdate. Эти фазы случаются перед построением шаблона. beforeMount случается перед первым построением, а beforeUpdate — перед всеми последующими.

Доступ к переменным в шаблоне контентной опции компонента

Основные положения

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

Было решено, что переменные, передаваемые по месту использования шаблона не должны пересекаться с переменными из замыкания по месту объявления шаблона. Поэтому доступ к переменным, сформированным по месту использования, осуществляется через переменную, название которой совпадает с названием контентной опции.

Например, Шаблон 1:

<Controls.list:View>
   <ws:itemTemplate>
      <div class="{{ itemTemplate.item.title }}">
         <ws:partial template="{{ itemTemplate.item.content }}" />
      </div>
   </ws:itemTemplate>
</Controls.list:View>

Шаблон ListView:

<ws:partial template="{{itemTemplate}}" item="{{item}}"/>

При этом было принято еще одно решение: чтобы само название контентной опции тоже случайно не перебило какую-то опцию из замыкания, в объект вместе с переменными по месту использования также помещается переменная из замыкания, если она есть.

То есть:

<ws:partial template="{{ itemTemplate }}" />
<Controls.list:View>
   <ws:itemTemplate>
      <div class="{{ itemTemplate.item.title }}">
         <ws:partial template="{{ itemTemplate.item.content }}" />
         <ws:partial template="{{ itemTemplate.itemTemplate }}" />
      </div>
   </ws:itemTemplate>
</Controls.list:View>

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

Передача набора переменных по месту использования шаблона

Опишем решение представленной ранее задачи о передачи параметров в HOC.

Стоит задача передать набор переменных по месту использования шаблона. У нас есть объект с полями, и мы не знаем, что за поля находятся внутри. Напрашивается вариант деструктуризации объекта. Поля этого объекта просто попадут в набор переменных, вычисленных в месте использования шаблона. Предлагается такой вариант синтаксиса:

<ws:partial template="{{ myTemplate }}" scope="{{_options}}" value1="text1" value2="text2"/>

Набор переменных, передаваемых в шаблон myTemplate, будет включать value1, value2 и все поля из объекта _options.

При этом логично предположить, что переменные, объявленные явно (value1 и value2), приоритетнее и могут перезатереть поля, взятые в объекте _options.

Атрибут scope позволяет просто расширить набор переменных, задаваемых по месту использования, это не должно быть переопределение всех доступных переменных области видимости целевого шаблона.

Например:

{{ value }}
<ws:template name="myTemplate">
   {{ value }}
   {{ newValue }}
</ws:template>
<ws:partial template="myTemplate" newValue="text"/>

Во внутреннем шаблоне должен быть доступ и к value, определенном на внешней области видимости, и к переменной newValue, объявленной по месту использования шаблона.

При этом, очевидно, если название переменной, объявляемой по месту использования, будет совпадать с названием переменной из внешней области, последняя будет менее приоритетна.

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

Например, Шаблон HOC выглядит так:

<ws:partial template="{{ content }}" scope="{{ _options }}"/>

Шаблон компонента выглядит так:

<ws:partial template="Controls/HOC" value="{{ myValue }}">
   <div>
      <div class="{{ abc }}"></div>
      <div class="{{ value }}"></div>
      <div class="{{ content.value }}"></div>
      <div class="{{ content.content }}"></div>
      <div class="{{ myValue }}"></div>
   </div>
</ws:partial>

При условии, что набор переменных области видимости компонента выглядит так:

{
   abc: 1,
   value: 2,
   myValue: 3,
   content: 4
}

Результат будет таким:

<div>
   <div class="1"></div>
   <div class="2"></div>
   <div class="3"></div>
   <div class="4"></div>
   <div class="3"></div>
</div>

Директива invisible-node

Директива invisible-node используется для обозначения элемента, который не добавляется в DOM дерево. Далее рассмотрены сценарии использования директивы.

Пример 1. В прикладном шаблоне в качестве корневого элемента используется директива <ws:if>.

  • WML:
    <!-- MyTemplate.wml -->
    <ws:if data="{{ column.result }}">
       <!-- вёрстка, добавляемая в DOM -->
    </ws:if>
  • WML:
    <!-- Parent.wml -->
    <Controls.grid:View resultsTemplate="wml!MyTemplate.wml" />

Когда условие не выполнено, шаблон получается пустым, т.е. без вёрстки. Поэтому такой шаблон будет невалидным.

Чтобы шаблон стал валидным, нужно добавить блок <ws:else> с директивой invisible-node.

  • WML:
    <ws:if data="{{ column.result }}">
       <!-- вёрстка, добавляемая в DOM -->
    </ws:if>
    <ws:else>
       <invisible-node />
    </ws:else>

Пример 2. Директива может быть использована при конфигурации контентной опции, чтобы показать отсутствие вёрстки.

  • WML:
    <Controls.grid:View>
       <ws:headerTemplate>
          <invisible-node />
       </ws:headerTemplate>
    </Controls.grid:View>

Подробнее об атрибутах

См. раздел Передача атрибутов

Для тегов опции не задаются. Добавлять директиву attr в тегах не обязательно:

<div class="some-class"></div> === <div attr:class="some-class"></div>

В следующем примере для корневого тега контрола Bar устанавливаем атрибуты title и class:

<Bar attr:title="+Документ" attr:class="myArea-myButton" />

В следующем примере title будет восприниматься опцией, class — атрибутом:

<Bar title="+Документ" attr:class="myArea-myButton" />

В следующем примере для тега div устанавливаем атрибут style, :

<div attr:style="background-color: #666" /> 
<!-- В данном случае следующая запись будет аналогичной: -->
<div style="background-color: #666" /> 

Когда директива attr не установлена, шаблонизатор трактует эту ситуацию как передача опции. В следующем примере в контрол передаётся опция caption и атрибут title:

<Controls.buttons:Button caption="Добавить сотрудника" attr:title="Задачи" />

Наследование, объединение и переопределение значений атрибутов контрола

Значения атрибутов, которые установлены в родительском контроле, имеют приоритет, поэтому ими переопределяются соответствующие значения атрибутов дочернего контрола. К исключению относятся атрибуты style и class, для которых всегда применяется объединение значений.

Когда в атрибутах style определены одинаковые CSS-свойства, приоритет отдаётся значению из родительского контрола.

Разметка контрола Foo.

<div
   attr:style="color: red; border: 1"
   attr:class="myArea-myButton"
   attr:tabindex="9"
   attr:tooltip="Форма регистрации">
   ...
</div>

Разметка контрола Bar с дочерним Foo.

<Foo
   attr:style="color: blue"
   attr:class="myArea-myBar"
   attr:tabindex="3" />

В результате, контрол Foo будет добавлен со следующими значениями атрибутов:

<div
   attr:style="color: blue; border: 1"
   attr:class="myArea-myButton myArea-myBar"
   attr:tabindex="3"
   attr:tooltip="Форма регистрации" />

Критика

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

Я предлагаю рассмотреть и принять решение по следующим критическим мыслям, пришедшим мне в голову и не дающим покоя:

Критика 1: Различия между ws:template и контентной опцией

Доступ к переменным по месту использования в шаблоне контентной опции осуществляется через объект, названный как и контентная опция. По логике то же самое, что и доступ к аргументам в функции через ключевое слово arguments. Только здесь не выбрано ключевое слово, а выбрано почему-то название контентной опции.

Доступ к переменным по месту использования в ws:template отличается. Он дается напрямую, будто мы используем именованные аргументы в функции.

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

Еще такая мысль, есть вот эти два варианта, но мы не даем возможности выбирать между этими вариантами. То есть не даем пользователю права выбора, объявлять ли переменные как именованные, которые будут перебивать переменные из замыкания, или же не объявлять и иметь доступ к переменным из замыкания.

Вот например:

{{ value }} <!-- выведется value0 -->
<ws:template name="template1">
   {{ value }} <!-- выведется value1 -->
   <ws:template name="template2">
      {{ value }} <!-- выведется value2 -->
   </ws:template>
   <ws:partial template="template2" value="value2"/>
</ws:template>
<ws:partial template="template1" value="value1"/>

Из самого внутреннего шаблона мы можем получить доступ только "value2" и "value1". А если делать доступ к аргументам через ключевое слово например, будет доступ только к переменным "value2" и "value0". Либо так либо так, выбора нет. а в Javascript есть выбор, можно объявлять именованный аргумент и тогда он перебьет переменную из замыкания, или не объявлять — и тогда будет доступ и к переменной через ключевое слово, и к замыканию.

Так как доступ к переменным в шаблоне контентной опции предоставляется по имени этой опции, и нет ключевого слова, имя опции может перекрыть переменную из замыкания, и было придумано добавить в объект эту переменную из замыкания. Чтобы можно было достучаться как-то так: itemTemplate.itemTemplate. Это какой-то треш.

Если не нужно перебивать переменные из замыкания в шаблоне, как вариант можно передавать объект по месту использования

<ws:partial template="{{itemTemplate}}" itemTemplate="{{'item': item}}"/>
  • так будет явно передавать объект itemTemplate, и все будет прозрачно.

Критика 2: Переменные, объявляемые через конструкцию ws:template name=...

У нас есть возможность объявить внутренний шаблон как контентную опцию. Мы задаем его прямо в опции компонента. И дальше эта опция используется в ws:partial. Используется через фигурные скобки как полноценная переменная.

<ws:partial template="{{ itemTemplate }}" /> 

А еще у нас есть возможность создавать внутренние шаблоны через ws:template. Мы присваиваем ему название, и также можем использовать в ws:partial, но только не как полноценную переменную, а как нечто иное.

<ws:partial template="myTemplate" /> 

Эта особенность ws:template выбивается из общей системы. Наверняка для них и принципы замыканий в шаблоне то не работают. Это сильно усложняет понимание шаблонизатора. Почему мы не наделяем эти внутренние шаблоны полноценными правами переменных? Мы могли бы объявлять их в отдельном объекте локальных переменных, и искать в tclosure.getter сначала по локальным переменным как наиболее приоритетным.

А еще мы могли бы например так:

<ws:template name="myTemplate"> Text </ws:template>
<Controls.list:View itemTemplate="{{ myTemplate }}"/>

то есть мы могли бы инициализировать itemTemplate и как переменной, пришедшей снаружи, и как переменной, объявленной в ws:template, и задать инлайновый шаблон.

Критика 3: Переменные, объявляемые в местах использования шаблонов

По аналогии с критикой 2 у нас есть возможность задавать name в местах использования шаблонов и компонентов. Мы используем эти названия каким-то хитрым образом, не как переменную. Придумана какая-то новая конструкция, все работает на новых рельсах, в обход системы областей видимости.

Пример:

<Controls.ItemComponent name="itemComponent"/>
<Controls.list:View itemTemplate="itemComponent"/>

Сейчас конвертируется в

<Controls.list:View>
   <ws:itemTemplate>
      <Controls.ItemComponent name="itemComponent"/>
   </ws:itemTemplate>
</Controls.list:View>

То есть по сути создается шаблон, в котором используется один компонент.

То же самое было бы можно сделать так:

<ws:template name="itemComponent">
   <Controls.ItemComponent/>
</ws:template>
<Controls.list:View itemTemplate="{{itemComponent}}"/>

И можно было бы добавить сахара, чтобы <Controls.ItemComponent name="itemComponent"/> разворачивался в конструкцию с ws:template

<Controls.ItemComponent name="itemComponent"/>
<Controls.list:View itemTemplate="{{itemComponent}}"/>

Переменная itemComponent стала бы полноценной переменной, все бы работало на одних рельсах.

Критика 4: Особенности работы компонентного шаблона

Особенности области видимости шаблона компонента были описаны ранее. Среди них замечено, что доступ к переменным по месту использования осуществляется через магический объект _options.

Непонятно чем все это обусловлено. Эти изменения нарушают принципы областей видимости. Чтобы поддержать принципы, можно было бы, например, в геттере переменной (tclosure.getter) смотреть, есть ли переменная на экземпляре, и если нет — смотреть в _options. И тогда defaultOptions занял бы свое место в приоритетах источников переменных как наименее приоритетное. А переменные, полученные по месту использования, будут менее приоритетны, чем переменные, объявленные на экземпляре. То есть переменные на экземпляре становятся локальными переменными шаблона. Если состояние компонента не определено для поля — берется значение по умолчанию или то, которое прилетело сверху. Можно будет указывать пользовательские значения по умолчанию для полей. Можно оставить, чтобы компонент продолжал складывать все в _options, если так уж нужен доступ к значениям по умолчанию, не перебитый состоянием.

Либо нужно менять принцип, и для обычных шаблонов тоже складывать переменные из места использования шаблона в отдельный объект _options (или найти ключевое слово, чтобы в критерии 1 не было объекта с именем контентной опции). И доступ к нему будет открыт, и не будет пересечения с локальными переменными, такими как переменная ws:for и названия шаблонов. Но тогда не будет похожих на нативные областей видимости.

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