Прошлую свою статью я посветил описанию «велосипеда» (загрузчика и шаблонизатора в рамках «легкого» framework’а). Волею судеб, для пары проектов я был вынужден выделить шаблонизатор и сделать его standalone версию, обогатив при этом рядом новых возможностей. Именно об front-end шаблонизаторе и пойдет речь.

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

  • Вы front-end разработчик, и вам интересно использование шаблонов.
  • Вы back-end разработчик, и вам интересно использование шаблонов на front-end’е.
  • Вы давно ищете какой-нибудь инструмент для систематизации своей коллекции UI-control'ов, накопившуюся за несколько лет.
  • Вы интересуетесь разработкой web-компонентов.
  • Вам хочется высказать критические замечания и порекомендовать angularJS.
  • У вас есть свободное время и вам интересно почитать об очередном велосипеде.
  • У вас нет свободного времени, но вам интересно.
  • Вы хороший и любознательный человек.



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

Например, шаблон в patterns – это просто HTML страница и ничего больше. Никакого специфичного синтаксиса вроде того, что используется в EJS и многих других шаблонизаторах.

<ul>
<% for(var i=0; i<supplies.length; i++) {%>
   <li><%= supplies[i] %></li>
<% } %>
</ul>


Весь синтаксис patterns ограничивается тремя определениями:

  • Hook. {{name_of_hook}}. Зацепка, с помощью которой вы можете помечать места в шаблоне для вставки контента.
  • Model. {{::name_of_ reference }}. Так вы можете указать на свойство узла или его атрибут, который должен быть связан с объектом модели для дальнейшего манипулирования.
  • DOM. {{$name_of_reference}}. Указав с помощью этой метки на узел, вы получите возможность очень быстро обращаться к данному узлу, изменять его, прикреплять события и делать прочие рутинные вещи.


Создание шаблона



Ну давайте на примере. Создадим popup для авторизации пользователя. Нам понадобятся четыре шаблона:

  • Всплывающее окно — html.
  • Разметка для окна авторизации — html.
  • Поле для текста — html.
  • Кнопка — html.


Ниже шаблон для всплывающего окна (popup).

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Flex.Template</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <link rel="stylesheet" type="text/css" href="pattern.css" />
</head>
<body>
    <div data-style="Popup" id="{{id}}">
        <div data-style="Popup.Container">
            <div data-style="Popup.Title">
                <p data-style="Popup.Title">{{title}}</p>
            </div>
            <div data-style="Popup.Content">{{content}}</div>
            <div data-style="Popup.Bottom">
                <p data-style="Popup.Bottom">{{bottom}}</p>
            </div>
        </div>
    </div>
</body>
</html>


Как вы уже заметили – это самый обыкновенный HTML файл. В HEAD вы можете подключать CSS и JS файлы, которые будут автоматически подключены вместе с шаблоном и закэшированы.

Кэширование – это важная часть patterns. И сами шаблоны (HTML) и ресурсы (CSS и JS) сохраняются в localStorage, а это значит, что при повторном использовании шаблона, все данные будут взяты не с сервера, а с клиента, что самым благоприятным образом сказывается на скорости отрисовки. Кроме того patterns сам следит за актуальностью кэша: всякий раз patterns запрашивает HEADERs всех шаблонов и их ресурсов; и если что-то изменилось, patterns самостоятельно обновит кэш, чтобы поддерживать всю систему в актуальном состоянии. Но, вернемся к нашему окну авторизации.

Шаблон разметки (далее буду приводить только содержание тега BODY, для экономии места)

<div data-type="Pattern.Login">
	<p>Login</p>
	{{login}}
	<p>Password</p>
	{{password}}
	<div data-type="Pattern.Controls">{{controls}}</div>
</div>


Поле для ввода текста (в нашем случае это будет логин и пароль)

<p>{{::value}}</p>
<div data-type="TextInput.Wrapper">
	<div data-type="TextInput.Container">
		<input data-type="TextInput" type="{{type}}" value="{{::value}}" name="TestInput"/>
	</div>
</div>


Обратите внимание на то, что мы связали INPUT.value и P.innerHTML через переменную названную value, используя приведенную выше метку {{::value}}. Таким образом, если какой-то текст будет введен в INPUT, то он будет отображен и в связанном параграфе. Кроме того, созданная переменная value будет помещена в модель.

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

    <a data-type="Buttons.Flat" id="{{id}}">{{title}}</a>


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

Присоединение шаблона



Шаблон может быть присоединен к странице (то есть отрисован) двумя способами:

  • Через вызов JavaScript метода
  • Через HTML разметку.


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

Отрисовка через JavaScript



