Успадкування та повторне використання шаблонів
Механізми повторного використання та успадкування шаблонів підвищать вашу продуктивність, оскільки кожен шаблон містить лише свій унікальний контент, а повторювані елементи та структури використовуються повторно. Представляємо три концепції: успадкування layout'ів, горизонтальне повторне використання та успадкування одиниць.
Концепція успадкування шаблонів Latte схожа на успадкування класів у PHP. Ви визначаєте батьківський шаблон, від якого можуть успадковуватися інші дочірні шаблони і можуть перезаписувати частини батьківського шаблону. Це чудово працює, коли елементи мають спільну структуру. Звучить складно? Не хвилюйтеся, це дуже просто.
Успадкування layout'ів {layout}
Розглянемо успадкування шаблону layout'у, тобто макета, на прикладі. Це
батьківський шаблон, який ми назвемо, наприклад, layout.latte
, і який
визначає каркас HTML-документа:
<!doctype html>
<html lang="en">
<head>
<title>{block title}{/block}</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="content">
{block content}{/block}
</div>
<div id="footer">
{block footer}© Copyright 2008{/block}
</div>
</body>
</html>
Теги {block}
визначають три блоки, які дочірні шаблони можуть
заповнити. Тег block лише повідомляє, що це місце може бути перезаписане
дочірнім шаблоном шляхом визначення власного блоку з такою самою
назвою.
Дочірній шаблон може виглядати так:
{layout 'layout.latte'}
{block title}My amazing blog{/block}
{block content}
<p>Welcome to my awesome homepage.</p>
{/block}
Ключовим тут є тег {layout}
. Він повідомляє Latte, що цей шаблон
«розширює» інший шаблон. Коли Latte рендерить цей шаблон, він спочатку
знаходить батьківський шаблон — у цьому випадку layout.latte
.
У цей момент Latte помічає три теги block у layout.latte
і замінює ці
блоки вмістом дочірнього шаблону. Оскільки дочірній шаблон не
визначив блок footer, замість нього використовується вміст з
батьківського шаблону. Вміст у тегу {block}
у батьківському
шаблоні завжди використовується як резервний.
Виведення може виглядати так:
<!doctype html>
<html lang="en">
<head>
<title>My amazing blog</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="content">
<p>Welcome to my awesome homepage.</p>
</div>
<div id="footer">
© Copyright 2008
</div>
</body>
</html>
У дочірньому шаблоні блоки можуть бути розміщені лише на найвищому рівні або всередині іншого блоку, тобто:
{block content}
<h1>{block title}Welcome to my awesome homepage{/block}</h1>
{/block}
Також блок завжди буде створено незалежно від того, чи буде
навколишня умова {if}
оцінена як true чи false. Тож, навіть якщо це не
виглядає так, цей шаблон визначає блок.
{if false}
{block head}
<meta name="robots" content="noindex, follow">
{/block}
{/if}
Якщо ви хочете, щоб виведення всередині блоку відображалося умовно, використовуйте замість цього наступне:
{block head}
{if $condition}
<meta name="robots" content="noindex, follow">
{/if}
{/block}
Простір поза блоками в дочірньому шаблоні виконується перед
рендерингом шаблону layout'у, тому ви можете використовувати його для
визначення змінних, таких як {var $foo = bar}
, і для поширення даних по
всьому ланцюжку успадкування:
{layout 'layout.latte'}
{var $robots = noindex}
...
Багаторівневе успадкування
Ви можете використовувати стільки рівнів успадкування, скільки вам потрібно. Поширеним способом використання успадкування layout'ів є наступний трирівневий підхід:
- Створіть шаблон
layout.latte
, який містить основний каркас зовнішнього вигляду сайту. - Створіть шаблон
layout-SECTIONNAME.latte
для кожної секції вашого сайту. Наприклад,layout-news.latte
,layout-blog.latte
тощо. Усі ці шаблони розширюютьlayout.latte
і включають стилі та дизайн, специфічні для окремих секцій. - Створіть індивідуальні шаблони для кожного типу сторінки, наприклад, для статті новин або запису в блозі. Ці шаблони розширюють відповідний шаблон секції.
Динамічне успадкування
Як назву батьківського шаблону можна використовувати змінну або будь-який вираз PHP, тому успадкування може поводитися динамічно:
{layout $standalone ? 'minimum.latte' : 'layout.latte'}
Ви також можете використовувати Latte API для автоматичного вибору шаблону layout'у.
Поради
Ось кілька порад щодо роботи з успадкуванням layout'ів:
- Якщо ви використовуєте
{layout}
у шаблоні, це має бути перший тег шаблону в цьому шаблоні. - Layout може автоматично
знаходитись (як, наприклад, у presenter'ах). У такому випадку,
якщо шаблон не повинен мати layout, він повідомляє про це тегом
{layout none}
. - Тег
{layout}
має псевдонім{extends}
. - Назва файлу layout'у залежить від завантажувача.
- Ви можете мати скільки завгодно блоків. Пам'ятайте, що дочірні шаблони не зобов'язані визначати всі батьківські блоки, тому ви можете заповнити розумні значення за замовчуванням у кількох блоках, а потім визначити лише ті, які вам потрібні пізніше.
Блоки {block}
Див. також анонімний {block}
Блок представляє спосіб змінити рендеринг певної частини шаблону, але жодним чином не втручається в логіку навколо нього. У наступному прикладі ми покажемо, як блок працює, а також як він не працює:
{foreach $posts as $post}
{block post}
<h1>{$post->title}</h1>
<p>{$post->body}</p>
{/block}
{/foreach}
Якщо ви відрендерите цей шаблон, результат буде точно таким же, як з
тегами {block}
, так і без них. Блоки мають доступ до змінних із
зовнішніх областей видимості. Вони лише дають можливість бути
перезаписаними дочірнім шаблоном:
{layout 'parent.Latte'}
{block post}
<article>
<header>{$post->title}</header>
<section>{$post->text}</section>
</article>
{/block}
Тепер при рендерингу дочірнього шаблону цикл буде використовувати
блок, визначений у дочірньому шаблоні child.Latte
, замість блоку,
визначеного в parent.Latte
; запущений шаблон тоді еквівалентний
наступному:
{foreach $posts as $post}
<article>
<header>{$post->title}</header>
<section>{$post->text}</section>
</article>
{/foreach}
Однак, якщо ми створимо нову змінну всередині іменованого блоку або замінимо значення існуючої, зміна буде видима лише всередині блоку:
{var $foo = 'foo'}
{block post}
{do $foo = 'new value'}
{var $bar = 'bar'}
{/block}
foo: {$foo} // виводить: foo
bar: {$bar ?? 'не визначено'} // виводить: не визначено
Вміст блоку можна змінити за допомогою фільтрів. Наступний приклад видаляє весь HTML і змінює регістр літер:
<title>{block title|stripHtml|capitalize}...{/block}</title>
Тег також можна записати як n:атрибут:
<article n:block=post>
...
</article>
Локальні блоки
Кожен блок перезаписує вміст батьківського блоку з такою самою назвою — крім локальних блоків. У класах це було б щось на зразок приватних методів. Таким чином, ви можете створювати шаблон, не турбуючись, що через збіг імен блоків вони будуть перезаписані з іншого шаблону.
{block local helper}
...
{/block}
Рендеринг блоків {include}
Див. також {include file}
Щоб вивести блок у певному місці, використовуйте тег
{include blockname}
:
<title>{block title}{/block}</title>
<h1>{include title}</h1>
Також можна вивести блок з іншого шаблону:
{include footer from 'main.latte'}
Блок, що рендериться, не має доступу до змінних активного контексту, за винятком випадків, коли блок визначено в тому ж файлі, де він і вставлений. Однак він має доступ до глобальних змінних.
Ви можете передавати змінні в блок таким чином:
{include footer, foo: bar, id: 123}
Як назву блоку можна використовувати змінну або будь-який вираз PHP. У
такому випадку перед змінною ми додаємо ключове слово block
, щоб
Latte вже під час компіляції знало, що це блок, а не включення шаблону, назва якого також
могла б бути у змінній:
{var $name = footer}
{include block $name}
Блок можна рендерити і всередині самого себе, що, наприклад, корисно при рендерингу деревовидної структури:
{define menu, $items}
<ul>
{foreach $items as $item}
<li>
{if is_array($item)}
{include menu, $item}
{else}
{$item}
{/if}
</li>
{/foreach}
</ul>
{/define}
Замість {include menu, ...}
ми можемо написати {include this, ...}
, де
this
означає поточний блок.
Блок, що рендериться, можна змінити за допомогою фільтрів. Наступний приклад видаляє весь HTML і змінює регістр літер:
<title>{include heading|stripHtml|capitalize}</title>
Батьківський блок
Якщо вам потрібно вивести вміст блоку з батьківського шаблону,
використовуйте {include parent}
. Це корисно, якщо ви хочете лише
доповнити вміст батьківського блоку, а не повністю його
перезаписати.
{block footer}
{include parent}
<a href="https://github.com/nette">GitHub</a>
<a href="https://twitter.com/nettefw">Twitter</a>
{/block}
Визначення {define}
Крім блоків, у Latte також існують «визначення». У звичайних мовах програмування ми б порівняли їх із функціями. Вони корисні для повторного використання фрагментів шаблону, щоб уникнути повторень.
Latte намагається робити речі простими, тому, по суті, визначення такі ж, як і блоки, і все, що сказано про блоки, стосується також і визначень. Вони відрізняються від блоків тим, що:
- вони укладені в теги
{define}
- вони рендеряться лише тоді, коли ви їх вставляєте
через
{include}
- їм можна визначати параметри, подібно до функцій у PHP
{block foo}<p>Hello</p>{/block}
{* виводить: <p>Hello</p> *}
{define bar}<p>World</p>{/define}
{* нічого не виводить *}
{include bar}
{* виводить: <p>World</p> *}
Уявіть, що у вас є допоміжний шаблон із колекцією визначень, як малювати HTML-форми.
{define input, $name, $value, $type = 'text'}
<input type={$type} name={$name} value={$value}>
{/define}
{define textarea, $name, $value}
<textarea name={$name}>{$value}</textarea>
{/define}
Аргументи завжди необов'язкові зі значенням за замовчуванням
null
, якщо не вказано значення за замовчуванням (тут 'text'
є
значенням за замовчуванням для $type
). Також можна оголошувати
типи параметрів: {define input, string $name, ...}
.
Шаблон з визначеннями завантажується за допомогою {import}
. Самі визначення рендеряться так само, як і блоки:
<p>{include input, 'password', null, 'password'}</p>
<p>{include textarea, 'comment'}</p>
Визначення не мають доступу до змінних активного контексту, але мають доступ до глобальних змінних.
Динамічні назви блоків
Latte дозволяє велику гнучкість при визначенні блоків, оскільки назва
блоку може бути будь-яким виразом PHP. Цей приклад визначає три блоки з
назвами hi-Peter
, hi-John
та hi-Mary
:
{foreach [Peter, John, Mary] as $name}
{block "hi-$name"}Hi, I am {$name}.{/block}
{/foreach}
У дочірньому шаблоні ми можемо пере-визначити, наприклад, лише один блок:
{block hi-John}Hello. I am {$name}.{/block}
Тож виведення виглядатиме так:
Hi, I am Peter.
Hello. I am John.
Hi, I am Mary.
Перевірка існування блоків {ifset}
Див. також {ifset $var}
За допомогою тесту {ifset blockname}
ми перевіряємо, чи існує блок (або
кілька блоків) у поточному контексті:
{ifset footer}
...
{/ifset}
{ifset footer, header, main}
...
{/ifset}
Як назву блоку можна використовувати змінну або будь-який вираз PHP. У
такому випадку перед змінною ми додаємо ключове слово block
, щоб
було зрозуміло, що це не тест на існування змінних:
{ifset block $name}
...
{/ifset}
Існування блоків також перевіряє функція hasBlock()
:
{if hasBlock(header) || hasBlock(footer)}
...
{/if}
Поради
Кілька порад щодо роботи з блоками:
- Останній блок верхнього рівня не обов'язково повинен мати закриваючий тег (блок закінчується кінцем документа). Це спрощує написання дочірніх шаблонів, які містять один основний блок.
- Для кращої читабельності ви можете вказати назву блоку в тегу
{/block}
, наприклад{/block footer}
. Однак назва повинна збігатися з назвою блоку. У великих шаблонах ця техніка допоможе вам визначити, які теги блоків закриваються. - Ви не можете безпосередньо визначити кілька тегів блоків з однаковою назвою в одному шаблоні. Однак цього можна досягти за допомогою динамічних назв блоків.
- Ви можете використовувати n:атрибути для визначення блоків
як
<h1 n:block=title>Welcome to my awesome homepage</h1>
- Блоки також можна використовувати без назв лише для застосування фільтрів:
{block|strip} hello {/block}
Горизонтальне повторне використання
{import}
Горизонтальне повторне використання є третім механізмом повторного
використання та успадкування в Latte. Воно дозволяє завантажувати блоки
з інших шаблонів. Це схоже на те, як ми створюємо файл із допоміжними
функціями в PHP, який потім завантажуємо за допомогою require
.
Хоча успадкування layout'ів шаблонів є однією з найпотужніших функцій Latte, воно обмежене простим успадкуванням — шаблон може розширювати лише один інший шаблон. Горизонтальне повторне використання — це спосіб досягти множинного успадкування.
Маємо файл з визначеннями блоків:
{block sidebar}...{/block}
{block menu}...{/block}
За допомогою команди {import}
ми імпортуємо всі блоки та визначення, визначені в blocks.latte
, до іншого
шаблону:
{import 'blocks.latte'}
{* тепер можна використовувати блоки sidebar та menu *}
Якщо ви імпортуєте блоки в батьківському шаблоні (тобто
використовуєте {import}
у layout.latte
), блоки будуть доступні
також у всіх дочірніх шаблонах, що дуже практично.
Шаблон, призначений для імпорту (наприклад, blocks.latte
), не повинен
розширювати інший шаблон, тобто використовувати
{layout}
. Однак він може імпортувати інші шаблони.
Тег {import}
повинен бути першим тегом шаблону після {layout}
.
Назва шаблону може бути будь-яким виразом PHP:
{import $ajax ? 'ajax.latte' : 'not-ajax.latte'}
Ви можете використовувати стільки команд {import}
у шаблоні,
скільки забажаєте. Якщо два імпортованих шаблони визначають один і той
самий блок, перемагає перший. Однак найвищий пріоритет має головний
шаблон, який може перезаписати будь-який імпортований блок.
Вміст перезаписаних блоків можна зберегти, вставивши блок так само, як вставляється батьківський блок:
{layout 'layout.latte'}
{import 'blocks.latte'}
{block sidebar}
{include parent}
{/block}
{block title}...{/block}
{block content}...{/block}
У цьому прикладі {include parent}
викликає блок sidebar
із шаблону
blocks.latte
.
Успадкування одиниць {embed}
Успадкування одиниць розширює ідею успадкування layout'ів до рівня фрагментів контенту. Тоді як успадкування layout'ів працює з «каркасом документа», який оживляють дочірні шаблони, успадкування одиниць дозволяє створювати каркаси для менших одиниць контенту та повторно використовувати їх де завгодно.
У спадкуванні одиниць ключовим є тег {embed}
. Він поєднує
поведінку {include}
та {layout}
. Дозволяє вставляти вміст іншого
шаблону чи блоку та необов'язково передавати змінні, так само як у
випадку {include}
. Також дозволяє перезаписувати будь-який блок,
визначений усередині вбудованого шаблону, як при використанні
{layout}
.
Наприклад, використаємо елемент акордеон. Розглянемо каркас
елемента, збережений у шаблоні collapsible.latte
:
<section class="collapsible {$modifierClass}">
<h4 class="collapsible__title">
{block title}{/block}
</h4>
<div class="collapsible__content">
{block content}{/block}
</div>
</section>
Теги {block}
визначають два блоки, які дочірні шаблони можуть
заповнити. Так, як у випадку батьківського шаблону при успадкуванні
layout'ів. Ви також бачите змінну $modifierClass
.
Давайте використаємо наш елемент у шаблоні. Тут на сцену виходить
{embed}
. Це надзвичайно потужний тег, який дозволяє нам робити все:
вставляти вміст шаблону елемента, додавати до нього змінні та додавати
до нього блоки з власним HTML:
{embed 'collapsible.latte', modifierClass: my-style}
{block title}
Hello World
{/block}
{block content}
<p>Lorem ipsum dolor sit amet, consectetuer adipiscing
elit. Nunc dapibus tortor vel mi dapibus sollicitudin.</p>
{/block}
{/embed}
Виведення може виглядати так:
<section class="collapsible my-style">
<h4 class="collapsible__title">
Hello World
</h4>
<div class="collapsible__content">
<p>Lorem ipsum dolor sit amet, consectetuer adipiscing
elit. Nunc dapibus tortor vel mi dapibus sollicitudin.</p>
</div>
</section>
Блоки всередині вбудованих тегів утворюють окремий шар, незалежний
від інших блоків. Тому вони можуть мати таку ж назву, як і блок поза
вбудовуванням, і на них це ніяк не впливає. За допомогою тегу include всередині тегів {embed}
ви можете вставляти
блоки, створені тут, блоки з вбудованого шаблону (які не є локальними), а також блоки з головного шаблону, які,
навпаки, є локальними. Ви також можете імпортувати блоки з інших файлів:
{block outer}…{/block}
{block local hello}…{/block}
{embed 'collapsible.latte', modifierClass: my-style}
{import 'blocks.latte'}
{block inner}…{/block}
{block title}
{include inner} {* працює, блок визначений всередині embed *}
{include hello} {* працює, блок є локальним у цьому шаблоні *}
{include content} {* працює, блок визначений у вбудованому шаблоні *}
{include aBlockDefinedInImportedTemplate} {* працює *}
{include outer} {* не працює! - блок знаходиться у зовнішньому шарі *}
{/block}
{/embed}
Вбудовані шаблони не мають доступу до змінних активного контексту, але мають доступ до глобальних змінних.
За допомогою {embed}
можна вставляти не тільки шаблони, але й інші
блоки, і, отже, попередній приклад можна було б записати таким чином:
{define collapsible}
<section class="collapsible {$modifierClass}">
<h4 class="collapsible__title">
{block title}{/block}
</h4>
...
</section>
{/define}
{embed collapsible, modifierClass: my-style}
{block title}
Hello World
{/block}
...
{/embed}
Якщо ми передаємо вираз у {embed}
і незрозуміло, чи це назва блоку
чи файлу, ми додаємо ключове слово block
або file
:
{embed block $name} ... {/embed}
Випадки використання
У Latte існують різні типи успадкування та повторного використання коду. Давайте підсумуємо основні концепції для більшої ясності:
{include template}
Випадок використання: Використання header.latte
та
footer.latte
всередині layout.latte
.
header.latte
<nav>
<div>Home</div>
<div>About</div>
</nav>
footer.latte
<footer>
<div>Copyright</div>
</footer>
layout.latte
{include 'header.latte'}
<main>{block main}{/block}</main>
{include 'footer.latte'}
{layout}
Випадок використання: Розширення layout.latte
всередині
homepage.latte
та about.latte
.
layout.latte
{include 'header.latte'}
<main>{block main}{/block}</main>
{include 'footer.latte'}
homepage.latte
{layout 'layout.latte'}
{block main}
<p>Homepage</p>
{/block}
about.latte
{layout 'layout.latte'}
{block main}
<p>About page</p>
{/block}
{import}
Випадок використання: sidebar.latte
у single.product.latte
та
single.service.latte
.
sidebar.latte
{block sidebar}<aside>Це бічна панель</aside>{/block}
single.product.latte
{layout 'product.layout.latte'}
{import 'sidebar.latte'}
{block main}<main>Сторінка продукту</main>{/block}
single.service.latte
{layout 'service.layout.latte'}
{import 'sidebar.latte'}
{block main}<main>Сторінка сервісу</main>{/block}
{define}
Випадок використання: Функції, яким ми передаємо змінні і щось рендеримо.
form.latte
{define form-input, $name, $value, $type = 'text'}
<input type={$type} name={$name} value={$value}>
{/define}
profile.service.latte
{import 'form.latte'}
<form action="" method="post">
<div>{include form-input, username}</div>
<div>{include form-input, password}</div>
<div>{include form-input, submit, Submit, submit}</div>
</form>
{embed}
Випадок використання: Вставка pagination.latte
у
product.table.latte
та service.table.latte
.
pagination.latte
<div id="pagination">
<div>{block first}{/block}</div>
{for $i = $min + 1; $i < $max - 1; $i++}
<div>{$i}</div>
{/for}
<div>{block last}{/block}</div>
</div>
product.table.latte
{embed 'pagination.latte', min: 1, max: $products->count}
{block first}Перша сторінка продукту{/block}
{block last}Остання сторінка продукту{/block}
{/embed}
service.table.latte
{embed 'pagination.latte', min: 1, max: $services->count}
{block first}Перша сторінка сервісу{/block}
{block last}Остання сторінка сервісу{/block}
{/embed}