Pull to refresh

Работаем с jQuery Templates

Reading time 20 min
Views 138K

Введение


Плагин jQuery Templates – это «движок шаблонов», работающий на стороне клиента как расширение jQuery.

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

Разумеется, jQuery Templates – не единственный и не первый «движок шаблонов», но у него есть большое преимущество перед альтернативными вариантами – поддержка со стороны jQuery Team. Это позволяет нам не бояться того, что этот плагин окажется заброшенным, и различные проблемы, возникающие при выходе новых версий браузеров, придется решать своими силами.

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


Немного истории


Этот плагин был разработан в компании Microsoft на основе JavaScript Micro-templating library (автор – John Resig), некоторые подробности о разработке можно узнать из блог-поста Stephen Walther «An Introduction to jQuery Templates» и из поста John Resig «Templating Syntax» на форуме jQuery. С октября прошлого года плагины jQuery Templates, jQuery DataLink и jQuery Globalization стали частью проекта jQuery.

Часть первая, теоретическая


Приступаем к работе


Давайте начнем. Приведенный ниже пример показывает список фильмов, заданный в массиве (полный код примера – в файле BasicSample1.htm):

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Простой пример (1)</title>
    <link href="Styles/Default.css" rel="Stylesheet" type="text/css" />
    <script src="Scripts/jquery-1.5rc1.js" type="text/javascript"></script>
    <script src="Scripts/jquery.tmpl.js" type="text/javascript"></script>
    <script src="DataItems.js" type="text/javascript"></script>
    <script id="movieTmpl" type="text/x-jquery-tmpl">
        <div class="movie-bag">
            <img src="Content/Thumbnails/${thumbnail}" class="thumbnail" />
            <div class="base-info">
                <h2>
                    ${title}
                </h2>
                <p>
                    Режиссер: ${director}<br />
                    В ролях: ${actors}<br />
                    Год: ${year}
                </p>
            </div>
        </div>
    </script>
    <script type="text/javascript">

        $(function () {
            $('#movieTmpl').tmpl(dataItems).appendTo('#movieListBag');
        });

    </script>
</head>
<body>
    <h1>Простой пример (1)</h1>
    <div id="movieListBag">
    </div>
</body>
</html>

А вот что вы увидите в браузере:



Давайте разберем этот пример подробно.

Итак, первое, что я делаю, это подключаю jQuery Core Library и jQuery Templates:

<script src="Scripts/jquery-1.5rc1.js" type="text/javascript"></script>
<script src="Scripts/jquery.tmpl.js" type="text/javascript"></script>

Ранее неоднократно говорилось о том, что jQuery Templates будут включены в jQuery Core Library – но в jQuery 1.5 RC1, вышедшем 24 января, шаблоны по прежнему отсутствуют.

Затем я загружаю список фильмов:

<script src="DataItems.js" type="text/javascript"></script>

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

Внутри файл DataItems.js выглядит следующим образом:

var dataItems = [
    {
        title: 'Бандиты',
        thumbnail: 'Bandits.jpg',
        director: 'Барри Левинсон',
        actors: ['Брюс Уиллис', 'Билли Боб Торнтон', 'Кейт Бланшетт'],
        year: 2001,
        budget: 95000000,
        grossRevenue: 67631903,
        rating: 0,
        frames: ['Bandits-1.jpg', 'Bandits-2.jpg', 'Bandits-3.jpg', 'Bandits-4.jpg', 'Bandits-5.jpg']
    },
    ...

Следующим шагом я создаю шаблон:

<script id="movieTmpl" type="text/x-jquery-tmpl">
    <div class="movie-bag">
        <img src="Content/Thumbnails/${thumbnail}" class="thumbnail" />
        <div class="base-info">
            <h2>
                ${title}
            </h2>
            <p>
                Режиссер: ${director}<br />
                В ролях: ${actors}<br />
                Год: ${year}
            </p>
        </div>
    </div>
</script>

Обратите внимание, что шаблон размещается в теге SCRIPT, а в качестве MIME-типа я указываю text/x-jquery-tmpl. Встретив при разборе документа незнакомый MIME-тип, браузер не пытается интерпретировать содержимое тега SCRIPT, что мне и требуется.

Вообще говоря, шаблон можно разместить в любом теге, например, в теге DIV:

<div id="movieTmpl" style="display: none">
    <div class="movie-bag">
        <img src="Content/Thumbnails/${thumbnail}" class="thumbnail" />
        <div class="base-info">
            <h2>
                ${title}
            </h2>
            <p>
                Режиссер: ${director}<br />
                В ролях: ${actors}<br />
                Год: ${year}
            </p>
        </div>
    </div>
</div>

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

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



А вот случае с таблицей все может быть гораздо интереснее (большое спасибо TEHEK за этот пример!):

<div id="movieTmpl" style="display: none">
    <table>
        <tbody>
            {{each dataItems}}
            <tr>
                <td>${title}</td>
                <td>${director}</td>
                <td>${year}</td>
            </tr>
            {{/each}}
        </tbody>
    </table>
</div>

Internet Explorer и Opera обработают этот код корректно:



А вот Chrome и Fire Fox «вытолкнут» лишний код за пределы таблицы, в результате чего таблица окажется пустой… Happy debugging! ;-)