За рендеринг шаблона отвечает метод get — _patterns.get(), который вернет экземпляр класса шаблона, который вы можете смонтировать (прикрепить к указанному узлу), через метод – render. Взгляните на пример ниже и все станет ясно.

var id = flex.unique();
_patterns.get({
	url     : '/patterns/popup/pattern.html',
	node    : document.body,
	hooks   : {
		id      : id,
		title   : 'Test dialog window',
		content : _patterns.get({
			url     : '/patterns/patterns/login/pattern.html',
			hooks   : {
				login   : _patterns.get({
					url     : '/patterns/controls/textinput/pattern.html',
					hooks   : {
						type: 'text',
					}
				}),
				password: _patterns.get({
					url     : '/patterns/controls/textinput/pattern.html',
					hooks   : {
						type: 'password',
					}
				}),
				controls: _patterns.get({
					url     : '/patterns/buttons/flat/pattern.html',
					hooks   : [{ title: 'login', id: 'login_button' }, { title: 'cancel', id: 'cancel_button' }]
				}),
			}
		})
	}
}).render();


Самый важный параметр – это url, где мы указываем место, откуда брать шаблон. Не менее важный параметр – это hooks. Помните в шаблонах мы указывали места для контента через метку – {{name}}. В параметре hooks, мы определяем контент для каждой такой метки.

Полное описание всех параметров, которые принимает метод _patterns.get(), вы можете найти здесь. А на результат этого примера можно посмотреть тут.

Но идем дальше.

Отрисовка через HMTL разметку



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

    <pattern src="/patterns/popup/pattern.html" style="display:none;">
        <id>0</id>
        <title>Test dialog window</title>
        <content src="/patterns/patterns/login/pattern.html">
            <login src="/patterns/controls/textinput/pattern.html">
                <type>text</type>
            </login>
            <password src="/patterns/controls/textinput/pattern.html">
                <type>password</type>
            </password>
            <controls src="/patterns/buttons/flat/pattern.html">
                <id>login_button</id><title>login</title>
                <id>cancel_button</id><title>cancel</title>
            </controls>
        </content>
    </pattern>


В данном случае для определения hook’ов мы используем одноименные теги. То есть приведенные ниже две конструкции по своему смыслу идентичные.

<pattern src="/patterns/popup/pattern.html" style="display:none;">
	<id>0</id>
	<title>Test dialog window</title>
	...
</pattern>


_patterns.get({
		url     : '/patterns/popup/pattern.html',
		hooks   : {
			id      : 0,
			title   : 'Test dialog window',
			...
		}
}).render();


Обратите внимание, что тег PATTERN мы используем только для корневого узла, а дальше лишь добавляем свойство SCR, чтобы обозначить, что в качестве контента hook (зацепки) будет использоваться вложенный шаблон.

То есть следующая разметка означает, что patterns должен найти шаблон по адресу, указанному в SRC и применить его с hook’ом type в значении “text”.

<login src="/patterns/controls/textinput/pattern.html">
	<type>text</type>
</login>


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

Повторение шаблона



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

Шаблон таблицы.

<table data-type="Demo.Table">
	<tr>
		<th>{{titles.column_0}}</th>
		<th>{{titles.column_1}}</th>
		<th>{{titles.column_2}}</th>
		<th>{{titles.column_3}}</th>
	</tr>
	{{rows}}
</table>


И шаблон строки в таблице.

<tr>
	<td>{{column_0}}</td>
	<td>{{column_1}}</td>
	<td>{{column_2}}</td>
	<td>{{column_3}}</td>
</tr>


Имея эти два шаблона и данные, мы можем отрисовать нашу таблицу.

var data_source = [];
for (var i = 0; i < 100; i += 1) {
	data_source.push({
		column_0: (Math.random() * 1000).toFixed(4),
		column_1: (Math.random() * 1000).toFixed(4),
		column_2: (Math.random() * 1000).toFixed(4),
		column_3: (Math.random() * 1000).toFixed(4),
	});
}
_patterns.get({
	url: '/patterns/table/container/pattern.html',
	node: document.body,
	hooks: {
		titles: {
			column_0: 'Column #0',
			column_1: 'Column #1',
			column_2: 'Column #2',
			column_3: 'Column #3',
		},
		rows: _patterns.get({
			url: '/patterns/table/row/pattern.html',
			hooks: data_source,
		})
	}
}).render();


Здесь вы можете найти работающий пример.

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

<controls src="/patterns/buttons/flat/pattern.html">
	<id>login_button</id><title>login</title>
	<id>cancel_button</id><title>cancel</title>
</controls>


Так же обратите внимание на то, что имена hook’ов в заголовках определены через точку {{titles.column_0}}, что позволяет нам в функции рендеринга использовать более осмысленное определение их значений. Так, все заголовки определяются в объекте titles.

