Drive your career as React Developer with Symphony Solutions!
×Закрыть

Шаблони для шаблонів шаблонізатора

Раніше все було простіше — вебсторінки містили в собі й розмітку, і стилі, і дані. З плином часу кількість сторінок на сайті невпинно збільшувалась, а разом із ним збільшувалась і кількість дубльованого коду. Для розв’язання цієї проблеми спочатку зі сторінок виокремили стилі, а згодом і самі дані, що призвело до масового використання шаблонізаторів.

Шаблонізатор

Зазвичай адміністративні частини сайтів складаються з різноманітних форм, за допомогою яких здійснюються операції над даними. Водночас ці форми подібні одна на одну внаслідок певної уніфікації розміщення елементів. Там навіть можуть бути три однакові кнопки для всіх форм: «Зберегти», «Видалити» та «Очистити».

Дублювання кнопок у різних формах вирішується елементарно стандартними методами будь-якого шаблонізатора. А от що робити, наприклад, з дубльованим кодом HTML-розмітки полів форми — питання трохи складніше.

Форма

Для демонстрації проблеми я створю форму «Користувач» середнього розміру з типовим набором полів, проте чотирма різними способами. Це дасть нам можливість порівняти обʼєм коду різних форм та зробити певні висновки щодо цього питання наприкінці статті.

Форма HTML

Спочатку створимо форму HTML згідно з документацією для початківців W3Schools.

Форма HTML

З одного боку, це доволі проста форма, з іншого — у ній забезпечено мінімальне різномаїття поширених типів полів. А цього цілком достатньо для нашого прикладу. Тому подивімось на початковий код.

<form action="user.php" method="POST">
    <label for="title">Назва</label>
    <input type="text" name="title" id="title" /><br />
    <label for="description">Опис</label>
    <textarea name="description" rows="2" id="description"></textarea><br />
    <label for="birthdate">Дата народження</label>
    <input type="date" name="birthdate" id="birthdate" /><br />
    <label for="gender">Стать</label>
    <fieldset id="gender">
        <input type="radio" name="gender" id="gender-1" checked>
        <label for="gender-1">Чоловіча</label>
        <input type="radio" name="gender" id="gender-2">
        <label for="gender-2">Жіноча</label>
        <input type="radio" name="gender" id="gender-3">
        <label for="gender-3">Інша</label>
    </fieldset>
    <label for="phone">Телефон</label>
    <input type="tel" name="phone" id="phone" /><br />
    <label for="email">Пошта</label>
    <input type="email" name="email" id="email" /><br />
    <label for="website">Вебсайт</label>
    <input type="url" name="website" id="website" /><br />
    <label for="password">Пароль</label>
    <input type="password" name="password" id="password" /><br />
    <label for="password2">Пароль (повторно)</label>
    <input type="password" name="password2" id="password2" /><br />
    <label for="access">Права доступу</label>
    <select name="access" id="access">
        <option>Користувач</option>
        <option>Автор</option>
        <option>Редактор</option>
        <option>Головний редактор</option>
        <option>Адміністратор</option>
    </select><br />
    <label for="subscribe">Повідомлення</label>
    <fieldset id="subscribe">
        <input type="checkbox" name="subscribe-1" id="subscribe-1" checked>
        <label for="subscribe-1">Присилати особисті повідомлення</label><br />
        <input type="checkbox" name="subscribe-2" id="subscribe-2" checked>
        <label for="subscribe-2">Присилати важливі повідомлення</label><br />
        <input type="checkbox" name="subscribe-3" id="subscribe-3" checked>
        <label for="subscribe-3">Присилати загальні повідомлення</label><br />
    </fieldset>
    <p class="buttons">
        <button type="submit" name="save">Зберегти</button>
        <button type="submit" name="delete">Видалити</button>
        <button type="reset" name="reset">Очистити</button>
    </p>
</form>

Форма містить 15 полів та реалізована за допомогою 47 рядків коду між тегами form, якщо не враховувати кнопки. Я навмисно не використовував додаткові атрибути value, placeholder, pattern, title та інші, щоби зайвий раз не ускладнювати приклад.

Втім навіть у такої простої форми є можливість для оптимізації. Для назви поля, замість окремого елемента label, можна використати однойменний атрибут. А під час виводу в шаблонізаторі перетворювати атрибут назад в елемент, як у прикладі вище. Унаслідок цього можна зменшити розмір коду аж на добру третину рядків.