Для тега SELECT будет наблюдаться аналогичная картина.

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

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

$('#movieTmpl').tmpl(dataItems).appendTo('#movieListBag');

Что при этом происходит, я изобразил на приведенной ниже диаграмме:



Итак:
  1. Метод .tmpl() получает текст шаблона – т.е. inner text элемента, полученного с помощью вызова $('#movieTmpl').
  2. Текст шаблона компилируется – на его основе создается функция JavaScript.
  3. Создается «экземпляр шаблона» — объект, который содержит ссылку на элемент данных (поле data), переданный как аргумент метода .tmpl(). Методу .tmpl() можно передать массив, объект, null или вызвать его без аргументов. Если передать массив, то для каждого элемента массива будет создан свой экземпляр шаблона, ссылающийся на этот элемент, во всех остальных случаях будет создан только один экземпляр.
  4. Вызывается скомпилированная функция-шаблон, которой передается объект-экземпляр. Функция возвращает текст шаблона, в котором сделаны все подстановки.
  5. Полученный на предыдущем шаге текст преобразуется в коллекцию HTML-элементов. Ссылки на эти элементы также сохраняются в объекте-экземпляре (поле nodes), что позволяет в дальнейшем легко обновить «выход» шаблона при изменении исходных данных (см. раздел «Динамическое обновление»).
  6. И, наконец, метод .tmpl() возвращает jQuery-коллекцию HTML-элементов, которые добавляются в документ с помощью вызова appendTo('#movieListBag').


Выражения


Для подстановки в шаблон значений используется тег ${...}. Внутри этого тега можно указать как наименование свойства объекта, переданного методу .tmpl(), так и любое корректное выражение JavaScript, в том числе вызов функции.

Использование свойств объекта (элемента массива):

<h2>
    ${title}
</h2>

Использование выражений JavaScript:

<p>
    Бюджет: $${(budget / 1000000).toFixed(0)} млн.<br />
    Сборы: $${(grossRevenue / 1000000).toFixed(1)} млн.
</p>


Поля и методы экземпляра шаблона


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

Каждый экземпляр шаблона содержит следующие поля:
  1. data – содержит ссылку на элемент данных, связанный с экземпляром шаблона;
  2. tmpl – содержит ссылку на скомпилированный шаблон, используемый для рендеринга;
  3. parent – если шаблон был вызван из другого шаблона с помощью тега {{tmpl}}, содержит ссылку на «родительcкий» экземпляр шаблона;
  4. nodes – после рендеринга содержит ссылки на HTML-элементы, порожденные в результате применения шаблона.

Кроме того, метод .tmpl() принимает два аргумента – data и options. С аргументом data вы уже познакомились, через него передается ссылка на элемент данных. А используя аргумент options, можно передать ссылку на объект, все поля и методы которого будут перенесены в каждый экземпляр шаблона, созданный в методе .tmpl().

Ниже приведен пример использования этого параметра:

<script id="movieTmpl" type="text/x-jquery-tmpl">
    <div class="movie-bag">
        <img src="Content/Thumbnails/${thumbnail}" class="thumbnail" />
        <div class="base-info">
            <h2>
                ${title}
            </h2>
            <p>
                Режиссер: ${director}<br />
                В ролях: ${actors}<br />
                Год: ${year}<br />
                Бюджет: $${$item.formatBudget(budget)} млн.<br />
                Сборы: $${$item.formatGrossRevenue(grossRevenue)} млн.
            </p>
        </div>
    </div>
</script>