Контролеры и функции обратного вызова



По сути в patterns контролер и функция обратного вызова – это одно и тоже. Отличие лишь в месте хранения.

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

_patterns.get({
    url     : 'some_url',
    callbacks: {
        //Callback-function definition
        success: function (results) {
            var instance    = this,
                dom         = results.dom,
                model       = results.model,
                binds       = results.binds,
                map         = results.map,
                resources   = results.resources;
            ...
        }
    },
}).render();


А вот чтобы создать контролер, нужно создать JS файл следующего содержания

_controller(function (results) {
    var instance    = this,
        dom         = results.dom,
        model       = results.model,
        binds       = results.binds,
        map         = results.map,
        resources   = results.resources;
    ...
});


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

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Flex.Template</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <link rel="stylesheet" type="text/css" href="pattern.css" />
    
    <!-- Attach controller of template -->
    <script type="text/javascript" src="conroller.js"></script>
    
</head>
<body>
    <div data-style="Popup">
        <div data-style="Popup.Container">
            <div data-style="Popup.Title">
                <p data-style="Popup.Title">{{title}}</p>
            </div>
            <div data-style="Popup.Content">{{content}}</div>
            <div data-style="Popup.Bottom">
                <p data-style="Popup.Bottom">{{bottom}}</p>
            </div>
        </div>
    </div>
</body>
</html>


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

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

Модель и связи



Два важных объекта, которые вы получаете – это model и binds

var model = results.model;
var binds = results.binds;


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

<tr>
	<td style="background:{{::background_0}};">{{column_0}}{{::column_0}}</td>
	<td style="background:{{::background_1}};">{{column_1}}{{::column_1}}</td>
	<td style="background:{{::background_2}};">{{column_2}}{{::column_2}}</td>
	<td style="background:{{::background_3}};">{{column_3}}{{::column_3}}</td>
</tr>


Как вы видите мы добавили пару связей. Во-первых, мы связали свойство background каждой ячейки с переменной background_n. То же самое мы сделали и со значениями самих ячеек, связав их с переменной column_n.

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

_patterns.get({
	...
	callbacks   : {
		success: function (results) {
			(function (model) {
				var fun = function () {
					var r = Math.round(19 * Math.random()),
						c = Math.round(3 * Math.random());
					model.__rows__[r]['column_' + c] = (Math.random() * 1000).toFixed(4);
					model.__rows__[r]['background_' + c] = 'rgb(' + Math.round(255 * Math.random()) + ', ' + Math.round(255 * Math.random()) + ', ' + Math.round(255 * Math.random()) + ')';
					setTimeout(fun, Math.ceil(50 * Math.random()));
				};
				fun();
			}(results.model));
		}
	}
}).render();


Посмотреть на слетевшую с катушек таблицу можно здесь.

Итак, объект model содержит ссылки на связанные значения. Обратите внимание на свойство __rows__. Через данную конструкцию __hook__, обозначаются уровни вложенности hook’ов. Так как данные содержатся не в корневом шаблоне (шаблоне таблице), а вложены в hook rows, то и доступ к ним возможен через model.__rows__. Двойное же подчеркивание введено как превентивная мера от конфликтов имен.

Если вы помните, то в шаблоне окна авторизации мы связывали INPUT.value с P.innerHTML. В функции обратного вызова мы так же получаем и ссылку на value.

_patterns.get({
	url     : '/patterns/popup/pattern.html',
	node    : document.body,
	hooks   : {
		id      : id,
		title   : 'Test dialog window',
		content : _patterns.get({
			url     : '/patterns/patterns/login/pattern.html',
			hooks   : {
				login   : _patterns.get({
					url     : '/patterns/controls/textinput/pattern.html',
					hooks   : {
						type: 'text',
					}
				}),
				password: _patterns.get({
					url     : '/patterns/controls/textinput/pattern.html',
					hooks   : {
						type: 'password',
					}
				}),
				controls: _patterns.get({
					url     : '/patterns/buttons/flat/pattern.html',
					hooks   : [{ title: 'login', id: 'login_button' }, { title: 'cancel', id: 'cancel_button' }]
				}),
			}
		})
	},
	callbacks: {
		success: function (results) {
			var instance    = this,
				model       = results.model;
			model.__content__.__login__.value = 'this new login';
		}
	},
}).render();


С model разобрались, но что же такое binds? А binds по своей структуре тоже самое что и model, за одним лишь исключением – «на конце» не значение, а методы.

success: function (results) {
	var instance    = this,
		dom         = results.dom,
		binds       = results.binds,
		id          = null;
	//Add handle
	id = binds.__content__.__login__.value.addHandle(function (name, value) {
		var obj = this;
	});
	//Remove handle
	binds.__content__.__login__.value.removeHandle(id);
}


