🦉 Язык шаблонов QWeb🦉

Содержание

Введение

[QWeb] (https://doc.open-odoo.ru/developer/13.0/ru/reference/qweb.html) — это основной механизм шаблонов, используемый Odoo. Он основан на формате XML и используется в основном для создания HTML. В OWL шаблоны QWeb компилируются в функции, которые генерируют виртуальное представление HTML.

<div>
    <span t-if="somecondition">Какая-то строка</span>
    <ul t-else="">
        <li t-foreach="messages" t-as="message">
            <t t-esc="message"/>
        </li>
    </ul>
</div>

Директивы шаблона указываются как XML-атрибуты с префиксом t-, например t-if для условий, при этом элементы и другие атрибуты рендерятся напрямую.

Чтобы избежать рендеринга элемента, также доступен элемент-заполнитель <t>, который выполняет свою директиву, но сам по себе не генерирует никаких выходных данных.

В этом разделе мы представляем язык шаблонов, включая его специфичные для Owl расширения.

Директивы

Для справки, вот список всех стандартных директив QWeb:

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

Описание элементов

Пробелы

Пробелы в шаблоне обрабатываются особым образом:

  • последовательные пробелы всегда объединяются в один пробел
  • если текстовый узел, состоящий только из пробелов, содержит разрыв строки, он игнорируется
  • предыдущие правила не применяются, если мы находимся в теге <pre>

Корневые узлы

По многим причинам шаблоны Owl QWeb должны иметь один корневой узел. Точнее, результат рендеринга шаблона должен иметь один корневой узел:

<!–– не ok: два корневых узла ––>
<t>
    <div>foo</div>
    <div>bar</div>
</t>

<!–– ok: результат имеет один корневой узел ––>
<t>
    <div t-if="someCondition">foo</div>
    <span t-else="">bar</span>
</t>

Дополнительные корневые узлы фактически будут игнорироваться (даже если они будут рендериться в памяти).

Примечание: это не относится к дочерним шаблонам (см. директиву t-call). В этом случае они будут встроены в основной шаблон и могут иметь много корневых узлов.

Иполнение выражений

Выражения QWeb — это строки, которые будут обрабатываться во время компиляции. Каждая переменная в выражении javascript будет заменена найденой в контексте то есть компонентом). Например, a + b.c(d) будет преобразовано в:

context["a"] + context["b"].c(context["d"]);

Полезно объяснить различные правила, применимые к этим выражениям:

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

    <div><p t-if="1 + 2 === 3">ok</p></div>
    

    допустимо, но следующее неверно:

    <div><p t-if="console.log(1)">NOT valid</p></div>
    
  2. оно может использовать что угодно в контексте рендеринга (как правило, компонент):

    <p t-if="user.birthday === today()">С днем рождения!</p>
    

    действителен, и будет считывать объект user из контекста и вызывать функцию today.

  3. оно может использовать несколько специальных операторов, чтобы избежать использования таких символов, как <, >, & или |. Это необходимо для того, чтобы убедиться, что мы по-прежнему пишем валидный XML.

    Wordreplaced with
    and&&
    or\|\|
    gt>
    gte>=
    lt<
    lte<=

    И мы можем написать так:

    <div><p t-if="10 + 2 gt 5">ok</p></div>
    

Статические HTML-узлы

Обычные, обычные html-узлы рендерятся сами как есть:

  <div>hello</div> <!–– результатом рендерига будет эта же строка ––>

Вывод данных

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

<p><t t-esc="value"/></p>

рендеринг со значением value, установленным на 42 в контексте рендеринга, дает:

<p>42</p>

Директива t-raw почти такая же, как t-esc, но без экранирования. Это в бывает полезно для вставки строки html куда-нибудь. Очевидно, что в целом это небезопасно, и его следует использовать только для заведомо проверенных строк.

<p><t t-raw="value"/></p>

рендеринг со значением value установленным как <span>foo</span> в контексте рендеринга дает:

<p><span>foo</span></p>

Обратите внимание, что, поскольку содержание выражения заранее неизвестно, директива t-raw должна анализировать html (и преобразовывать его в виртуальную структуру dom) для каждого рендеринга. Таким образом, это будет намного медленнее, чем обычный шаблон. Поэтому рекомендуется по возможности ограничивать использование t-raw.

Назначение переменных

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

Это делается с помощью директивы t-set, которая принимает имя создаваемой переменной. Устанавливаемое значение может быть предоставлено двумя способами:

  1. атрибут t-value, содержащий выражение, и будет установлен результат его выполнения:

    <t t-set="foo" t-value="2 + 1"/>
    <t t-esc="foo"/>
    

    напечатает 3. Обратите внимание, что оценка выполняется во время рендеринга, а не во время компиляции.

  2. если атрибута t-value нет, тело узла сохраняется и его значение устанавливается как значение переменной:

    <t t-set="foo">
        <li>ok</li>
    </t>
    <t t-esc="foo"/>
    

    сгенерирует &lt;li&gt;ok&lt;/li&gt; (содержимое экранировано, так как мы использовали директиву t-esc)