На одній формі це, можливо, несуттєва економія, але якщо їх із 20-30 подібних — це вже зовсім інша справа. Хоча це ще дрібниці в порівнянні з тим, що нас очікує далі.

Форма Bootstrap

Прості форми, на кшталт наведеної вище, в реальному житті трапляються доволі рідко. Зазвичай, для того щоби покращити зовнішній вигляд, у них додають багато різних елементів. Форма Bootstrap більше подібна на ті, що здебільшого використовуються на сайтах.

Форма Bootstrap

Набір полів у ній повністю ідентичний до попередньої форми, однак вигляд вона має суттєво привабливіший. Зокрема для вирівнювання використовується сітка й у ній реалізований механізм адаптивного масштабування під екрани різного розміру. А тепер зазирнемо до неї «під капот».

<form action="user.php" method="POST" class="mt-4">
    <div class="form-group row">
        <label for="form-title" class="col-sm-4 col-form-label">Назва</label>
        <div class="col-sm-8">
            <input type="text" name="title" class="form-control" id="form-title" />
        </div>
    </div>
    <div class="form-group row">
        <label for="form-description" class="col-sm-4 col-form-label">Опис</label>
        <div class="col-sm-8">
            <textarea name="description" rows="2" class="form-control" id="form-description">
            </textarea>
        </div>
    </div>
    <div class="form-group row">
        <label for="form-birthdate" class="col-sm-4 col-form-label">Дата народження</label>
        <div class="col-sm-8">
            <input type="date" name="birthdate" class="form-control" id="form-birthdate" />
        </div>
    </div>
    <fieldset class="form-group">
        <div class="row">
            <div class="col-form-label col-sm-4">Стать</div>
            <div class="col-sm-8">
                <div class="form-check form-check-inline">
                    <input type="radio" name="gender" id="form-gender-1" class="form-check-input" checked />
                    <label class="form-check-label" for="form-gender-1">Чоловіча</label>
                </div>
                <div class="form-check form-check-inline">
                    <input type="radio" name="gender" id="form-gender-2" class="form-check-input">
                    <label class="form-check-label" for="form-gender-2">Жіноча</label>
                </div>
                <div class="form-check form-check-inline">
                    <input type="radio" name="gender" id="form-gender-3" class="form-check-input">
                    <label class="form-check-label" for="form-gender-3">Інша</label>
                </div>
            </div>
        </div>
    </fieldset>
    <div class="form-group row">
        <label for="form-phone" class="col-sm-4 col-form-label">Телефон</label>
        <div class="col-sm-8">
            <input type="tel" name="phone" class="form-control" id="form-phone" />
        </div>
    </div>
    <div class="form-group row">
        <label for="form-email" class="col-sm-4 col-form-label">Пошта</label>
        <div class="col-sm-8">
            <input type="email" name="email" class="form-control" id="form-email" />
        </div>
    </div>
    <div class="form-group row">
        <label for="form-website" class="col-sm-4 col-form-label">Вебсайт</label>
        <div class="col-sm-8">
            <input type="url" name="website" class="form-control" id="form-website" />
        </div>
    </div>
    <div class="form-group row">
        <label for="form-password" class="col-sm-4 col-form-label">Пароль</label>
        <div class="col-sm-8">
            <input type="password" name="password" class="form-control" id="form-password" />
        </div>
    </div>
    <div class="form-group row">
        <label for="form-password2" class="col-sm-4 col-form-label">Пароль (повторно)</label>
        <div class="col-sm-8">
            <input type="password" name="password2" class="form-control" id="form-password2" />
        </div>
    </div>
    <div class="form-group row">
        <label for="form-access" class="col-sm-4 col-form-label">Права доступу</label>
        <div class="col-sm-8">
            <select name="access" title="Ваш рівень доступу" class="form-control" id="form-access">
                <option>Користувач</option>
                <option>Автор</option>
                <option>Редактор</option>
                <option>Головний редактор</option>
                <option>Адміністратор</option>
            </select>
        </div>
    </div>
    <fieldset class="form-group">
        <div class="row">
            <div class="col-form-label col-sm-4">Повідомлення</div>
            <div class="col-sm-8">
                <div class="form-check">
                    <input type="checkbox" name="subscribe-1"
                           id="form-subscribe-1" class="form-check-input" checked />
                    <label class="form-check-label" for="form-subscribe-1">
                        Присилати особисті повідомлення
                    </label>
                </div>
                <div class="form-check">
                    <input type="checkbox" name="subscribe-2"
                           id="form-subscribe-2" class="form-check-input" checked />
                    <label class="form-check-label" for="form-subscribe-2">
                        Присилати важливі повідомлення
                    </label>
                </div>
                <div class="form-check">
                    <input type="checkbox" name="subscribe-3"
                           id="form-subscribe-3" class="form-check-input" checked />
                    <label class="form-check-label" for="form-subscribe-3">
                        Присилати загальні повідомлення
                    </label>
                </div>
            </div>
        </div>
    </fieldset>
    <fieldset class="text-center mt-5">
        <button type="submit" name="save" class="btn btn-primary mx-1">Зберегти</button>
        <button type="submit" name="delete" class="btn btn-danger mx-1">Видалити</button>
        <button type="reset" name="reset" class="btn btn-secondary mx-1">Очистити</button>
    </fieldset>