И их (методов) всего два:

  • addHandle
  • removeHandle


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

DOM и карта



Еще два интересных объекта – это DOM и map.

var dom = results.dom;
var map = results.map;


Изменим немного шаблон кнопки для нашего окна авторизации, чтобы продемонстрировать возможности объекта dom.

    <a data-type="Buttons.Flat" id="{{id}}" {{$button}}>{{title}}</a>


Итак, мы добавили ссылку {{$button}} к узлу кнопки. Таким образом, мы отметили узел, в отношении которого patterns создаст коллекцию методов для работы с данным узлом.

success: function (results) {
    var instance    = this,
        dom         = results.dom;
    dom.listed.__content__.__controls__[0].button.on('click', function () {
        alert('You cannot login. It\'s just test. Login is "' + model.__content__.__login__.value + '", and password is "' + model.__content__.__password__.value + '"');
    });
    dom.listed.__content__.__controls__[1].button.on('click', function () {
        alert('Do not close me, please.');
    });
    dom.grouped.__content__.__controls__.button.on('click', function () {
        alert('This is common handle for both buttons');
    });
}


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

Здесь же я лишь обращу ваше внимание на то, что объект dom имеет два свойства:

  • grouped
  • listed


Первое свойство содержит сгруппированные методы. То есть, так как на форме у нас две кнопки, то при обращении, например, к методу on (прикрепление событий), мы прикрепим событие сразу к двум кнопкам. Если же нам нужен доступ к каждой отдельной кнопке, то нам необходимо использовать свойство listed.

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

success: function (results) {
    var instance    = this,
        map         = results.map,
        nodes       = null;
    //Will find all P in whole popup
    nodes = map.__context.select('p');
    //Will find all P inside popup in content area
    nodes = map.content.__context.select('p');
    //Will find all P in textbox-control of login
    nodes = map.content.login.__context.select('p');
}


То есть map.content.login.__context.select('p') будет искать все параграфы только внутри части шаблона, относящейся к шаблону текстового поля, определенному для указания логина.

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

Обмен данными



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

_patterns.get({
    url     : '/patterns/popup/pattern.html',
    node    : document.body,
    hooks   : {
        id      : id,
        title   : 'Test dialog window',
        content : _patterns.get({
            url     : '/patterns/login/pattern.html',
            hooks   : {
                login   : _patterns.get({
                    url     : '/patterns/controls/textinput/pattern.html',
                    hooks   : {
                        type: 'text',
                    }
                }),
                password: _patterns.get({
                    url     : '/patterns/controls/textinput/pattern.html',
                    hooks   : {
                        type: 'password',
                    }
                }),
                controls: _patterns.get({
                    url     : '/patterns/buttons/flat/pattern.html',
                    hooks   : [{ title: 'login', id: 'login_button' }, { title: 'cancel', id: 'cancel_button' }]
                }),
            },
        })
    },
    resources: {
        field1  : 'one',
        field2  : 'two'
    },
    callbacks: {
        success: function (results) {
            var instance    = this,
                resources   = results.resources;
            window.console.log(resources.field1);
            window.console.log(resources.field2);
            //Result in console:
            //one
            //two
        }
    },
}).render();


Именно оно и будет передано в функцию обратного вызова, как это продемонстрировано в примере. Таким образом, вы получаете возможность обмена данными между моментами: до отрисовки и после.

Условия или меняющиеся шаблоны



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

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

<ul>
<% for(var i=0; i<supplies.length; i++) {%>
   <li><%= supplies[i] %></li>
<% } %>
</ul>


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

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Flex.Template</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <link rel="stylesheet" type="text/css" href="pattern.css" />
    <!--Attach JS file with condition-handle-->
    <script type="text/javascript" src="conditions.js"></script>
</head>
<body>
    <p>{{::value}}</p>
    <div data-type="TextInput.Wrapper">
        <div data-type="TextInput.Container">
            <input data-type="TextInput" type="{{type}}" value="{{::value}}" name="TestInput" {{$input}}/>
        </div>
        <!--type=password-->
        <div data-type="TextInput.Info.Icon"></div>
        <div data-type="TextInput.Info.Popup">
            <p>You can use in password only letters, number and _</p>
        </div>
        <!--type-->
    </div>
</body>
</html>


Итак, как вы видите, мы добавили немного новой разметки, а именно:

<!--[condition_name]=[condition_value]-->
<div data-type="TextInput.Info.Icon"></div>
<div data-type="TextInput.Info.Popup">
	<p>You can use in password only letters, number and _</p>
</div>
<!--[condition_name]-->


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

Так же вы не могли не заметить и прикрепленного JS файла – conditions.js. Вот его содержание:

