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