$(function () {
    $('#movieTmpl')
        .tmpl(
            dataItems,
            {
                formatBudget: function (value) {
                    return (value / 1000000).toFixed(0);
                },
                formatGrossRevenue: function (value) {
                    return (value / 1000000).toFixed(1);
                }
            })
        .appendTo('#movieListBag');
});

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

И, наконец, экземпляр шаблона содержит методы update() и html(), использование которых я покажу далее.

Как выглядит скомпилированный шаблон?


Как выглядит скомпилированный шаблон, можно увидеть, воспользовавшись методом .template(), который как раз и осуществляет компиляцию шаблонов. Этот метод возвращает объект-функцию, содержимое которой легко посмотреть:

$('#compiledTemplateBag').text('' + $('#movieTmpl').template());

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

function anonymous(jQuery, $item) {
    var $ = jQuery, call, _ = [], $data = $item.data;

    with ($data) {
        _.push('<div class="movie-bag"> <img src="Content/Thumbnails/');

        if (typeof (thumbnail) !== 'undefined' && (thumbnail) != null) {
            _.push($.encode((typeof (thumbnail) === 'function' ? (thumbnail).call($item) : (thumbnail))));
        }

        _.push('" class="thumbnail" /> <div class="base-info"> <h2> ');

        if (typeof (title) !== 'undefined' && (title) != null) {
            _.push($.encode((typeof (title) === 'function' ? (title).call($item) : (title))));
        }

        _.push(' </h2> <p> Режиссер: ');

        if (typeof (director) !== 'undefined' && (director) != null) {
            _.push($.encode((typeof (director) === 'function' ? (director).call($item) : (director))));
        }

        _.push('<br /> В ролях: ');

        if (typeof (actors) !== 'undefined' && (actors) != null) {
            _.push($.encode((typeof (actors) === 'function' ? (actors).call($item) : (actors))));
        }

        _.push('<br /> Год: ');

        if (typeof (year) !== 'undefined' && (year) != null) {
            _.push($.encode((typeof (year) === 'function' ? (year).call($item) : (year))));
        }
        _.push(' </p> </div> </div>');
    }

    return _;
}

Думаю, что теперь вам должно быть понятно, как обрабатываются выражения, указанные в теге ${...} – и это понимание может существенно помочь вам при отладке! Дело в том, что jQuery Templates выполняет относительно простое преобразование текста шаблона, поэтому если вы допустите ошибку в выражении, то сообщение об ошибке будет относиться к тексту полученной в результате преобразования функции и часто может быть крайне невразумительным.



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

Что ж, пожалуй, на этом рассказ о работе jQuery Templates стоит завершить и перейти к его практическому применению.

Часть вторая, практическая


Условия


Для того чтобы применять части шаблона в зависимости от некоторых условий, в jQuery Templates используются теги {{if}}...{{else}}...{{/if}}.

Приведенный ниже пример показывает использование этих тегов (полный код примера – в файле IfElseTag.htm):

<p>
    Носитель:
    {{if $item.data.media == 'dvd'}}
        <img src="Images/media-dvd.png" />
    {{else $item.data.media == 'blue-ray'}}
        <img src="Images/media-blueray.png" />
    {{else}}
        <img src="Images/media-unspecified.png" />
    {{/if}}
</p>

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



В качестве условия в тегах {{if}} и {{else}} можно использовать любое корректное JavaScript-выражение.

Обработка коллекций


Для обработки коллекций в шаблонах используется тег {{each}}...{{/each}}. Приведенный ниже пример показывает использование тега {{each}} для вывода списка актеров (полный код примера – в файле EachTag1.htm):

В ролях:
{{each actors}}
    ${$value}
    {{if $index < $data.actors.length - 1}}
        ,
    {{/if}}
{{/each}}

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



В качестве аргумента тегу {{each}} можно передать массив, объект, или jQuery-коллекцию. Внутри тег {{each}} использует вызов jQuery.each(), поэтому все, что сказано в документации о поведении jQuery.each() справедливо и для тега {{each}}. Пример ниже демонстрирует использование тега {{each}} для показа всех свойств объекта (полный код примера – в файле EachTag2.htm):

<script id="objTmpl" type="text/x-jquery-tmpl">
    <div>
        <dl>
            {{each $data}}
                <dt>
                    ${$index}
                </dt>
                <dd>
                    ${$value}
                </dd>
            {{/each}}
        </dl>
    </div>