Директива t-set действует как обычная переменная в большинстве языков программирования. Она лексически ограничена (внутренние узлы являются дочерними областяим), может быть затенена, ...

Состояния

Директива t-if полезна для условного рендеринга чего-либо. Он оценивает выражение, заданное как значение атрибута, и затем действует соответствующим образом.

<div>
    <t t-if="condition">
        <p>ok</p>
    </t>
</div>

Элемент рендерится, если условие (оцениваемое в текущем контексте рендеринга) истинно:

<div>
    <p>ok</p>
</div>

но если условие ложно, оно удаляется из результата:

<div>
</div>

Условный рендеринг применяется к носителю директивы, который не обязательно должен быть <t>:

<div>
    <p t-if="condition">ok</p>
</div>

даст те же результаты, что и в предыдущем примере.

Также доступны дополнительные директивы условного перехода t-elif и t-else:

<div>
    <p t-if="user.birthday == today()">С днем рождения!</p>
    <p t-elif="user.login == 'root'">Добров пожаловать хозяин!</p>
    <p t-else="">Добро пожаловать!</p>
</div>

Динамические атрибуты

Можно использовать директиву t-att- для добавления динамических атрибутов. Его основное использование — выполнение выражения (во время рендеринга) и привязка атрибута к его результату:

Например, если в контексте рендеринга для id установлено значение 32,

<div t-att-data-action-id="id"/> <!-- результат: <div data-action-id="32"></div> -->

Если выражение оценивается как ложное значение, оно вообще не будет установлено:

<div t-att-foo="false"/>  <!-- результат: <div></div> -->

Иногда бывает удобно отформатировать атрибут с интерполяцией строк. В этом случае можно использовать директиву t-attf-. Это полезно, когда нам нужно смешивать литеральные и динамические элементы, такие как классы CSS.

<div t-attf-foo="a {{value1}} is {{value2}} of {{value3}} ]"/>
<!-- результат если значения установлены в 1,2 и 3: <div foo="a 0 is 1 of 2 ]"></div> -->

Если нам нужны полностью динамические имена атрибутов, то есть дополнительная директива: t-att, которая принимает либо объект (с сопоставлением ключей с их значениями), либо пару [key, value]. Например:

<div t-att="{'a': 1, 'b': 2}"/> <!-- результат: <div a="1" b="2"></div> -->

<div t-att="['a', 'b']"/> <!-- <div a="b"></div> -->

Динамический атрибут class

Для удобства Owl поддерживает специальный случай для случая t-att-class: можно использовать объект с ключами, описывающими классы, и значениями, логическими значениями, обозначающими, присутствует класс или нет:

<div t-att-class="{'a': true, 'b': true}"/> <!-- результат: <div class="a b"></div> -->

<div t-att-class="{'a b': true, 'c': true}"/> <!-- результат: <div class="a b c"></div> -->

Обратите внимание, что его можно комбинировать с обычным атрибутом класса:

<div class="a" t-att-class="{'b': true}"/> <!-- результат: <div class="a b"></div> -->

Динамические имена тегов

При написании общих компонентов или шаблонов конкретный тег для элемента HTML еще не известен. В таких ситуациях полезна директива t-tag. Он просто динамически исполняет выражение для использования в качестве имени тега. Шаблон:

<t t-tag="tag">
    <span>content</span>
</t>

будет рендериться как <div><span>content</span></div>, если ключ контекста tag установлен на div.

Циклы

QWeb имеет директиву итерации t-foreach, которая принимает выражение, возвращающее коллекцию для итерации, и второй параметр t-as, предоставляющий имя для использования в текущем элементе итерации:

<t t-foreach="[1, 2, 3]" t-as="i">
    <p><t t-esc="i"/></p>
</t>

будет отрендерено как:

<p>1</p>
<p>2</p>
<p>3</p>

Подобно условиям, t-foreach применяется к элементу, имеющему атрибут директивы, и

<p t-foreach="[1, 2, 3]" t-as="i">
    <t t-esc="i"/>
</p>

эквивалентен предыдущему примеру.

t-foreach может перебирать массив (текущий элемент будет текущим значением) или объект (текущий элемент будет текущим ключом).

В дополнение к имени, переданному через t-as, t-foreach предоставляет несколько других переменных для различных точек данных (примечание: $as будет заменено именем, переданным t-as):

  • $as_value: текущее значение итерации, идентичное $as для списков и целых чисел, но для объектов предоставляет значение (где $as предоставляет ключ)
  • $as_index: текущий индекс итерации (первый элемент итерации имеет индекс 0)
  • $as_first: является ли текущий элемент первым в итерации (эквивалентно $ as_index == 0)
  • $as_last: независимо от того, является ли текущий элемент последним в итерации (эквивалентно $as_index + 1 == $as_size), требуется доступ к размеру итерируемого

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

<t t-set="existing_variable" t-value="false"/>
<!-- existing_variable теперь False -->

