Генерация HTML: удобнее чем хелперы и чистый HTML28.10.2014 15:03
Писать чистый HTML часто неудобно, особенно если нужно делать динамические вставки.Шаблонизаторы частично решают эту проблему, но их причудливый синтаксис нужно изучать, мириться с ограничениями, вкладывать одни шаблоны в другие для повторного использования, в целом попытка хороша, но что-то не то.
В некоторых фреймворках есть хелперы, в частности написать эту статью меня вынудила Aura.Html. С хелперами иная история — они изначально задуманы для реального упрощения, поскольку одной командой могут генерировать хороший кусок HTML кода, но они в большинстве заточены под определённое использование, и что-то дальше этого выглядит слишком криво.
Как более универсальное решение было бы не плохо не изобретать причудливый синтаксис, а использовать самый обычный PHP и всем знакомые примитивные CSS-селекторы.
Размышляя в таком духе некоторое время назад я принялся пилить свой велосипед. Велосипед получился, использовался в рамках другого велосипеда, потом отделился, много раз обновлялся, и сейчас я хотел бы поделиться им с сообществом.
Как оно работает? Идея была в том, чтобы сделать как можно проще:
h: div ('Content')
что на выходе даст
Content
Это самый простой пример. Название метода — тэг, внутри передается значение. Если нужно добавить атрибутов — не проблема:
h: div (
'Content',
[
'class' => 'some-content'
]
)
Content
И можно было бы подумать, что проще уже никак, но тут на помощь приходят CSS-селекторы, и немного уличной магии:
h::{'div.some-content'}('Content')
На выходе будет то же самое. С первого взгляда может показаться немного странным, но на практике весьма удобно.В сравнении с Aura.Html
В начале я упоминал Aura.Html, стоит сравнить как генерируется HTML там, и тут.Aura.Html (пример из документации):
$helper→input (array (
'type' => 'search',
'name' => 'foo',
'value' => 'bar',
'attribs' => array ()
));
Наш вариант:
h::{'input[type=search][name=foo][value=bar]'}()
Любой из параметров можно было вынести в массив.На выходе:
И ещё вариант посерьезней.Aura.Html (пример из документации):
$helper→input (array (
'type' => 'select',
'name' => 'foo',
'value' => 'bar',
'attribs' => array (
'placeholder' => 'Please pick one',
),
'options' => array (
'baz' => 'Baz Label',
'dib' => 'Dib Label',
'bar' => 'Bar Label',
'zim' => 'Zim Label',
),
))
Наш вариант:
h::{'select[name=foo]'}([
'in' => [
'Please pick one',
'Baz Label',
'Dib Label',
'Bar Label',
'Zim Label'
],
'value' => [
'',
'baz',
'dib',
'bar',
'zim'
],
'selected' => 'bar',
'disabled' => ''
])
Тут in используется явно, его можно использовать для передачи внутренностей тэга, как Content в примере с div выше. Используются как общие правила, так и некоторые специальные, немного подробнее о которых дальше.На выходе то же самое:
Специальная обработка
Все тэги следуют общим правилам обработки, но есть некоторые тэги, которые имеют дополнительные конструкции для удобства.Например:
h::{'input[name=agree][type=checkbox][value=1][checked=1]'}()
Работает похоже с select, в value значение, а checked проставится когда совпадет одноименный элемент передаваемого массива.Ещё один пример использования in и специальной обработкой input[type=radio]:
h::{'input[type=radio]'}([
'checked' => 1,
'value' => [0, 1],
'in' => ['Off', 'On']
])
Off
On
Никаких оберток label не добавляется специально, чтобы сделать код максимально общим и предсказуемым.Если нужно обработать массив
Это, наверное, самая часто используемая вместе с контролем вложенности возможность, так как данные и правда часто приходят откуда-то в виде массива.Для обработки массива его можно передать прямо вместо значения:
h::{'tr td'}([
'First cell',
'Second cell',
'Third cell'
])
Либо даже опустить лишние скобки в самом простом случае
h::{'tr td'}(
'First cell',
'Second cell',
'Third cell'
)
На выходе:
First cell
Second cell
Third cell
Каждый элемент массива будет обработан отдельно, то есть вполне законно передавать не только строки, но и некоторые атрибуты, правда, иногда это выглядит слишком монструозно:
h::{'tr.row td.cs-left[style=text-align: left;][colspan=2]'}(
'First cell',
[
'Second cell',
[
'class' => 'middle-cell',
'style' => 'color: red;',
'colspan' => 1
]
],
[
'Third cell',
[
'colspan' => false
]
]
)
Если в вызове тоже были указаны атрибуты — class и style будут расширены, остальные перезаписаны, атрибуты с логическим значением false будут удалены.
First cell
Second cell
Third cell
С помощью волшебной палочки, которая не является привычной частью CSS-селектора (это единственное исключение, без которого можно обойтись), можно управлять тем, как будут обрабатываться уровни вложенности:
h::{'tr| td'}([
[
'First row, first column',
'First row, second column'
],
[
'Second row, first column',
'Second row, second column'
]
])
First row, first column
First row, second column
Second row, first column
Second row, second column
Если массив получен из базы данных, или иного хранилища — удобно использовать такой массив напрямую, и это можно сделать передав в специальный атрибут insert:
$array = [
[
'text' => 'Text1',
'id' => 10
],
[
'text' => 'Text2',
'id' => 20
]
];
h: a (
'$i[text]',
[
'href' => 'Page/$i[id]',
'insert' => $array
]
)
Text1
Text2
Можно и в одну строчку все атрибуты написать:
$array = [
[
'id' => 'first_checkbox',
'value' => 1
],
[
'id' => 'second_checkbox',
'value' => 0
],
[
'id' => 'third_checkbox',
'value' => 1
]
];
h::{'input[id=$i[id]][type=checkbox][checked=$i[value]][value=1]'}([
'insert' => $array
])
А ещё всё это можно расширять
Этот класс представляет только общие, ни к чему не привязанные правила генерации HTML, которые могут быть использованы независимо от окружения.Но иногда хочется упростить выполнение более сложных рутинных операций.Например, я использую многие элементы UIkit на фронтенде, и, например, для переключателя нужна особым образом подготовленный HTML.Скопировав оригинальный код обработки input и слегка отредактировав можно получить такой результат:
h: radio ([
'checked' => 1,
'value' => [0, 1],
'in' => ['Off', 'On']
])
Так же можно переопределить метод pre_processing, и реализовать произвольную обработку атрибутов непосредственно перед рендерингом тэга, например, при наличии атрибута data-title я навешиваю класс, и таким образом получаю всплывающую подсказку над элементом при наведении.Преимущество использования
Генерируется HTML без шанса оставить тэг незакрытым, или что-то в этом роде.Везде используются общие правила обработки, которые логичны, весьма быстро запоминаются, и являются намного чаще удобными, чем наоборот.Можно использовать с абсолютно любыми тэгами, даже с веб-компонентами (пример писать не буду, и так много примеров).Нет никаких зависимостей, есть возможность унаследовать и переопределить/расширить по желанию всё что угодно, так как это всего лишь один статический класс, и больше ничего.На выходе обычная строка, которую можно легко использовать вместе с абсолютно любым кодом, использовать на входе следующего вызова класса.Где взять и почитать
На этом, пожалуй, хватит примеров.Исходный код на GitHubТам же есть документация с подробным объяснением всех нюансов использования и всех поддерживаемых конструкций.Поставить можно через composer, либо просто подключив файл с классом.Пример наследования с добавлением функциональностиПланы
Нужно всё-таки отрефакторить __callStatic (), не сломав при этом ничего)Было бы круто переписать на Zephir, и сделать расширение для PHP (это скорее мечта, но, возможно, когда-то возьмусь и за нее).