Как создать новый блок
July 27, 2015 · View on GitHub
Первый шаг мы сделали, теперь у нас есть страница test-page.page.js.
Блок, который мы создадим в этом руководстве, будем проверять именно на ней. Если вы её создали, тогда она выглядит точно как у нас:
module.exports = function (pages) {
pages.declare('index-page', function (params) {
var options = params.options;
return {
block: 'page',
title: 'index page',
styles: [
{url: options.assetsPath + '.css'}
],
scripts: [
{url: options.assetsPath + '.' + params.lang + '.js'}
],
body: [
// здесь ваши блоки
]
};
});
}
Если такой страницы у вас ещё нет, создайте её и возвращайтесь.
Как создать новый блок
Сделаем несложный блок, например, input. Перед тем, как его делать, попробуем его позвать. А вдруг?
module.exports = function (pages) {
pages.declare('test-page', function (params) {
var options = params.options;
return {
block: 'page',
title: 'test page',
styles: [
{url: options.assetsPath + '.css'}
],
scripts: [
{url: options.assetsPath + '.' + params.lang + '.js'}
],
body: [
{block: 'input'} // Добавьте вызов блока
]
};
});
}
Запустим приложение командой make и запросим в браузере нашу страницу localhost:8080/test/. Успех? В браузере
появился блок, хотя мы его даже не создавали?
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>test page</title>
<link rel="stylesheet" href="/build/test/_test.css">
</head>
<body class="page _init" data-block="page">
<div class="input" data-block="input"></div>
<script src="/build/test/_test.js" type="text/javascript"></script>
</body>
</html>
Шаблонизатор сам создал блок. Где-то внутри bt-шаблонизатора есть дефолтный шаблон, который генерит html-тег для вашего блока, даже если вы сами не писали никакого шаблона.
Это бывает удобно, но не в нашем случае. Блок input должен быть представлен в HTML как тег input,
поэтому нам без своего шаблона не обойтись. И без стилей и без js-поведения нам не обойтись. Поэтому создадим сразу
нужные файлы блока.
Создаём файлы
В консоли прервём приложение сочетанием клавиш Ctrl-C и воспользуемся инструментом для создания нового блока. Это небольшой bash-скрипт, который задаст вам один
вопрос, а после создаст сам блок:
make block
# Введите имя блока: input
На файловой системе появится директория blocks/input, а в ней несколько файлов.
Откройте в редакторе файл blocks/input/input.bt.js:
module.exports = function (bt) {
bt.match('input', function (ctx) {
ctx.setTag('span');
ctx.setContent('Содержимое блока');
});
};
У блока появился шаблон, который придумали мы, не вы. Это всё исправим, но сначала убедимся,
что новый шаблон наложился на блок input. Как это проверить? Обратите внимание,
в результате успешного наложения шаблона в HTML должен измениться тег, потому что в шаблоне есть два метода - один
меняет тег блоку, второй задаёт конент.
Сейчас там такой код:
<div class="input" data-block="input"></div>
После наложения шаблона должен стать таким:
<span class="input" data-block="input">'Содержимое блока'</span>
Опять запустим проект make, обновим в браузере localhost:8080/test/ и откроем HTML-код страницы. Ничего не
произошло — тег div не исчез, span вместо него не пояился, и контента нет и в помине.
Ошибка ли?
Смотрим в консоль. Нет сообщений об ошибке. Значит, не ошибка, это мы что-то не доделали.
Мы не описали зависимость. Что такое зависимости, прочтите здесь, сейчас для этого самое время. А если уже читали, тогда самое время добавить зависимость.
На странице test должен появиться блок input, следовательно страница "зависит" от блока.
Пока мы не сообщим странице эту зависимость, странице неоткуда брать информацию об этом.
Откройте файл pages/test-page/test-page.deps.yaml. В нём только одна строка
- page
Вы уже понимаете, это означает, что страница зависит от блока page.
И правда, в декларации страницы мы раньше звали только блок page, а теперь внутрь page мы позовём блок input,
поэтому опишем дополнительную зависимость:
- page
- input
Обновите в браузере страницу и загляните в HTML. Успех? Мы видим, что шаблон отработал,
потому что html-тег у блока изменился так, как мы того и хотели:
<span class="input" data-block="input">Содержимое блока</span>
Теперь напишем актуальный шаблон, который должен уметь...
а) генерить блок в виде тега input
б) устанавливать в тег значение, которые мы задали ему при декларации блока.
Пишем актуальный шаблон
Попробуем набросать первый вариант, открываем файл blocks/input/input.bt.js. Пока он выглядит как заготовка:
module.exports = function (bt) {
bt.match('input', function (ctx) {
ctx.setTag('span');
ctx.setContent('Содержимое блока');
});
};
Редактируем:
module.exports = function (bt) {
bt.match('input', function (ctx) {
ctx.setTag('input');
var currentValue = ctx.getParam('value');
ctx.setAttr('value', currentValue);
});
};
Разберём по-строчно.
ctx.setTag('input');
Метод setTag() генерит в конечном HTML тег, заданный единственным параметром.
var currentValue = ctx.getParam('value');
Метод getParam() принимает некий параметр "value" (откуда он, что за параметр - пока не важно) и сохраняет его
значение в локальную переменную currentValue.
ctx.setAttr('value', currentValue);
Метод setAttr() генерит в конечном HTML атрибут, заданный первым параметром и устанавливает в атрибут строку,
переданную вторым параметром.
Сохраняем, обновляем страницу, смотрим в HTML. Сработало, но не всё. Нет атрибута value
<input class="input" data-block="input"></input>
Всё правильно, потому что метод setAttr(attrName, attrValue) создаёт атрибут только если второй параметр
attrValue не равен null. А у нас он как раз и равен null, потому что шаблон готов принять параметр,
а мы такой параметр ему не передали.
Передадим параметр. Смотрите, как у блока input я объявил кастомный параметр value: 'Привет, Бивис'
module.exports = function (pages) {
pages.declare('test-page', function (params) {
var options = params.options;
return {
block: 'page',
title: 'test page',
styles: [
{url: options.assetsPath + '.css'}
],
scripts: [
{url: options.assetsPath + '.' + params.lang + '.js'}
],
body: [
{
block: 'input',
value: 'Привет, Бивис'
}
]
};
});
}
Теперь обновите страницу в браузере. Успех? :)
И в html оно выглядит так:
<input class="input" data-block="input" value="Привет, Бивис"></input>
Обратите внимание, в бивисе нет зарезервированных полей, кроме block и view. Вместо value: 'Привет, Бивис' можно было указать любое другое, например, не знаю... inputText: 'Привет, Бивис'. Важно лишь в шаблоне принимать этот параметр методом ctx.getParam('inputText')
И вот структура нашего блока почти готова. Стоп. Текстовое поле формы без атрибута name? Как форма передаст его
значение на сервер? Ок, выльем и этот атрибут. Вы уже знаете, как это делается,
мы могли бы и не показывать:
module.exports = function (bt) {
bt.match('input', function (ctx) {
ctx.setTag('input');
var currentValue = ctx.getParam('value');
ctx.setAttr('value', currentValue);
ctx.setAttr('name', 'loginField');
});
};
И в HTML оно выглядит так:
<input class="input" data-block="input" name="loginField" value="Привет, Бивис"></input>
Мы захардкодили имя будущего атрибута. Не зазорно, если мы точно знаем, что имя у инпута никогда не поменяется. В таком случае мы думаем так: "Почему бы и не прибить гвоздём"?
А если серьёзно, этим шагом мы не столько "хардкодим", сколько изолируем от внешнего мира внутреннее
устройство блока, никому не разрешаем извне менять значение атрибута name. Мы сознательно ограничиваем API
этого блока.
Вы можете делать API блока более открытым или... менее открытым. Это решать конкретно вам. Мы стараемся открывать блок наружу только самым минимальным образом, чтобы сохранить максимальный контроль над внутренним устройством блока.
Стилизация
Откройте в редакторе файл blocks/input/input.styl, там написано:
.input {
/* здесь стили блока */
}
Напишите какое-нибудь свойство и обновите страницу в браузере. Я только что написал border: 1px solid red; и
проверил — у меня в браузере вокруг блока появилась рамка. У вас тоже успех?
В Stylus можно делать много разных вещей. Некоторые из них мы сознательно не используем. Точнее, мы используем лишь некоторые, которые не смогут повредить стабильности стилей. Но это тема отдельного документа, там мы опишем подходы, которые мы себе разрешаем, и опишем вредные возможности, которые мы себе строго запрещаем.
А вообще про Stylus стоит почитать на официальном сайте http://learnboost.github.io/stylus/
Пока сделаем вид, что в styl-файлах мы пишем чистый CSS и перейдём к самому интересному. К программированию
поведения блока.
Проектирование
Перед программированием поведения очень полезно спроектировать блок. Искусство проектирования может показаться нетривиальным и избыточным. Но проектирование — это то, чем мы занимаемся в процессе разработки осознанно или неосознанно, хотим мы того или нет. Делаем мы это в голове в виде мыслей или на бумаге в виде схем и диаграмм, но каждый из нас этим занимается постоянно. Займёмся и сейчас.
Изобразим UML-диаграмму будущего блока. Это необязательно для вас, но мы сделаем, чтобы явно показать ход наших мыслей.
Что умеет блок input? Пользователь вводит в него текст для того, чтобы форма отправила это значение на сервер.
Следовательно, инпут должен уметь отдавать введённое значение.
Отобразим это в PlantUML-диаграмме с помощью простого описания:
class Input {
+getValue(): String
}
Чтобы сделать из этого красивую картинку, отправляем этот код в http://www.plantuml.com/plantuml/form :
Теперь красиво и наглядно :)
Двигаемся дальше. Инпут ещё должен уметь принимать в себя значение, следовательно добавим в него метод-сеттер и сделаем ещё одну красивую картинку:
На этом остановимся, не будем раньше времени усложнять блок.
Скорее всего вы не проектируете свои приложения с помощью UML-схем. Честно говоря, мы бы тоже с удовольствием не
использовали эти схемы ;)
Нам трудно без формальных схем проектировать большие приложения, поэтому мы этим и занимаемся. Если вам
интересно, мы читаем про UML на сайте http://plantuml.sourceforge.net/,
а красивые картинки генерим здесь: http://www.plantuml.com/plantuml/form
Что мы понимаем, когда смотрим на составленные UML-схемы? Мы понимаем, что наш блок пока довольно простой и умеет
он только лишь значение отдавать или получать. Удерживая в голове эту информацию, как вектор,
мы начинаем программировать поведение.
Поведение
Чтобы блок мог взаимодействовать с пользователем (или с другими блоками без участия пользователя),
Бивису необходимо сообщить, что блок интерактивный, что для него написано js-поведение.
Для этого мы помечаем блок, устанавливаем на нём "метку", по которой бивис-движок понимает, что для блока написано поведение.
Такую метку ставит метод enableAutoInit().
module.exports = function (bt) {
bt.match('input', function (ctx) {
ctx.enableAutoInit();
ctx.setTag('input');
ctx.setAttr('value', ctx.getParam('value'));
ctx.setAttr('name', 'loginField');
});
};
Он создаёт в HTML-коде блока специальный класс _init, по которому Бивис понимает,
что блок интерактивный и его нужно инициализировать.
<input class="input _init" data-block="input" name="loginField" value="Привет, Бивис"></input>
Теперь запрограммируем поведение. Создайте файл input.js в директории blocks/input и скопируйте в него заготовку:
modules.define(
'input',
['inherit', 'block'],
function (provide, inherit, YBlock) {
var Input = inherit(YBlock, {
// инстанс-методы
}, {
// статические методы
});
provide(Input);
}
);
Это модуль. Модульных систем много разных, Бивис использует
Ymaps Modules, которые придумали и разработали
разработчики из Яндекс.Карт. Пройдите по ссылке, прочитайте.
В основе модулей лежит очень простая идея, и в документации к Ymaps Modules простым языком описывается,
зачем модульная система нужна, как её использовать, и что конкретно означает каждая строчка из кода,
который мы привели выше.
Не стали читать? ;) Там чтения на пять минут. Мы всё таки настаиваем, вам понравится. Прочитайте.
Прочитали? Тогда вы понимаете, что код внутри анонимной функции - это и есть код,
который программирует поведение вашего input. Можно было бы программировать его по старинке,
навешивая события на DOM-элементы самим или через jQuery-хелперы. Но мы предпочитаем работать с блоками через
абстракции, чтобы не зависеть от HTML-реализации блока.
Поэтому мы в зависимостях модуля input указали два других модуля — inherit и block.
Первый модуль возвращает функцию inherit, которая используется для создания классов и наследования (подробное
описание читайте на странице автора:
https://github.com/dfilatov/node-inherit.
Второй модуль - это класс, который и реализует механизмы абстрагирования от DOM-узлов. Это базовый визуальный блок.
Все блоки, которые вы создаёте в проекте и которые пользователь может увидеть на страницах
вашего сайта (мы такие блоки называем визуальными), должны наследоваться от
модуля block с помощью модуля inherit.
В этом коде мы создаём класс Input, как наследника от класса Yblock, и пока больше ничего полезного не делаем.
var Input = inherit(YBlock, {
// инстанс-методы
}, {
// статические методы
});
А этой строкой мы "провайдим" наш класс Input наружу. Как бы высовываем из модуля ручку наружу,
за которую другие модули могут схватиться и позвать его к себе через зависимости.
provide(Input);
Теперь добавим пустой конструктор в блок инстанс-методов:
var Input = inherit(YBlock, {
__constructor: function () {
}
// инстанс-методы
}, {
// статические методы
});
Метод __constructor() запускается автоматически, когда создаётся экземпляр класса Input.
Точно такой же метод есть в базовом классе YBlock, от которого Input наследуется.
То есть внутри Input мы перезаписали конструктор, а после обязательно нужно вызвать базовый конструктор,
чтобы он сделал всё, "что нужно". Сейчас не так важно, что базовый констуруктор такого "нужного" там делает. Важно
запомнить, что вызов базового метода происходит так: this.__base.apply(this, arguments):
var Input = inherit(YBlock, {
__constructor: function () {
this.__base.apply(this, arguments);
}
// инстанс-методы
}, {
// статические методы
});
Теперь переопределим статический метод, который будет возвращать имя конкретно этого блока:
var Input = inherit(YBlock, {
__constructor: function () {
this.__base.apply(this, arguments);
}
// инстанс-методы
}, {
// статические методы
getBlockName: function () {
return 'input'; // вернуть необходимо имя блока
}
});
В некоторых ситуациях вам нужно будет спросить имя блока, этот метод вернёт вам актуальное имя. Следует относиться к этому методу, как к обязательному.
И теперь посмотрим на нашу заготовку целиком:
modules.define(
'input', // создали модуль, дали ему имя
[
'inherit', // зависимость от inherit
'block' // зависимость от YBlock
],
function (
provide,
inherit, // получаем функцию inherit
YBlock // получаем блок YBlock
) {
var Input = inherit(YBlock, {
// Создаём свой конструктор
__constructor: function () {
// Вызываем базовый конструктор
this.__base.apply(this, arguments);
// здесь описываем то, что происходит сразу после создания инстанса класса
}
// здесь опишем инстанс-методы класса Input
}, {
// здесь опишем статические методы
// при наследовании от YBlock, необходимо переопределить статический метод getBlockName
getBlockName: function () {
return 'input'; // вернуть имя блока
}
});
provide(Input); // Выставить из модуля наружу "ручку"
}
);
А теперь напишем те два метода для нашего блока, ради которых всё задумано - один отдаёт значение, второй устанавливает.
modules.define(
'input',
['inherit', 'block'],
function (provide, inherit, YBlock) {
var Input = inherit(YBlock, {
__constructor: function () {
this.__base.apply(this, arguments);
console.log(this.getValue());
},
getValue: function() {
return this.getDomNode().val();
},
setValue: function(value) {
this.getDomNode().val(value);
}
}, {
getBlockName: function () {
return 'input';
}
});
provide(Input);
});
А чтобы проверить, что эта красота работает, в конструкторе вызовем один из методов, например this.getValue(),
и будем ожидать, что в момент загрузки страницы в браузерной консоли появится текстовое сообщение.
Проверяем? Открываем на странице firebug-консоль, обновляем страницу и... ничего не происходит. Конструктор не выполнился? А почему?
А потому что по дефолту в Бивисе отключена автоматическая инициализация всех блоков. Это, главным образом, экономит процессорное время, чтобы не делать лишних действий. Решение о том, нужно ли инициализировать все блоки сразу во время загрузки страницы (или только какие-то блоки "по мере их готовности") принимаете вы — разработчик проекта.
Если вы хотите, чтобы бивис инициализировал все блоки, у которых в шаблоне указано ctx.enableAutoInit(), автоматически во время загрузки страницы (а вы именно этого в данном примере хотите), вам нужно указать дополнительную зависимость для страницы.
Снова откройте файл pages/test-page/test-page.deps.yaml и допишите:
- page
- input
- block: block
elem: auto-init
Вы добавили зависимость от блока по имени block (случайная тавтология),
а точнее от его элемента auto-init, который и занимается тем, что инициализирует все ваши блоки при загрузке
страницы.
Теперь обновите страницу в браузере. Успех? Я вижу в консоли тот самый текст, который есть сейчас в текстовом поле. Вы тоже видите? Значит, мы написали поведение для блока.
Что дальше?
К этому моменту вы получили 90% всех знаний и том, как пользоваться бивисом. Вы умеете создавать структуру, писать стили и программировать поведение.
Оставшиеся 10% - узкие прикладные задачи, которые мы с вами решим в отдельных руководствах:
- Как обращаться за данными и передавать в блоки?
- Как сделать такой же блок, но чуть-чуть другой
- Как настроить локализацию в проекте
В документе про CSS мы расскажем:
- про то, как написать разные css-представления для одного блока
- как описывать состояния блока
- про опасные и безопасные приемы в Stylus (например, почему глобальные миксины это плохо, а глобальные переменные - это не плохо)
В документе про JS мы расскажем:
- про два способа инициализации блока
- про доступ и операции над внутренностями блока
- про навешивание событий
В документе про BT мы расскажем:
- как строить внутреннюю структуру блока
- как можно параметрами менять внутреннее устройство блока
- как видозменять контекст (на примере DOCTYPE и layout-for-page из bevis-blog)
А если вы не нашли ответа на свой вопрос, создайте issue прямо здесь и задайте этот вопрос, не стесняйтесь. Мы ответим так быстро, как только сможем :)
Успехов вам!