</form>

Як ми бачимо, розмітка форми відчутно «погладшала»: розмір коду для тієї ж кількості полів збільшився у 2-3 рази. Навіть під час поверхневого огляду в око відразу впадає величезна кількість дубльованого коду, який можна й потрібно усувати.

Форма Google

Якщо розмір форми Bootstrap вас не вразив, наведу ще один приклад з авторитетного сайту. Для цього я створив аналогічну форму «Користувач» в Google Forms з тим же набором полів. Наводити в статті зображення форми та зразок коду я не буду через їхні великі розміри та ризик порушення авторських прав.

Вгадайте, скільки рядків зайняла та ж сама кількість полів в Google Forms? Я нарахував приблизно 504 рядки й це без врахування перенесення довгих рядків. Заради обʼєктивності маю зазначити, що розмітка форм в Google Forms розробляється для максимальної універсальності. Як наслідок, вкладені елементи div в ній здебільшого зайві для мого прикладу.

Попри це, форми з великою кількістю елементів в інтернеті далеко не рідкість. І відповідно проблема дубльованого коду, наприклад, у типових формах адміністративних частин, стоїть досить гостро.

Шаблон

Я знайшов три способи розв’язання цієї проблеми. Перший і найпростіший — підняти праву руку та, опускаючи її, промовити заклинання «Та і грець із ним!». Не виключаю, що якась частина розробників саме так і робить у подібних ситуаціях.

Другий спосіб трохи складніший — необхідно з контролера разом із даними передати в шаблон конфігурацію форми. А вже шаблонізатор, на основі переданої йому конфігурації, динамічно створює форму разом із даними. Унаслідок цього можна повністю уникнути дублювання коду.

Втім у цього підходу є один маленький недолік — він порушує правила архітектурного шаблону MVC. Адже контролер не має ніякого відношення до форм, це юрисдикція вигляду (View), у нашому випадку шаблонізатора. Тому ми поступово підходимо до третього способу — шаблону для шаблонів шаблонізатора.

Шаблон для шаблона

Якщо коротко — конфігурацію форми з другого способу переносимо з контролера в шаблонізатор у вигляді додаткового проміжного шаблона. Шаблонізатор спочатку обробляє його, вставляючи в нього дані, а потім передає в інший шаблон, де на його основі генерують вихідну форму. Нижче я якраз навожу приклад такого проміжного шаблона для нашої форми «Користувач», реалізованого за допомогою XSLT.

<xsl:template match="user">
    <h1>Користувач</h1>
    <small class="text-muted">Приклад форми XSLT з даними користувача</small>
    <xsl:variable name="template">
        <form action="user.php">
            <input type="text" name="title" label="Назва" />
            <textarea name="description" rows="2" label="Опис" />
            <input type="date" name="birthdate" label="Дата народження" />
            <fieldset type="radio" name="gender" inline="inline" label="Стать">
                <input label="Чоловіча" checked="checked" />
                <input label="Жіноча" />
                <input label="Інша" />
            </fieldset>
            <input type="tel" name="phone" label="Телефон" />
            <input type="email" name="email" label="Пошта" />
            <input type="url" name="website" label="Вебсайт" />
            <input type="password" name="password" label="Пароль" />
            <input type="password" name="password2" label="Пароль (повторно)" />
            <select name="access" label="Права доступу">
                <option>Користувач</option>
                <option>Автор</option>
                <option>Редактор</option>
                <option>Головний редактор</option>
                <option>Адміністратор</option>
            </select>
            <fieldset type="checkbox" label="Повідомлення">
                <input name="subscribe-1" label="Присилати особисті повідомлення" checked="checked" />
                <input name="subscribe-2" label="Присилати важливі повідомлення" checked="checked" />
                <input name="subscribe-3" label="Присилати загальні повідомлення" checked="checked" />
            </fieldset>
        </form>
    </xsl:variable>
    <xsl:call-template name="form">
        <xsl:with-param name="template" select="exslt:node-set($template)" />
    </xsl:call-template>