</script>

Внутри тега {{each}} доступно две переменные: $value, которая содержит ссылку на текущий элемент массива, и $index, которая содержит индекс текущего элемента массива или имя свойства.

Разумеется, внутри тега {{each}} можно использовать другие теги, а кроме того, вам по-прежнему будут доступны переменные $item и $data. В приведенном примере переменные $index и $data вместе с тегом {{if}} используются, чтобы вывести запятую между именами актеров – к сожалению, переменная $last не предусмотрена, хотя она была бы очень полезна!

Наконец, если у вас вдруг возникнет такая необходимость, вы можете изменить имена переменных по умолчанию. В примере, приведенном ниже, эти имена изменены на myIndex и myValue (полный код примера – в файле EachTag3.htm):

В ролях:
{{each(myIndex, myValue) actors}}
    ${myValue}
    {{if myIndex < $data.actors.length - 1}}
        ,
    {{/if}}
{{/each}}

Кстати, попытка изменить имя только для переменной $index ни к чему хорошему не приведет – ошибки не будет, но и доступа к текущему значению вы тоже не сможете получить!

Вложенные шаблоны


Шаблоны могут быть очень большими – и тогда имеет смысл разделить их на несколько частей меньшего объема или включать повторяющиеся части, которые логично выделить в отдельный шаблон. В jQuery Templates это осуществляется с помощью вложенных шаблонов, для вызова которых используется тег {{tmpl}}.

Пример ниже иллюстрирует, как вынести часть кода шаблона в другой шаблон (полный код примера – в файле NestedTemplates1.htm):

<script id="movieTmpl" type="text/x-jquery-tmpl">
    ...
    <p>
        Режиссер: ${director}<br />
        В ролях: {{tmpl '#actorsTmpl'}}<br />
        Год: ${year}
    </p>
    ...
</script>
<script id="actorsTmpl" type="text/x-jquery-tmpl">
    {{each actors}}
        ${$value}
        {{if $index < $data.actors.length - 1}}
            ,
        {{/if}}
    {{/each}}
</script>

В теге {{tmpl}} обязательно указывается jQuery-селектор вызываемого шаблона или имя шаблона, ранее сохраненного в кэше. Т.к. в этом примере других аргументов у тега {{tmpl}} нет, вложенный шаблон получит тот же самый элемент данных, что и родительский – но экземпляр шаблона у него будет свой, причем поле parent в нем будет ссылаться на родительский экземпляр шаблона.

Следующий пример демонстрирует передачу во вложенный шаблон нового элемента данных и использование ссылки на родительский экземпляр шаблона. Как и в случае использования метода .tmpl(), если при вызове вложенного шаблона указать массив, то шаблон будет применен для каждого элемента массива (полный код примера – в файле NestedTemplates2.htm):

<script id="movieTmpl" type="text/x-jquery-tmpl">
    ...
    <p>
        Режиссер: ${director}<br />
        В ролях: {{tmpl(actors) '#actors_template'}}<br />
        Год: ${year}
    </p>
    ...
</script>
<script id="actors_template" type="text/x-jquery-tmpl">
    ${$data}
    {{if $data !== $item.parent.data.actors[$item.parent.data.actors.length - 1]}}
        ,
    {{/if}}
</script>

И, наконец, последний пример в этом разделе показывает, как передать во вложенный шаблон аргумент options, а заодно демонстрирует, как аргумент options можно использовать для определения последнего элемента в обрабатываемом массиве (полный код примера – в файле NestedTemplates3.htm):

<script id="movieTmpl" type="text/x-jquery-tmpl">
    ...
    <p>
        Режиссер: ${director}<br />
        В ролях: {{tmpl(actors, { last: actors[actors.length - 1] }) '#actors_template'}}<br />
        Год: ${year}
    </p>
    ...
</script>
<script id="actors_template" type="text/x-jquery-tmpl">
    ${$data}
    {{if $data !== $item.last}}
        ,
    {{/if}}
</script>


Трансформация


Еще одна интересная возможность jQuery Templates связана с трансформацией HTML-разметки, для чего используется тег {{wrap}} (вообще говоря, wrap – это «обертывание», но мне кажется что термин «трансформация» лучше отражает суть происходящего).

Классический пример использования тега {{wrap}} – это создание закладок:



Вот как это выглядит внутри (полный код примера – в файле Transformation1.htm):

