|
| 1 | +AJAX і сніпети |
| 2 | +************** |
| 3 | + |
| 4 | +<div class=perex> |
| 5 | + |
| 6 | +Сучасні веб-додатки сьогодні працюють наполовину на сервері, а наполовину в браузері. AJAX є життєво важливим об'єднуючим фактором. Яку підтримку пропонує фреймворк Nette? |
| 7 | +- надсилання фрагментів шаблонів (так званих *сніпетів*) |
| 8 | +- передача змінних між PHP і JavaScript |
| 9 | +- Налагодження додатків AJAX |
| 10 | + |
| 11 | +</div> |
| 12 | + |
| 13 | +AJAX-запит може бути виявлений за допомогою методу сервісу [інкапсуляція HTTP-запиту |http:request] `$httpRequest->isAjax()` (визначає на основі HTTP-заголовка `X-Requested-With`). Існує також скорочений метод у презентері: `$this->isAjax()`. |
| 14 | + |
| 15 | +AJAX-запит нічим не відрізняється від звичайного - викликається презентер з певним поданням і параметрами. Від презентера також залежить, як він відреагує: він може використати свої процедури для повернення фрагмента HTML-коду (сніпету), XML-документа, об'єкта JSON або фрагмента коду Javascript. |
| 16 | + |
| 17 | +Існує попередньо оброблений об'єкт `payload`, призначений для надсилання даних у браузер у форматі JSON. |
| 18 | + |
| 19 | +```php |
| 20 | +public function actionDelete(int $id): void |
| 21 | +{ |
| 22 | + if ($this->isAjax()) { |
| 23 | + $this->payload->message = 'Успішно'; |
| 24 | + } |
| 25 | + // ... |
| 26 | +} |
| 27 | +``` |
| 28 | + |
| 29 | +Для повного контролю над виведенням JSON використовуйте метод `sendJson` у презентері. Це негайно перерве роботу презентера, і ви обійдетеся без шаблону: |
| 30 | + |
| 31 | +```php |
| 32 | +$this->sendJson(['key' => 'value', /* ... */]); |
| 33 | +``` |
| 34 | + |
| 35 | +Якщо ми хочемо надіслати HTML, ми можемо встановити спеціальний шаблон для AJAX-запитів: |
| 36 | + |
| 37 | +```php |
| 38 | +public function handleClick($param): void |
| 39 | +{ |
| 40 | + if ($this->isAjax()) { |
| 41 | + $this->template->setFile('path/to/ajax.latte'); |
| 42 | + } |
| 43 | + // ... |
| 44 | +} |
| 45 | +``` |
| 46 | + |
| 47 | + |
| 48 | +Naja .[#toc-naja] |
| 49 | +================= |
| 50 | + |
| 51 | +[Бібліотека Naja |https://naja.js.org] використовується для обробки AJAX-запитів на стороні браузера. [Встановіть |https://naja.js.org/#/guide/01-install-setup-naja] його як пакет node.js (для використання з Webpack, Rollup, Vite, Parcel та іншими): |
| 52 | + |
| 53 | +```shell |
| 54 | +npm install naja |
| 55 | +``` |
| 56 | + |
| 57 | +...або вставити безпосередньо в шаблон сторінки: |
| 58 | + |
| 59 | +```html |
| 60 | +<script src="https://unpkg.com/naja@2/dist/Naja.min.js"></script> |
| 61 | +``` |
| 62 | + |
| 63 | + |
| 64 | +Сніпети .[#toc-snippety] |
| 65 | +======================== |
| 66 | + |
| 67 | +Однак існує набагато потужніший інструмент вбудованої підтримки AJAX - сніпети. Їхнє використання дає змогу перетворити звичайний застосунок на AJAX-додаток за допомогою лише кількох рядків коду. Як усе це працює, показано в прикладі Fifteen, код якого також доступний у збірці або на [GitHub |https://github.com/nette-examples/fifteen]. |
| 68 | + |
| 69 | +Принцип роботи сніпетів полягає в тому, що всю сторінку передають під час початкового (тобто не-AJAX) запиту, а потім із кожним AJAX [subrequest |components#Signal] (запит до того самого подання того самого презентера) тільки код змінених частин передають до сховища `payload`, згаданого раніше. |
| 70 | + |
| 71 | +Сніпети можуть нагадати вам Hotwire для Ruby on Rails або Symfony UX Turbo, але Nette придумав їх чотирнадцятьма роками раніше. |
| 72 | + |
| 73 | + |
| 74 | +Інвалідація .[#toc-invalidation-of-snippets] |
| 75 | +============================================ |
| 76 | + |
| 77 | +Кожен нащадок класу [Control |components] (яким є і Presenter) здатний пам'ятати, чи були якісь зміни під час запиту, що вимагають повторного відображення. Існує кілька способів впоратися з цим: `redrawControl()` і `isControlInvalid()`. Приклад: |
| 78 | + |
| 79 | +```php |
| 80 | +public function handleLogin(string $user): void |
| 81 | +{ |
| 82 | + // Об'єкт має повторно відображатися після того, як користувач увійшов у систему |
| 83 | + $this->redrawControl(); |
| 84 | + // ... |
| 85 | +} |
| 86 | +``` |
| 87 | +Однак Nette забезпечує ще більш тонкий дозвіл, ніж цілі компоненти. Перераховані методи приймають ім'я так званого "фрагмента" як необов'язковий параметр. "Фрагмет" це, по суті, елемент у вашому шаблоні, позначений для цієї мети макросом Latte, докладніше про це пізніше. Таким чином, можна попросити компонент перемалювати тільки *частину* свого шаблону. Якщо весь компонент недійсний, то всі його фрагменти відображаються заново. Компонент є "недійсним", якщо будь-який з його субкомпонентів є недійсним. |
| 88 | + |
| 89 | +```php |
| 90 | +$this->isControlInvalid(); // -> false |
| 91 | + |
| 92 | +$this->redrawControl('header'); // анулює фрагмент з ім'ям 'header' |
| 93 | +$this->isControlInvalid('header'); // -> true |
| 94 | +$this->isControlInvalid('footer'); // -> false |
| 95 | +$this->isControlInvalid(); // -> true, принаймні один фрагмент недійсний |
| 96 | + |
| 97 | +$this->redrawControl(); // робить недійсним весь компонент, кожен фрагмент |
| 98 | +$this->isControlInvalid('footer'); // -> true |
| 99 | +``` |
| 100 | + |
| 101 | +Компонент, який отримав сигнал, автоматично позначається для перемальовування. |
| 102 | + |
| 103 | +Завдяки перемальовуванню фрагментів ми точно знаємо, які частини яких елементів мають бути перемальовані. |
| 104 | + |
| 105 | + |
| 106 | +Тег `{snippet} … {/snippet}` .{toc: Tag snippet} |
| 107 | +================================================ |
| 108 | + |
| 109 | +Рендеринг сторінки відбувається так само, як і під час звичайного запиту: завантажуються одні й ті самі шаблони тощо. Однак найважливіше - це не допустити потрапляння до вихідного сигналу тих частин, які не повинні потрапити до вихідного сигналу; інші частини мають бути пов'язані з ідентифікатором і надіслані користувачеві у форматі, зрозумілому для обробника JavaScript. |
| 110 | + |
| 111 | + |
| 112 | +Синтаксис .[#toc-sintaksis] |
| 113 | +--------------------------- |
| 114 | + |
| 115 | +Якщо в шаблоні є елемент управління або фрагмент, ми повинні обернути його за допомогою парного тега `{snippet} ... {/snippet}` - відмальований фрагмент буде "вирізаний" і відправиться в браузер. Він також укладе його в допоміжний тег `<div>` (можна використовувати інший). У наступному прикладі визначено сніппет з ім'ям `header`. Він також може являти собою шаблон компонента: |
| 116 | + |
| 117 | +```latte |
| 118 | +{snippet header} |
| 119 | + <h1>Hello ... </h1> |
| 120 | +{/snippet} |
| 121 | +``` |
| 122 | + |
| 123 | +Якщо ви хочете створити сніппет з іншим елементом, що містить, відмінним від `<div>`, або додати користувацькі атрибути до елемента, ви можете використовувати таке визначення: |
| 124 | + |
| 125 | +```latte |
| 126 | +<article n:snippet="header" class="foo bar"> |
| 127 | + <h1>Hello ... </h1> |
| 128 | +</article> |
| 129 | +``` |
| 130 | + |
| 131 | + |
| 132 | +Динамічні сніпети .[#toc-dinamiceskie-snippety] |
| 133 | +=============================================== |
| 134 | + |
| 135 | +У Nette ви також можете визначити сніпети з динамічним ім'ям, заснованим на параметрі часу виконання. Це найбільше підходить для різних списків, де нам потрібно змінити лише один рядок, але ми не хочемо переносити весь список разом із ним. Прикладом цього може бути: |
| 136 | + |
| 137 | +```latte |
| 138 | +<ul n:snippet="itemsContainer"> |
| 139 | + {foreach $list as $id => $item} |
| 140 | + <li n:snippet="item-$id">{$item} <a class="ajax" n:href="update! $id">обновить</a></li> |
| 141 | + {/foreach} |
| 142 | +</ul> |
| 143 | +``` |
| 144 | + |
| 145 | +Існує один статичний сніппет `itemsContainer`, що містить кілька динамічних сніпетів: `пункт-0`, `пункт-1` і так далі. |
| 146 | + |
| 147 | +Ви не можете перемалювати динамічний фрагмент безпосередньо (перемальовування `item-1` не має ефекту), ви маєте перемалювати його батьківський фрагмент (у цьому прикладі `itemsContainer`). При цьому виконується код батьківського сніпета, але браузеру передаються тільки його вкладені сніпети. Якщо ви хочете передати тільки один із вкладених сніпетів, вам потрібно змінити введення для батьківського сніпета, щоб не генерувати інші вкладені сніпети. |
| 148 | + |
| 149 | +У наведеному прикладі необхідно переконатися, що під час AJAX-запиту до масиву `$list` буде додано тільки один елемент, тому цикл `foreach` виводитиме тільки один динамічний фрагмент. |
| 150 | + |
| 151 | +```php |
| 152 | +class HomepagePresenter extends Nette\Application\UI\Presenter |
| 153 | +{ |
| 154 | + /** |
| 155 | + * Этот метод возвращает данные для списка. |
| 156 | + * Обычно это просто запрос данных из модели. |
| 157 | + * Для целей этого примера данные жёстко закодированы. |
| 158 | + */ |
| 159 | + private function getTheWholeList(): array |
| 160 | + { |
| 161 | + return [ |
| 162 | + 'First', |
| 163 | + 'Second', |
| 164 | + 'Third', |
| 165 | + ]; |
| 166 | + } |
| 167 | + |
| 168 | + public function renderDefault(): void |
| 169 | + { |
| 170 | + if (!isset($this->template->list)) { |
| 171 | + $this->template->list = $this->getTheWholeList(); |
| 172 | + } |
| 173 | + } |
| 174 | + |
| 175 | + public function handleUpdate(int $id): void |
| 176 | + { |
| 177 | + $this->template->list = $this->isAjax() |
| 178 | + ? [] |
| 179 | + : $this->getTheWholeList(); |
| 180 | + $this->template->list[$id] = 'Updated item'; |
| 181 | + $this->redrawControl('itemsContainer'); |
| 182 | + } |
| 183 | +} |
| 184 | +``` |
| 185 | + |
| 186 | + |
| 187 | +Сніпети в увімкненому шаблоні .[#toc-snippety-vo-vklyucennom-sablone] |
| 188 | +===================================================================== |
| 189 | + |
| 190 | +Може трапитися так, що сніппет міститься в шаблоні, який вмикається з іншого шаблону. У цьому разі необхідно обернути код включення в другому шаблоні макросом `snippetArea`, потім перемалювати як snippetArea, так і сам сніппет. |
| 191 | + |
| 192 | +Макрос `snippetArea` гарантує, що код усередині нього буде виконано, але браузеру буде надіслано тільки фактичний фрагмент включеного шаблону. |
| 193 | + |
| 194 | +```latte |
| 195 | +{* parent.latte *} |
| 196 | +{snippetArea wrapper} |
| 197 | + {include 'child.latte'} |
| 198 | +{/snippetArea} |
| 199 | +``` |
| 200 | +```latte |
| 201 | +{* child.latte *} |
| 202 | +{snippet item} |
| 203 | +... |
| 204 | +{/snippet} |
| 205 | +``` |
| 206 | +```php |
| 207 | +$this->redrawControl('wrapper'); |
| 208 | +$this->redrawControl('item'); |
| 209 | +``` |
| 210 | + |
| 211 | +Ви також можете поєднувати його з динамічними сніпетами. |
| 212 | + |
| 213 | + |
| 214 | +Додавання та видалення .[#toc-dobavlenie-i-udalenie] |
| 215 | +==================================================== |
| 216 | + |
| 217 | +Якщо додати новий елемент у список і анулювати `itemsContainer`, AJAX-запит поверне фрагменти, включно з новим, але javascript-обробник не зможе його відобразити. Це відбувається тому, що немає HTML-елемента з новоствореним ID. |
| 218 | + |
| 219 | +У цьому випадку найпростіший спосіб - обернути весь список у ще один сніпет і визнати його недійсним: |
| 220 | + |
| 221 | +```latte |
| 222 | +{snippet wholeList} |
| 223 | +<ul n:snippet="itemsContainer"> |
| 224 | + {foreach $list as $id => $item} |
| 225 | + <li n:snippet="item-$id">{$item} <a class="ajax" n:href="update! $id">обновить</a></li> |
| 226 | + {/foreach} |
| 227 | +</ul> |
| 228 | +{/snippet} |
| 229 | +<a class="ajax" n:href="add!">Добавить</a> |
| 230 | +``` |
| 231 | + |
| 232 | +```php |
| 233 | +public function handleAdd(): void |
| 234 | +{ |
| 235 | + $this->template->list = $this->getTheWholeList(); |
| 236 | + $this->template->list[] = 'New one'; |
| 237 | + $this->redrawControl('wholeList'); |
| 238 | +} |
| 239 | +``` |
| 240 | + |
| 241 | +Те ж саме стосується і видалення елемента. Можна було б надіслати порожній сніппет, але зазвичай списки можуть бути посторінковими, і було б складно реалізувати видалення одного елемента і завантаження іншого (який раніше перебував на іншій сторінці посторінкового списку). |
| 242 | + |
| 243 | + |
| 244 | +Надсилання параметрів компоненту .[#toc-otpravka-parametrov-komponentu] |
| 245 | +======================================================================= |
| 246 | + |
| 247 | +Коли ми надсилаємо параметри компоненту через AJAX-запит, чи то сигнальні, чи постійні параметри, ми повинні надати їхнє глобальне ім'я, яке також містить ім'я компонента. Повне ім'я параметра повертає метод `getParameterId()`. |
| 248 | + |
| 249 | +```js |
| 250 | +$.getJSON( |
| 251 | + {link changeCountBasket!}, |
| 252 | + { |
| 253 | + {$control->getParameterId('id')}: id, |
| 254 | + {$control->getParameterId('count')}: count |
| 255 | + } |
| 256 | +}); |
| 257 | +``` |
| 258 | + |
| 259 | +І обробити метод з відповідними параметрами в компоненті. |
| 260 | + |
| 261 | +```php |
| 262 | +public function handleChangeCountBasket(int $id, int $count): void |
| 263 | +{ |
| 264 | + |
| 265 | +} |
| 266 | +``` |
0 commit comments