_conditions({
    type: function (data) {
        return data.type;
    }
});


Как вы можете видеть, там определена функция (type) соответствующая названию условия в разметке .

Так что же произойдет после отрисовки обновленного шаблона окна авторизации? Логика действий patterns будет довольно простой: обнаружив условия в шаблоне текстового поля, patterns попытается найти функцию type (по имени условия). Найдя эту функцию, patterns передаст ей значения hook'ов (аргумент функции – data). Если эта функция вернет определенное в условии значение password, то дополнительная часть разметки будет включена в шаблон.

Здесь работающий пример нашего обновленного окна авторизации.

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

_patterns.get({
    url     : '/patterns/popup/pattern.html',
    node    : document.body,
    hooks   : {
        id      : id,
        title   : 'Test dialog window',
        content : _patterns.get({
            url     : '/patterns/login/pattern.html',
            hooks   : {
                login   : _patterns.get({
                    url     : '/patterns/controls/textinput/pattern.html',
                    hooks   : {
                        type: 'text',
                    },
                    conditions : {
                        type: function (data) {
                             return data.type;
                        }
                    },
                }),
                password: _patterns.get({
                    url     : '/patterns/controls/textinput/pattern.html',
                    hooks   : {
                        type: 'password',
                    },
                    conditions : {
                        type: function (data) {
                             return data.type;
                        }
                    },
                }),
                controls: _patterns.get({
                    url     : '/patterns/buttons/flat/pattern.html',
                    hooks   : [{ title: 'login', id: 'login_button' }, { title: 'cancel', id: 'cancel_button' }]
                }),
            },
        })
    },
}).render();


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

  • Во-первых, имея условия, определенные по умолчанию (это те что прикреплены к шаблону в виде отдельного JS), мы получаем возможность переопределить их, не влезая в шаблон – через функцию рендеринга (как это показано выше). Таким образом нам не нужно плодить кучу компонентов, отличающихся совсем немного, так как мы всегда можем немного «поправить» логику под текущие нужды.
  • Во-вторых, и это для меня главное – у нас есть возможность быстро пересобрать шаблон, если вводные данные изменились. Что бы было яснее: обычный шаблонизатор, анализирует условия и делает итоговый шаблон, который и монтируется в разметку; если данные изменились, нужно удалять отрисованный экземпляр и собирать шаблон заново. Подход с функциями-условиями позволяет обойтись без этой дорогой операции и пересобрать лишь небольшой кусок шаблона, к которому относится условие.


Чтобы лучше понять «во-вторых» давайте изменим шаблон строки для нашей таблицы.

<tr>
	<td>{{column_0}}{{::column_0}}</td>
	<td>{{column_1}}{{::column_1}}</td>
	<td>{{column_2}}{{::column_2}}</td>
	<td>
		<div>
			<p>{{column_3}}{{::column_3}}</p>
			<!--value_sets=0-->
				<!--sub_value_sets=0-->
					<p>This value is less than 111</p>
				<!--sub_value_sets-->
				<!--sub_value_sets=0.5-->
					<p>This value is more than 111 and less than 222</p>
				<!--sub_value_sets-->
				<!--sub_value_sets=1-->
					<p>This value is more than 222 and less than 333</p>
				<!--sub_value_sets-->
			<!--value_sets-->
			<!--value_sets=0.5-->
				<p>This value is more than 333 and less than 666</p>
			<!--value_sets-->
			<!--value_sets=1-->
				<p>This value is more than 666 and less than 1000</p>
			<!--value_sets-->
		</div>
	</td>
</tr>


Выглядит все мудрено, правда? Но взглянув на функции-условия, станет все ясно.

var conditions = {
	value_sets: function (data) {
		if (data.column_3 <= 333                        ) { return '0';     }
		if (data.column_3 > 333 && data.column_3 <= 666 ) { return '0.5';   }
		if (data.column_3 > 666                         ) { return '1';     }
	},
	sub_value_sets: function (data) {
		if (data.column_3 <= 111                        ) { return '0';     }
		if (data.column_3 > 111 && data.column_3 <= 222 ) { return '0.5';   }
		if (data.column_3 > 222                         ) { return '1';     }
	},
};
conditions.value_sets.      tracking = ['column_3'];
conditions.sub_value_sets.  tracking = ['column_0'];
_conditions(conditions);


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

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

Давайте запустим рендеринг, добавив немного динамики.