<script id="tabsContent" type="text/x-jquery-tmpl">
    {{wrap(null, { viewState: $item.viewState }) '#tabsTmpl'}}
        <h1>English</h1>
        <div>
            <h2>The Ballad of East and West</h2>
            <h3>Rudyard Kipling</h3>
            <p>OH, East is East, and West is West...</p>
        </div>
    {{/wrap}}
</script>

Исходные данные для трансформации размещаются в шаблоне tabContent – именно этот шаблон я дальше будут инстанцировать.

HTML-разметка, которую я буду трансформировать, помещается в теге {{wrap}}. Для тега {{wrap}} используется точно такая же нотация вызова, что и для тега {[tmpl}} – т.е., вы обязательно должны указать селектор или имя шаблона и дополнительно можете передать ссылку на элемент данных и options. В данном случае в параметре options я передаю ссылку на объект viewState, который содержит индекс выбранной закладки.

Код для трансформации выглядит следующим образом:

<script id="tabsTmpl" type="text/x-jquery-tmpl">
    <div class="tab-head">
        {{each $item.html("h1", true)}}
            <div class="tab {{if $index == $item.viewState.index}}active{{/if}}">
                ${$value}
            </div>
        {{/each}}
    </div>
    <div class="tab-body">
        {{html $item.html("div")[$item.viewState.index]}}
    </div>
</script>

Здесь в DIV.tab-head содержатся ярлыки закладок, а в DIV.tab-body – содержимое выбранной закладки.

Вызов $item.html(«h1», true)} производит выборку данных. Первый параметр (filter) задает фильтр для элементов первого уровня, а второй параметр (textOnly) указывает, что я хочу получить только inner text каждого выбранного элемента – т.е. в данном случае я получу коллекцию строк, каждая из которых будет содержать текст из соответствующего тега H1. Из этой коллекции строк я и создаю ярлыки закладок.

Меня немного расстраивает тот факт, что я не могу указать в методе html() произвольный селектор – но, к счастью, мне никто не мешает использовать на результатах этого метода любые селекторы jQuery.

Еще один забавный момент связан со способом задания селектора – при попытке задать селектор в моих любимых одинарных кавычках при компиляции шаблона возникает ошибка.

Следующим шагом я выбираю из исходных данных DIV, соответствующий активной закладке, и помещаю его в DIV.tab-body. Обратите внимание, что в данном случае я использую тег {{html}}, а не ${...} – дело в том, что при использовании тега ${...} выполняется экранирование специальных символов, а мне это в данном случае не нужно.

Наконец, я инстанцирую шаблон:

var viewState = { index: 0 };
$('#tabsContent').tmpl(null, { viewState: viewState }).appendTo('#tabsBag');

И устанавливаю обработчики нажатий на закладки:

$('#tabsBag').delegate('.tab', 'click', function () {
    var item = $.tmplItem(this);
    item.viewState.index = $(this).index();
    item.update();
});

Что же происходит в обработчике нажатия? Во-первых, с помощью вызова $.tmplItem(this) я получаю ссылку на экземпляр шаблона, связанный с текущим элементом. Затем, я меняю индекс выбранной закладки – помните, что у нас в каждом экземпляре шаблона содержится ссылка на объект viewState? И в финале я вызываю метод update() экземпляра шаблона, что вызывает повторный рендеринг шаблона, но уже с другим значением во viewState.

Следующий пример более сложный, но зато и более полезный.

Первое его отличие от предыдущего примера заключается в том, что исходные данные для трансформации я создаю динамически (полный код примера – в файле Transformation2.htm):

<script id="movieListTmpl" type="text/x-jquery-tmpl">
    {{wrap(null, {viewState: $item.viewState}) '#tabsTmpl'}}
        {{tmpl(dataItems) '#movieTmpl'}}
    {{/wrap}}
        </script>
<script id="movieTmpl" type="text/x-jquery-tmpl">
    <div class="movie-bag">
        <img src="Content/Thumbnails/${thumbnail}" class="thumbnail" />
        ...
    </div>
</script>

var viewState = { index: 0 };
$('#movieListTmpl').tmpl({ dataItems: dataItems }, { viewState: viewState }).appendTo('#tabsBag');

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

<script id="tabsTmpl" type="text/x-jquery-tmpl">
    <div class="tab-head">
        {{each $item.html("div")}}
            <div class="tab {{if $index == $item.viewState.index}}active{{/if}}">
                ${$('h2', $value).text()}
            </div>
        {{/each}}
    </div>
    <div class="tab-body">
        {{html $item.html("div")[$item.viewState.index]}}
    </div>
