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