var data_source = [];
for (var i = 0; i < 100; i += 1) {
	data_source.push({
		column_0: (Math.random() * 1000).toFixed(4),
		column_1: (Math.random() * 1000).toFixed(4),
		column_2: (Math.random() * 1000).toFixed(4),
		column_3: (Math.random() * 1000).toFixed(4),
	});
}
_patterns.get({
	url         : '/patterns/table/container/pattern.html',
	node        : document.body,
	hooks       : {
		titles  : {
			column_0: 'Column #0',
			column_1: 'Column #1',
			column_2: 'Column #2',
			column_3: 'Column #3',
		},
		rows    : _patterns.get({
			url: '/patterns/table/row_con/pattern.html',
			hooks: data_source,
		})
	},
	callbacks   : {
		success: function (results) {
			(function (model) {
				var fun = function () {
					var r = Math.round(99 * Math.random()),
						c = Math.round(3 * Math.random());
					model.__rows__[r]['column_' + c] = (Math.random() * 1000).toFixed(4);
					setTimeout(fun, Math.ceil(50 * Math.random()));
				};
				fun();
			}(results.model));
		}
	}
}).render();


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

Вместо завершения



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

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

  • Благодаря тому, что шаблон – это всего лишь HTML и никакого нестандартного синтаксиса не используется, шаблоны можно запускать отдельно от страницы и отлаживать.
  • Благодаря нестандартному подходу к условиям шаблон можно частично «пересобрать» без перезагрузки.
  • Благодаря встроенной системе кэширования весь шаблон (включая его ресурсы) будет храниться на стороне клиента, что снижает нагрузку на трафик.


Но это главные преимущества лишь для меня, для вас они могут быть другие, либо вообще отсутствовать.

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

Это страница проекта на github’е.

Спасибо больше за ваше внимание.
Поделиться с друзьями
-->