</xsl:template>

Як ви, мабуть, уже помітили, в шаблоні для шаблону немає нічого зайвого — тільки те, що дійсно необхідно для генерації форми. Його розмір менший, ніж розмір форми HTML з першого прикладу, але водночас вона виводить повноцінну форму Bootstrap з другого прикладу. До того ж працювати з таким компактним шаблоном не тільки зручніше, а і приємніше.

Конкретно в XSLT 1.0 шаблон для шаблона реалізується за допомогою функції node-set() з розширення exslt. Вона дає змогу записати шаблон у змінну, перетворюючи його в дані, які можна передати в інший шаблон для проведення необхідної трансформації. А як саме цей механізм можна реалізувати в шаблонізаторі, з якими працюєте ви, пишіть у коментарях.

Шаблон для шаблонізатора

Від попереднього шаблону не буде ніякої користі, якщо його не трансформувати в необхідний нам кінцевий варіант форми Bootstrap. Для цього необхідно створити додатково шаблони для шаблонізатора, які будуть цим займатись.

<xsl:template name="form">
   <xsl:param name="template" />
    <form method="POST" class="mx-auto mt-4">
        <xsl:copy-of select="$template/form/@*" />
        <xsl:apply-templates select="$template/form/*" />
        <div class="form-group text-center py-5">
            <input type="submit" name="_save" value="Зберегти" class="btn btn-primary mx-1" />
            <input type="submit" name="_delete" value="Видалити" class="btn btn-danger mx-1" />
            <input type="reset" name="_reset" value="Очистити" class="btn btn-secondary mx-1" />
        </div>
    </form>
</xsl:template>

<xsl:template match="form/*">
    <div class="form-group row">
        <label for="form-{@name}" class="col-sm-4 col-form-label">
            <xsl:value-of select="@label" />
            <xsl:if test="@required"><span class="text-danger">*</span></xsl:if>
        </label>
        <div class="col-sm-8">
            <xsl:copy>
                <xsl:copy-of select="@*[local-name() != 'label']" />
                <xsl:attribute name="id">form-<xsl:value-of select="@name" /></xsl:attribute>
                <xsl:attribute name="class">form-control</xsl:attribute>
                <xsl:copy-of select="node()" />
            </xsl:copy>
        </div>
    </div>
</xsl:template>

<xsl:template match="form/fieldset">
    <fieldset class="form-group">
        <div class="row">
            <div class="col-form-label col-sm-4"><xsl:value-of select="@label" /></div>
            <div class="col-sm-8"><xsl:apply-templates select="input" /></div>
        </div>
    </fieldset>
</xsl:template>

<xsl:template match="form/fieldset/input">
    <div class="form-check">
        <xsl:if test="../@inline">
            <xsl:attribute name="class">form-check form-check-inline</xsl:attribute>
        </xsl:if>
        <xsl:variable name="id">
            <xsl:text>form-</xsl:text><xsl:value-of select="../@name" />
            <xsl:text>-</xsl:text><xsl:value-of select="position()" />
        </xsl:variable>
        <xsl:copy>
            <xsl:copy-of select="@*[local-name() != 'label']" />
            <xsl:attribute name="type"><xsl:value-of select="../@type" /></xsl:attribute>
            <xsl:attribute name="name">
                <xsl:choose>
                    <xsl:when test="../@type='radio'"><xsl:value-of select="../@name" /></xsl:when>
                    <xsl:otherwise><xsl:value-of select="@name" /></xsl:otherwise>
                </xsl:choose>
            </xsl:attribute>
            <xsl:attribute name="id"><xsl:value-of select="$id" /></xsl:attribute>
            <xsl:attribute name="class">form-check-input</xsl:attribute>
        </xsl:copy>
        <label class="form-check-label" for="{$id}"><xsl:value-of select="@label" /></label>
    </div>
</xsl:template>

Як бачите, нічого складного: чотири невеличкі шаблони трансформують шаблон, переданий за допомогою вхідного параметра template у вихідний HTML. Водночас атрибути label елементів форми та атрибути елемента fieldset, яких згідно зі стандартом там бути не повинно, на вихід не потрапляють. Головне, не забувати рефакторити ці шаблони, коли у вашому проєкті відбуваються структурні зміни пов’язані з виводом форм.