</script>

В браузере этот пример будет выглядеть так:



Кэширование шаблонов


В каждом вызове $('#...').tmpl(...) происходит компиляция шаблона, что, несмотря на резко возросшую скорость работы JavaScript в современных браузерах, может крайне негативно сказаться на производительности. Разработчики jQuery Templates никак не могли обойти своим вниманием этот очевидный факт, поэтому в jQuery Templates предусмотрен механизм для предварительной компиляции и кэширования шаблонов.

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

$('#movieTmpl').template('movieTmpl');

Скомпилированный шаблон сохраняется во внутреннем кэше jQuery Templates под именем movieTmpl. Для обращения к кэшированному шаблону используется метод jQuery.tmpl(), и первым параметром ему передается имя кэшированного шаблона:

$.tmpl('movieTmpl', dataItems).appendTo('#movieListBag');

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

Код шаблона практически не отличается от тех, что я использовал ранее, единственное его отличие – под описанием фильма дополнительно размещаются ссылки для навигации (полный код примера – в файле CachedTemplates.htm):

<script id="movieTmpl" type="text/x-jquery-tmpl">
    <div class="movie-bag">
        <img src="Content/Thumbnails/${thumbnail}" class="thumbnail" />
        <div class="base-info">
            <h2>
                ${title}
            </h2>
            <p>
                Режиссер: ${director}<br />
                В ролях: ${actors}<br />
                Год: ${year}
            </p>
        </div>
    </div>
    <div>
        {{if $item.canMoveBack}}
            <a href="javascript:" class="nav-link" x-inc="-1">[Назад]</a>
        {{/if}}
        {{if $item.canMoveFwd}}
            <a href="javascript:" class="nav-link" x-inc="1">[Вперед]</a>
        {{/if}}
    </div>
</script>

Сопутствующий скрипт так же несложен:

var movieIndex = 0;

$(function () {
    $('#movieTmpl').template('movieTmpl');

    updateMovie();

    $('#movieBag').delegate('.nav-link', 'click', function () {
        movieIndex += parseInt($(this).attr('x-inc'));
        updateMovie();
    });
});

function updateMovie() {
    $('#movieBag').empty();

    $('#movieBag').append(
        $.tmpl('movieTmpl',
        dataItems[movieIndex],
        {
            canMoveBack: movieIndex > 0,
            canMoveFwd: movieIndex < dataItems.length - 1
        }));
}

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

Вот как этот пример выглядит в браузере:



Динамическая загрузка шаблонов


К сожалению, код, показанный ниже, работать не будет:

<script id="movieTmpl" src="Templates/DynamicLoading.htm" type="text/x-jquery-tmpl"></script>

Браузер, конечно, загрузит соответствующий файл – но вот получить доступ к его содержимому у вас все равно не получится.

Но шаблон все-таки можно разместить в отдельном файле, и для этого потребуется буквально одна дополнительная строчка кода (полный код примера – в файле DynamicLoading.htm):

$(function () {
    $.get('Templates/DynamicLoading.htm', {}, function (templateBody) {
        $.tmpl(templateBody, dataItems).appendTo('#movieListBag');
    });
});

Т.к. в данном случае мы получаем шаблон в виде текста, для его инстанцирования применяется метод jQuery.tmpl(), первым аргументом которому передается полученный текст шаблона.

Да, метод jQuery.tmpl() используется для инстанцирования как кэшированных шаблонов по имени, так и шаблонов, заданных в виде текста (традиция!..) – впрочем, он достаточно «умен», чтобы отличить их друг от друга.

Если вам нужно загрузить несколько связанных шаблонов, вы можете воспользоваться библиотекой WaitSync (см. «Синхронизация асинхронных вызовов. WaitSync») или написать свой синхронизатор (полный код примера – в файле DynamicLoading2.htm):