Комментарии (44)


  1. ChALkeRx
    05.06.2016 16:42
    +11


    Извините, но это всё, что надо знать о вашем шаблонизаторе на данный момент.


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


    1. AlexWriter
      05.06.2016 16:47

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


      1. ChALkeRx
        05.06.2016 16:53
        +6

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

        Неверно (если речь действительно про конкретную модель, а не про включение глобального флага «экранировать всё везде»).
        Так можно предотвратить только частный случай, но это не решает проблемы в целом — рано или поздно у вас в проекте окажется какая-то модель, где вы забыли поставить условную галочку «убрать xss» (как в этом примере).


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


        Категорично — именно поэтому. Для реальных проектов это нельзя использовать как минимум пока не будет исправлена эта проблема (выше я написал, почему). «На данный момент» — потому что я всё-таки надеюсь, что вы проблему исправите =).


        А по поводу архитектуры — в таких случая можно сделать opt-out из автоматического экранирования, например по коду шаблона или по типу передаваемой переменной (по-разному делают).


        1. ChALkeRx
          05.06.2016 17:04
          +4

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


          Поэтому, пожалуйста, сделайте подстановку значений в шаблон безопасной из коробки =).


          1. AlexWriter
            05.06.2016 17:53

            Добавлен новый параметр [serialize] в колекцию параметров метода _patterns.get(). Пока заменяет экранирует только теги. Но расширить функционал — не проблема. По умолчанию в true. Еще раз спасибо за предложение.
            ЗЫ
            Документацию обновлю позже )


            1. ChALkeRx
              05.06.2016 20:41
              +3

              Если вы про https://github.com/DmitryAstafyev/Patterns/commit/3cdd1ba89, то этого будет недостаточно — вы экранируете только < и >.


              Ничто не мешает <input value="{{value}}" /> превратиться в <input value="" onmouseover="…">, если я правильно понимаю.


              И ещё — этот коммит сломал поля ввода: когда я тут в поле набираю <3 и убираю фокус, видимое содержимое поля превращается в &lt;3.


              Вы точно хорошо подумали над архитектурой?


      1. gearbox
        05.06.2016 17:56

        Экранизацию я как-то из виду упустил. Но это не вопрос архитектуры.

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


        1. AlexWriter
          05.06.2016 18:00

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


          1. gearbox
            05.06.2016 18:50

            Есть еще вариант — не пришлось добавлять экранирование так как не требуется by design. Это тоже архитектура. Я ни в коем случае не пытаюсь покритиковать Ваш подход, просто напоминаю что есть и другие решения, если не Вам так тем кто будет это читать — может быть и полезно и интересно.


    1. Delphinum
      05.06.2016 17:20

      все подставляемые переменные должны экранироваться по умолчанию

      Зачем же так строго? Программисты достаточно умны, чтобы понять какие переменные опасны в модели, а какие опасными быть не могут впринципе.


      1. AlexWriter
        05.06.2016 17:54

        Я с вами согласен тоже, но добавил сериализацию. Пусть будет )


      1. sim3x
        05.06.2016 22:10
        +2

        Спорное утверждение


  1. kobezzza
    05.06.2016 18:12
    +2

    1. IvanPanfilov
      05.06.2016 19:03
      -1

      — namespace example
      — template helloWorld(name = 'world')
      < .hello
      Hello {name}!

      буээээ

      «зацените как мы извращаемся чтобы вывести Hello world! в div»


      1. kobezzza
        05.06.2016 19:06
        +2

        Только глупец будет оценивать инструмент по «Hello World».


        1. IvanPanfilov
          06.06.2016 07:39

          а он на что то еще годится?
          чего же тогда нет примера реального приложения?
          возможно потомучто слишком страшно получается?


          1. YNile
            06.06.2016 17:17
            +1

            Документация SS написана как раз на самом языке. Вполне нормально выглядит)


    1. AlexWriter
      06.06.2016 02:25
      +1

      Спасибо за ссылку. Почитал, очень интересно. У нас с вами разное целеполагание. У вас более глобально, что ли. У меня же все приземленно )). Моя задача была – шаблонизатор только для front-end.

      Да, есть возможность вставки шаблонов в HTML, но опять же и сборка шаблона, и вся работа по его анализу – все на клиенте.

      Кроме того, все началось с простой и банальной задачи. Я опишу, если позволите. Было приложение, со множеством разных «окошек» (админка в общем). И там была куча разных контролов. Почти все создавалось на серверной стороне. Потом пришла в голову мысль – а не перенести ли нам все это на клиента – нехай он сам парится, а на сервере все почистить, минимизировать и в идеале оставить только API. Посчитали количество контролов, вышли на несколько десятков очень простых элементов, которые могут работать автономно (то есть не зависят от какой-либо библиотеки и/или фреймвока). Тогда и пришла в голову мысль, а не распихать ли эти контролы по папочкам с JS и CSS, которые к ним относятся. Ну а на клиенте просто их подключать по мере необходимости.

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

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

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

      Иными словами, вот эта штука, которую я тут на ваш суд выставил – это такой инструмент, который позволяет представления вытаскивать с back-end на front-end. И главное, это позволяет проводить отладку каждого шаблона в отрыве от всего проекта. И такого рода инструмент он не универсален, безусловно, он для определенного круга задач.


  1. KingOfNothing
    05.06.2016 19:13
    +3

    Выглядит сложно. Какие преимущества по сравнению, например, с React`ом?


    1. k12th
      05.06.2016 22:13
      +4

      Автору не надо учить React, вот какое преимущество.


    1. AlexWriter
      05.06.2016 22:40
      -1

      Спасибо за комментарий. Я думаю, что сравнивать этот шаблонизатор с React нельзя — разные масштабы, да и React — это не шаблонизатор. Ну а то что сделал я — это для очень узкого круга задач — фактически это просто инструмент для создания не слишком сложных UI компонентов (по крайне мере я его так применяю). У React спект применения значительно, значительно шире.
      Для меня (я могу судить только по себе :)) главное приимущество: это возможность открывать шаблоны в отрыве от проекта, отлаживать их, править стили. Мне это удобно. Даже если компонент стал сложный, с логикой какой-то, то на создание тестовой страницы (что бы открыть его отдельно) уходит 3-и минуты и все, я снова могу отлаживать копмонент в отрыве от всего приложения.


      1. youlose
        06.06.2016 10:18
        +4

        Из официальной документации — «Lots of people use React as the V in MVC.»
        На мой взгляд Javascript в данный момент времени не испытывает недостатка в самых разнообразных шаблонизаторах, ЗАЧЕМ нужен ваш и почему не использовали готовые?


  1. AlexWriter
    05.06.2016 22:40

    .


  1. ChALkeRx
    06.06.2016 00:01
    +2

    Ещё замечание: у вас в единственном файле исходника количество строчек почти дошло до 10 тысяч (9820).
    Наверное, надо что-то разбить на более мелкие файлы и затем собирать их в один.


    1. AlexWriter
      06.06.2016 00:19
      -3

      Если посмотреть на код и свернуть блоки до 2-ой вложенности, то вы увидите 9-ть независимых друг от друга модулей (в том смысле, независимых, что они могут существовать отдельно). То что вы видите на github, да и на промо сайте — это уже результирующий файл.


      1. ChALkeRx
        06.06.2016 00:47
        +3

        То что вы видите на github, да и на промо сайте — это уже результирующий файл.

        Ну так выложите на гитхаб реальные исходники и скрипт для сборки из них тогда =).


        1. AlexWriter
          06.06.2016 01:07
          -10

          Поясните, пожалуйста, зачем выкладывать «расковырянную» версию и почему то, что лежит сейчас на github нереальные исходники? )

          Просто я искренне полагал (и полагаю), что рабочий «мусор» на паблике ни к чему. Есть файл, есть история с которой видны изменения ну и так далее.

          Если вам интересен какой-то отдельный модуль, то просто сверните код до второго уровня вложенности и вы увидите все девять модулей по отдельности. Да и станет очевидным то, как все это собирается.


          1. ChALkeRx
            06.06.2016 01:33
            +8

            Поясните, пожалуйста, зачем выкладывать «расковырянную» версию и почему то, что лежит сейчас на github нереальные исходники? )

            Исходники — предпочтительная для внесения изменений форма программы. Вам же самому удобнее вносить изменения в «расковыренную» версию, а эту вы собираете (или я не так понял?). Остальные люди тоже обычно не любят ковырять файлы на 10 тысяч строк, а разбивают на более мелкие куски.


            Следовательно, исходники — именно «расковырянная» версия.


  1. ChALkeRx
    06.06.2016 01:32

    Edit: промазал с ответом, извините. Переношу на нужный уровень.


  1. akzhan
    06.06.2016 02:09
    +3

    Сегодня на Хабре день велосипедов и неясных идей.


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


    И посмотрите на досуге Jade, например. Возможно, вам понравятся какие-нибудь идеи.


  1. Per_Ardua
    06.06.2016 09:31

    Спасибо за проделанную работу. Выглядит довольно красиво :)


    1. AlexWriter
      06.06.2016 11:31

      Спасибо )


  1. fetis26
    06.06.2016 09:51

      <pattern src="/patterns/popup/pattern.html" style="display:none;">

    Я не понял, а чем плох тег template?
    https://developer.mozilla.org/en/docs/Web/HTML/Element/template


    1. k12th
      06.06.2016 10:46

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


    1. AlexWriter
      06.06.2016 11:39
      -2

      Спасибо за комментарий.
      Потому что тег TEMPLATE часть стандарта, а мне не хотелось свое «нестандартное» решение смешивать со стандартами. Однако в patterns предусмотрена простая возможность переопределения корневого тега шаблона.



  1. Writerim
    06.06.2016 11:25

    Хотелось бы посмотреть сравнительные тесты например с React Js.
    «DOM. {{$name_of_reference}}. Указав с помощью этой метки на узел, вы получите возможность очень быстро обращаться к данному узлу, изменять его, прикреплять события и делать прочие рутинные вещи.»
    Особенно при работе с DOM.


    1. AlexWriter
      06.06.2016 12:16

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


  1. AlexWriter
    06.06.2016 12:18

    Спасибо всем за радужный прием)

    Из всего спектра комментариев лишь одно дельное замечание, про экранирование (упущенное из виду). Спасибо.
    На все остальное (там, где я проигнорировал) я могу ответить парой абзацев, потому как обсуждать, например, то, как проект выложен на github’е я смысла не вижу.

    Я писал свое решение, не потому что других решений не существует, а потому что хотел разобраться как это работает. Для меня это здоровая (от слова здоровье) мотивация разработчика. И если собственник проекта не против экспериментов, то для программиста – это находка и глупость упускать подобный шанс; шанс понять, как оно работает изнутри; шанс проверить себя – смогу / не смогу сделать работящее самостоятельное решение. Не пригодится в будущем? Возможно. Кому-то не понравится? Конечно! Но, хвала Зевсу, для меня это не главное и практически не важно.

    Видимо часть людей просто стала забывать, что разработка – это прежде всего творчество, искусство, возможность дать своей мысли форму. По крайне мере для меня – это именно так и именно по этой причине я пришел в эту профессию. Поэтому что я могу сказать? «Велосипедил» и буду «велосипедить» дальше, даже несмотря на то, что рядом лежит ReactJS, AngularJS, Jade, EJS и прочие тысячи инструментов.

    Еще раз спасибо за Ваше время.


    1. caballero
      06.06.2016 12:21

      я на описании своих велосипедов тоже выгребаю подобные коменты — привыкайте.


  1. caballero
    06.06.2016 12:19

    Не очень понятна идея тащить разметку и данные на клиента и там рендерить, выслушивая матюки владельцев мобильных девайсов.
    Гораздо логичнее рендерить класическим способом на сервере, пользуясь копеечной стоимостью облачных решений.
    А если уже делать это на клиенте то гораздо эффективнее затащить всю разметку одним запросом — а там и старый добрый Mustache (с декларативной, кстати, разметкой без всяких for и if else) вполне справится.


    1. AlexWriter
      06.06.2016 13:14

      Спасибо за ваш комментарий.

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

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

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


  1. mik222
    06.06.2016 17:26

    Идея любопытная.
    Посмотрите на доступе hiccup
    github.com/weavejester/hiccup
    — В чем его плюсы. Фантастическая композабельность. Решение с вечными скобочками через paredit


    1. AlexWriter
      06.06.2016 17:26

      Спасибо, обязательно посмотрю.