Генерация HTML: удобнее чем хелперы и чистый HTML

Писать чистый 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 (это скорее мечта, но, возможно, когда-то возьмусь и за нее).

© Habrahabr.ru