Висновки

А тепер давайте для аналізу виведемо таблицю з цифрами, які були експериментально отримані в усіх попередніх прикладах.

Назва формиКількість рядківРядків на поле
HTML422.8
Bootstrap1087.2
Google50433.6
XSLT251.7

Для експерименту ми використали форми з однаковою кількістю полів (15), проте із різною кількістю рядків HTML-розмітки. В останньому стовпчику я навів для зручності коефіцієнт, який показує відношення кількості рядків коду до кількості полів форми. Він наочно демонструє обсяг надлишкового, а разом із ним і дубльованого коду.

Як видно із таблиці, вивід форми за допомогою проміжного шаблона виявився з найменшим коефіцієнтом — 1.7 рядків на одне поле. До недоліків цього підходу можна віднести незначне збільшення тривалості формування сторінки через додаткову трансформацію проміжного шаблону. Однак, наприклад, для адміністративної частини сайту це збільшення абсолютно не критичне.

LinkedIn
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Як видно із таблиці, вивід форми за допомогою проміжного шаблона виявився з найменшим коефіцієнтом — 1.7 рядків на одне поле.

Это читерство) Надо считать вместе с «шаблонами для шаблонизатора». Тогда суммарное кол-во строк выйдет гораздо больше. И основной недостаток этого подхода (кроме xslt, конечно), как и всех шаблонов в принципе, в том, что используется промежуточная абстракция в виде «шаблонов для шаблонизатора». Эту абстракцию надо поддерживать, она сама по себе будет служить источником багов, её функционал придется расширять, с усложнением требований к формам, а хоть какая-то выгода от неё будет лишь в том случае, если таких форм будет очень много и они будут сильно схожи между собой по функциональности.

Другий спосіб трохи складніший — необхідно з контролера разом із даними передати в шаблон конфігурацію форми. А вже шаблонізатор, на основі переданої йому конфігурації, динамічно створює форму разом із даними. У наслідок цього можна повністю уникнути дублювання коду.

Втім у цього підходу є один маленький недолік — він порушує правила архітектурного шаблону MVC. Адже контролер не має ніякого відношення до форм, це юрисдикція вигляду (View), у нашому випадку шаблонізатора.

На самом деле правильный второй способ — и оптимальный в данном случае — иметь динамический view, т.е. генерировать на сервере (php, java, etc) или клиенте (react, angular, elm, etc) форму, куда потом развертывать модель.

Ваш третий вариант может теоретически иметь смысл только если вообще никакой возможности нету генерить view или использовать нормальные templating engines, да и то я бы лично еще подумал что лучше — немного простой повторяющейся разметки или жуткий зомбозавр xslt.

Основний акцент статті саме на підході до генерації форм, а XSLT я використав лише для прикладу. Я не frontend-розробник тому не знаю як цей механізм можна реалізувати на клієнті (React, Angular, Vue.js, etc).

На самом деле правильный второй способ — и оптимальный в данном случае — иметь динамический view, т.е. генерировать на сервере (php, java, etc) или клиенте (react, angular, elm, etc) форму, куда потом развертывать модель.

В другому способі, вказаному в статті, «конфігурація» форми зберігається саме в контролері із-за простоти реалізації, але порушує правила архітектурного шаблону MVC.

Можливо ви пропонуєте четвертий спосіб, з врахуванням можливостей та особливостей шаблонізатора на клієнті? Тоді «конфігурація» форми зберігається в View на сервері, а її перетворення в форму відбувається на клієнті. Особисто мені ця ідея імпонує.

Старший писарь Ванек давно с интересом наблюдал, как прилежно пишет вольноопределяющийся и при этом хохочет во все горло. Он встал и наклонился к вольноопределяющемуся, который тут же принялся ему объяснять:

— Страшно весело писать историю батальона впрок. Главное, чтобы все развивалось систематически. Во всем должна быть система.

— Систематическая система,- заметил старший писарь Ванек, скептически улыбаясь.

— Да,- небрежно обронил вольноопределяющийся,- систематизированная систематическая система при написании истории батальона. Мы не можем с самого начала одержать большую победу. Все должно развиваться постепенно, согласно определенному плану. Наш батальон не может сразу выиграть мировую войну.

Сей текст отдаёт стариной и копипастой, ну может переведён на русский. Но XSLT мёртв, ибо родился мёртвым. Вся его роль — прямая отдача чужого контента на сторонние ресурсы.

Моргните нам два раза если вас держат в заложниках джависты-староверы

Подписаться на комментарии