$(function () {
    var ws = new WaitSync(function () {
        $.tmpl('movieTmpl', dataItems).appendTo('#movieListBag');
    });

    $.ajax({
        cache: false,
        url: 'Templates/MovieTmpl.htm',
        success: ws.wrap('MovieTmpl', function (templateBody) {
            $.template('movieTmpl', templateBody);
        }),
        error: ws.wrap('MovieTmpl', function () {
            alert('Error loading MovieTmpl.htm!');
        })
    });

    $.ajax({
        cache: false,
        url: 'Templates/ActorsTmpl.htm',
        success: ws.wrap('ActorsTmpl', function (templateBody) {
            $.template('actorsTmpl', templateBody);
        }),
        error: ws.wrap('ActorsTmpl', function () {
            alert('Error loading ActorsTmpl.htm!');
        })
    });
});

Обратите внимание, что в данном случае шаблон actorsTmpl вызывается по имени (файл Templates\MovieTmpl.htm):

<p>
    Режиссер: ${director}<br />
    В ролях: {{tmpl(actors, { last: actors[actors.length - 1] }) 'actorsTmpl'}}<br />
    Год: ${year}
</p>


Динамическое обновление


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

В приведенном ниже примере для каждого фильма добавлена возможность изменить его рейтинг (полный код примера – в файле DynamicUpdate1.htm):

<script id="movieTmpl" type="text/x-jquery-tmpl">
    <div class="movie-bag">
        <img src="Content/Thumbnails/${thumbnail}" class="thumbnail" />
        <div class="base-info">
            ...
            <p>
                Рейтинг:
                <img src="Images/rating-down.png" alt="" title="-1" class="rating-button" x-inc="-1" />
                ${rating}
                <img src="Images/rating-up.png" alt="" title="+1" class="rating-button" x-inc="1" />
            </p>
        </div>
    </div>
</script>

Сопутствующий код:

$(function () {
    $('#movieTmpl').tmpl(dataItems).appendTo('#movieListBag');

    $('#movieListBag').delegate('.rating-button', 'click', function () {
        var item = $.tmplItem(this);
        item.data.rating += parseInt($(this).attr('x-inc'));
        item.update();
    });
});

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

В браузере этот пример выглядит так:



Следующий пример демонстрирует подмену связанного шаблона (полный код примера – в файле DynamicUpdate2.htm):

<script id="movieShortTmpl" type="text/x-jquery-tmpl">
    <div class="movie-bag">
        {{tmpl '#movieMainTmpl'}}
        <p style="clear: both"><a href="javascript:" class="more-details">[Больше...]</a></p>
    </div>
</script>
<script id="movieFullTmpl" type="text/x-jquery-tmpl">
    <div class="movie-bag">
        {{tmpl '#movieMainTmpl'}}
        <p style="clear: both">Кадры из фильма:</p>
        <div>
            {{each frames}}
                <img src="Content/Frames/${$value}" />
            {{/each}}
        </div>
        <p><a href="javascript:" class="more-details">[Меньше...]</a></p>
    </div>
</script>

Здесь я использую два шаблона, movieShortTmpl и movieFullTmpl, общая часть которых вынесена в шаблон movieMainTmpl.

Сопутствующий код:

$(function () {
    var shortTemplate = $('#movieShortTmpl').template('shortTemplate');
    var fullTemplate = $('#movieFullTmpl').template();

    $.tmpl('shortTemplate', dataItems).appendTo('#movieListBag');

    $('#movieListBag').delegate('.more-details', 'click', function () {
        var item = $.tmplItem(this);
        item.tmpl = item.tmpl === shortTemplate ? fullTemplate : shortTemplate;
        item.update();
    });
});

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

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

Код же обработчика нажатия на ссылки «Больше/меньше» очень похож на код из предыдущего примера, в нем я присваиваю полю tmpl ссылку на один из скомпилированных ранее шаблонов и вызываю метод update() для рендеринга.

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



Заключение


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

Загрузить jQuery Templates можно с web-сайта ASP.NET CDN или напрямую из репозитария GitHub:

Документация по jQuery Templates доступна на сайте документации jQuery.

В примерах я использовал jQuery 1.5 RC1, но работать jQuery Templates будут, начиная с версии jQuery 1.4.2 – в моем последнем проекте была использована именно такая связка.

Ссылки, приведенные ниже, помогут вам больше узнать о jQuery Templates:

Чтобы получить представление о производительности jQuery Templates по сравнению с другими «движками шаблонов», взгляните на статью Brian Landau «Benchmarking Javascript Templating Libraries».

И в заключение я хочу выразить благодарность Виталию Дильмухаметову и Денису Гладких за ценные замечания, сделанные в процессе работы над статьей.
Tags:
Hubs:
+193
Comments 67
Comments Comments 67

Articles