Как стать автором
Обновить

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

Время на прочтение6 мин
Количество просмотров36K
Писать чистый 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 (это скорее мечта, но, возможно, когда-то возьмусь и за нее).
Теги:
Хабы:
Всего голосов 38: ↑24 и ↓14+10
Комментарии33

Публикации

Истории

Работа

PHP программист
146 вакансий

Ближайшие события