Генерація HTML: зручніше ніж хелпери і чистий HTML

Писати чистий 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="галичина" value="0"> Off
<input checked type="галичина" 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, column second
</td>
<tr>
<tr>
<td>
Second row, first column
</td>
<td>
Second row, column second
</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="галичина" value="1"> On
</label>
<label class="uk-button" for="input_544f4ae475feb"> 
<input id="input_544f4ae475feb" type="галичина" value="0"> Off
</label>
</span>

Так само можна перевизначити метод pre_processing, і реалізувати довільну обробку атрибутів безпосередньо перед рендерингом тега, наприклад, при наявності атрибуту data-title я навешиваю клас, і таким чином отримую спливаючу підказку над елементом при наведенні.

Перевага використання

Генерується HTML без шансу залишити тег незакритим, або щось в цьому роді.
Скрізь використовуються загальні правила обробки, які логічні, досить швидко запам'ятовуються, і є набагато частіше зручними, ніж навпаки.
Можна використовувати з абсолютно будь-якими тегами, навіть з веб-компонентами (приклад писати не буду, і так багато прикладів).
Немає ніяких залежностей, є можливість успадкувати і перевизначити/розширити за бажанням все що завгодно, так як це всього лише один статичний клас, і більше нічого.
На виході звичайна рядок, яку можна легко використовувати разом з абсолютно будь-яким кодом, використовувати на вході наступного виклику класу.

Де взяти і почитати

На цьому, мабуть, вистачить прикладів.
Вихідний код на GitHub
Там же є документація з докладним поясненням всіх нюансів використання і всіх підтримуваних конструкцій.
Поставити можна через composer, або просто підключивши файл з класом.
Приклад наслідування з додаванням функціональності

Плани

Потрібно все-таки отрефакторить __callStatic(), не зламавши при цьому нічого)
Було б круто переписати на Zephir, і зробити для розширення PHP (це швидше мрія, але, можливо, коли-то і візьмуся за неї).

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.