<p t-foreach="Array(3)" t-as="i">
    <t t-set="existing_variable" t-value="true"/>
    <t t-set="new_variable" t-value="true"/>
    <!-- existing_variable и new_variable теперь true -->
</p>

<!-- existing_variable всегда true -->
<!-- new_variable undefined -->

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

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

Рассмотрим следующую ситуацию: у нас есть список из двух элементов [{text: "a"}, {text: "b"}], и мы визуализируем их в этом шаблоне:

<p t-foreach="items" t-as="item"><t t-esc="item.text"/></p>

Результатом будут два тега <p> с текстом a и b. Теперь, если мы поменяем их местами и перерендерим шаблон, Owl должен знать, каково намерение:

  • должна ли Owl поменять местами узлы DOM,
  • или он должен сохранить узлы DOM, но с обновленным текстовым содержимым?

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

Есть много других случаев, когда это важно: теги input с их значением, классы css и анимации, положение прокрутки...

Итак, директива t-key используется для идентификации элемента. Это позволяет Owl понять, действительно ли разные элементы списка отличаются или нет.

Приведенный выше пример можно изменить, добавив ID: [{id: 1, text: "a"}, {id: 2, text: "b"}]. Тогда шаблон может выглядеть так:

<p t-foreach="items" t-as="item" t-key="item.id"><t t-esc="item.text"/></p>

Директива t-key полезна для списков (t-foreach). Ключ должен быть уникальным числом или строкой (объекты работать не будут: они будут преобразованы в строку "[object Object]", которая явно не уникальна).

Кроме того, ключ может быть установлен в теге t или в его дочерних элементах. Все следующие варианты эквивалентны:

<p t-foreach="items" t-as="item" t-key="item.id">
  <t t-esc="item.text"/>
</p>

<t t-foreach="items" t-as="item" t-key="item.id">
  <p t-esc="item.text"/>
</t>

<t t-foreach="items" t-as="item">
  <p t-key="item.id" t-esc="item.text"/>
</t>

Если директивы t-key нет, Owl будет использовать индекс в качестве ключа по умолчанию.

Примечание: директива t-foreach принимает только массивы (списки) или объекты. Это не работает с другими итерируемыми объектами, такими как Set. Однако это всего лишь вопрос использования javascript-оператора .... Например:

<t t-foreach="...items" t-as="item">...</t>

Оператор ... преобразует Set (или любые другие итерации) в список, который будет работать с Owl QWeb.

Рендеринг дочерних шаблонов

Шаблоны QWeb можно использовать для рендеринга верхнего уровня, но их также можно использовать внутри другого шаблона (чтобы избежать дублирования или присвоения имен частям шаблонов), используя директиву t-call:

<div t-name="other-template">
    <p><t t-value="var"/></p>
</div>

<div t-name="main-template">
    <t t-set="var" t-value="owl"/>
    <t t-call="other-template"/>
</div>

будет отображаться как <div><p>owl</p></div>. В этом примере показано, что дочерний шаблон отображается с контекстом выполнения родителя. Дочерний шаблон фактически встроен в основной шаблон, но в дочерней области: переменные, определенные в дочернем шаблоне, не экранируются.

Иногда может потребоваться передать информацию в дочерний шаблон. В этом случае содержимое тела директивы t-call доступно в виде специальной магической переменной 0:

<t t-name="other-template">
    Этот шаблон был вызван с содержимым:
    <t t-raw="0"/>
</t>

<div t-name="main-template">
    <t t-call="other-template">
        <em>содержимое</em>
    </t>
</div>

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

<div>
    Этот шаблон был вызван с содержимым:
    <em>содержимое</em>
</div>

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

<t t-call="other-template">
    <t t-set="var" t-value="1"/>
</t>
<!-- "var" здесь не существует -->

Динамические дочерние шаблоны

Директиву t-call также можно использовать для динамического вызова дочернего шаблона с использованием интерполяции строк. Например:

<div t-name="main-template">
    <t t-call="{{template}}">
        <em>содержимое</em>
    </t>
</div>

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

Переводы

По умолчанию QWeb указывает, что шаблоны должны быть переведены. Если такое поведение нежелательно, существует директива t-translation, которая может отключить переводы (если для нее установлено значение off) со следующими правилами:

  • каждый текстовый узел будет заменен своим переводом,
  • каждое из следующих значений атрибутов также будет переведено: title, placeholder, label и alt,
  • перевод текстовых узлов можно отключить с помощью специального атрибута t-translation, если его значение равно off.

См. здесь для получения более подробной информации о том, как настроить функция перевода в Owl QWeb.

Отладка

Реализация javascript QWeb предоставляет две полезные директивы отладки:

t-debug добавляет оператор отладчика во время рендеринга шаблона:

<t t-if="a_test">
    <t t-debug=""/>
</t>

остановит выполнение, если инструменты разработчика браузера открыты.

t-log принимает параметр выражения, выполняет выражение во время рендеринга и записывает результат в console.log:

<t t-set="foo" t-value="42"/>
<t t-log="foo"/>

выведет в консоль 42.