-
-
Notifications
You must be signed in to change notification settings - Fork 282
/
components.texy
473 lines (329 loc) · 21.4 KB
/
components.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
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
Komponenty a ovládací prvky
***************************
/--div .[perex]
Komponenty představují základní kámen znovupoužitelnosti kódu, usnadňují vám práci a dovolují využívat práce komunity. Komponenty jsou báječné. Řekneme si
- jak psát komponenty?
- co jsou to signály?
- jak posílat flash zprávy?
- jak na AJAX?
\--
Komponenta představuje vykreslitelný objekt. Jsou to například formuláře, menu, ankety a podobně. V rámci jedné stránky jich může existovat libovolný počet. Na stránkách https://componette.com můžete najít open-source komponenty, které sem umístili dobrovolníci z komunity okolo Nette Framework.
Příklad komponenty a jejího začlenění do stránky najdete v [příkladu Fifteen |https://github.com/nette/examples/tree/master/Fifteen].
Komponenta je zpravidla potomkem třídy [api:Nette\Application\UI\Control], tím také začneme:
/--php
use Nette\Application\UI\Control;
class PollControl extends Control
{
}
\--
.[note]
Bavíme-li se o komponentách, obvykle myslíme potomky třídy [Control |api:Nette\Application\UI\Control]. Přesnější by tedy bylo používat termín „controls“ (tj. ovládací prvky), ale „kontrola“ má v češtině zcela jiný význam a spíš se ujaly „komponenty“.
Šablony
=======
Komponenta obsahuje továrnu na svou [šablonu |latte:]. Ta standardně vytvoří šablonu, předá ji některé základní proměnné a zaregistruje [standardní filtry |latte:filters]. O vykreslení se už musíme postarat sami, a to v metodě `render()`. Tam také musíme určit soubor, ze kterého bude šablona načtena, a zaregistrovat proměnné, které se budou v šabloně používat. Šablonu můžeme umístit do stejné složky a pod stejným názvem jako komponentu:
/--php
public function render()
{
// vložíme do šablony nějaké parametry
$this->template->param = $value;
// a vykreslíme ji
$this->template->render(__DIR__ . '/poll.latte');
}
\--
Ze šablony můžeme komponentě i předat parametry. Ty se předají až metodě `render()`.
/--html
{control poll $poll}
\--
/--php
// PollControl.php
public function render($poll) { ... }
\--
/--php
// PollPresenter.php
protected function createComponentPoll() { ... }
\--
Odkazy
======
Pomocí metody `link()` odkazujeme [na jednotlivé signály|#signál neboli subrequest]. V šablonách se odkazy vykreslují pomocí makra `{link}`, ze šablony komponenty můžeme odkázat i na libovolný presenter pomocí makra `{plink}`.
Příklad použití v komponentě:
/--php
$url = $this->link('click!', $x, $y);
\--
Příklad použití v šabloně:
/--html
<a n:href="click! $x, $y"> ... </a>
\--
Flash zprávy
============
Komponenta má své vlastní úložiště flash zpráv nezávislé na presenteru. Jde o zprávy, které např. informují o výsledku operace, po kterých **následuje přesměrování.**
Zasílání obstarává metoda [flashMessage |api:Nette\Application\UI\Control::flashMessage()]. Prvním parametrem je text zprávy a nepovinným druhým parametrem její typ (error, warning, info apod.). Metoda `flashMessage()` vrací instanci flash zprávy, které je možné přidávat další informace.
Příklad:
/--php
public function deleteFormSubmitted(Nette\Application\UI\Form $form)
{
// ... požádáme model o smazání záznamu ...
// předáme flash zprávu
$this->flashMessage('Položka byla smazána.');
$this->redirect(...); // a přesměrujeme
}
\--
Šabloně jsou tyto zprávy automaticky předány v proměnné `$flashes`. Tato proměnná obsahuje pole s objekty (`stdClass`), které obsahují vlastnosti `message` (text zprávy), `type` (typ zprávy) a mohou obsahovat již zmíněné extra informace.
Příklad:
/--php
{foreach $flashes as $flash}
<div class="flash {$flash->type}">{$flash->message}</div>
{/foreach}
\--
Nejdůležitejší samozřejmě je, že pokud po uložení zprávy `flashMessage()` následuje přesměrování, bude i v dalším požadavku v šabloně existovat stejný parametr `$flashes`. Zprávy zůstanou poté živé další 3 sekundy – například pro případ, že by z důvodu chybného přenosu uživatel stránku dal obnovit. Pokud někdo dvakrát za sebou obnoví stránku (F5), tak mu zpráva tedy nezmizí, pokud klikne jinam, tak ji už neuvidí.
Signál neboli subrequest
========================
Signál (aneb subrequest) je komunikace se serverem pod prahem normálního view, tedy akce, které se dějí, aniž by se změnilo view. View může měnit pouze presenter, proto komponenty pracují vždy pod tímto prahem, tudíž `$component->link()` vede na signál, `$presenter->link()` obvykle na view (nebo signál, je-li označen vykřičníkem přidaným na konec). Pro úplnost, i komponenta může volat `$this->presenter->link('view')`.
Signál způsobí znovunačtení stránky úplně stejně jako při původním požadavku (kromě případu, kdy je volán AJAXem) a vyvolá metodu `signalReceived($signal)`, jejíž výchozí implementace ve třídě `Nette\Application\UI\Component` se pokusí zavolat metodu složenou ze slov `handle{signal}`. Další zpracování je na daném objektu. Objekty, které dědí od `Component` (tzn. `Control` a `Presenter`) reagují tak, že se snaží zavolat metodu `handle{signal}` s příslušnými parametry.
Jinými slovy: vezme se definice funkce `handle{signal}` a všechny parametry, které přišly s požadavkem, a k argumentům se podle jména dosadí parametry z URL a pokusí se danou metodu zavolat. Např. jako prametr `$id` se předá hodnota z parametru `id` v URL, jako `$something` se předá `something` z URL, atd. A pokud metoda neexistuje, metoda `signalReceived` vyvolá [výjimku |api:Nette\Application\UI\BadSignalException].
Ukázka zpracování signálu:
/--php
public function handleClick($x, $y)
{
if (!$this->isClickable($x, $y)) {
throw new Nette\Application\UI\BadSignalException('Action not allowed.');
}
// ... processing of signal ...
}
\--
.[note]
Signál může přijímat jakákoliv komponenta, presenter nebo objekt, který implementuje rozhraní `ISignalReceiver` a je připojený do stromu komponent.
Mezi hlavní příjemce signálů budou patřit `Presentery` a vizuální komponenty dědící od `Control`. Signál má sloužit jako znamení pro objekt, že má něco udělat – anketa si má započítat hlas od uživatele, blok s novinkami se má rozbalit a zobrazit dvakrát tolik novinek, formulář byl odeslán a má zpracovat data a podobně.
.[note]
Signál se vždy volá na aktuálním presenteru a view, tudíž není možné jej směřovat jinam.
URL pro signál vytváříme pomocí metody [Component::link() |api:Nette\Application\UI\Component::link()]. Jako parametr `$destination` předáme řetězec `{signal}!` a jako `$args` pole argumentů, které chceme signálu předat. Signál se vždy volá na aktuální view s aktuálními parametry, parametry signálu se jen přidají. Navíc se přidává hned na začátku **parametr `?do`, který určuje signál**.
Jeho formát je buď `{signal}`, nebo `{signalReceiver}-{signal}`. `{signalReceiver}` je název komponenty v presenteru. Proto nemůže být v názvu komponenty pomlčka – používá se k oddělení názvu komponenty a signálu, je ovšem možné takto zanořit několik komponent.
Metoda [isSignalReceiver()|api:Nette\Application\UI\Presenter::isSignalReceiver()] ověří, zda je komponenta (první argument) příjemcem signálu (druhý argument). Druhý argument můžeme vynechat – pak zjišťuje, jestli je komponenta příjemcem jakéhokoliv signálu. Jako druhý parameter lze uvést `true` a tím ověřit, jestli je příjemcem nejen uvedená komponenta, ale také kterýkoliv její potomek.
V kterékoliv fázi předcházející `handle{signal}` můžeme vykonat signál manuálně zavoláním metody [processSignal()|api:Nette\Application\UI\Presenter::processSignal()], která si bere na starosti vyřízení signálu – vezme komponentu, která se určila jako příjemce signálu (pokud není určen příjemce signálu, je to presenter samotný) a pošle jí signál.
Příklad:
/--php
if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) {
$this->processSignal();
}
\--
Tím je signál provedený předčasně a už se nebude znovu volat.
Subrequest vs. request
----------------------
Rozdíly mezi signálem a požadavkem:
- subrequest přenáší všechny komponenty
- request přenáší pouze perzistentní komponenty
Invalidace a snippety
=====================
Při signálu může dojít ke změnám, které si vyžadují překreslit komponentu. K tomu slouží metody [redrawControl()|api:Nette\Application\UI\Control::redrawControl()] a [isControlInvalid()|api:Nette\Application\UI\Control::isControlInvalid()], což je základem AJAXu v Nette.
Nette však nabízí ještě jemnější rozlišení aktuálnosti, než na úrovni komponent, a to tzv. [snippetů |ajax] neboli ústřižků.
Lze tedy invalidovat a validovat na úrovni těchto snippetů (každá komponenta jich může mít libovolné množství). Pokud se invaliduje celá komponenta, tak je i každý její snippet považován za invalidní. Komponenta je invalidní i tehdy, pokud je invalidní některá z jejích podřazených komponent.
Více informací naleznete na [stránce věnované AJAXu |ajax].
Perzistentní parametry
======================
Často se stává, že je v komponentách potřeba držet nějaký parametr pro uživatele po celou dobu, kdy se s komponentou pracuje. Může to být například číslo stránky ve stránkování. Takový parametr označíme jako perzistentní pomocí anotace `@persistent`.
/--php
class PollControl extends Control
{
/** @persistent */
public $page = 1;
...
}
\--
Tento parametr bude automaticky přenášen v každém odkazu jako GET parametr, a to až do chvíle, kdy uživatel stránku s touto komponentou opustí.
.[caution]
Nikdy slepě nevěřte perzistentním parametrům, protože mohou být snadno podvrženy (přepsáním v URL adrese stránky). Ověřte si například, zda je číslo stránky v platném rozsahu.
Komponenty se závislostmi
=========================
Co když ale naše komponenta potřebuje nějaké věci k tomu aby fungovala, třeba PollControl potřebuje PollManager přes který by hlasovala a ukládala ankety, provede se to [vstříknutím |dependency-injection] do konstruktoru:
/--php
class PollControl extends Control
{
/** @var App\Model\PollManager */
private $pollManager;
/** @var int Id ankety pro kterou vytváříme komponentu */
private $pollId;
public function __construct($pollId, PollManager $pollManager)
{
$this->pollManager = $pollManager;
$this->pollId = $pollId;
}
public function handleVote($voteId)
{
$this->pollManager->vote($pollId, $voteId);
//...
}
}
\--
No jo, ale jak se to do toho konstruktoru dostane? Na to si napíšeme továrnu, tedy třídu, která nám komponentu vytvoří. Tahle továrna se bude chovat jako služba, takže ji zároveň musíme zaregistrovat jako službu do konfiguračního souboru.
/--php
class PollControlFactory
{
/** @var PollManager */
private $pollManager;
public function __construct(PollManager $pollManager)
{
$this->pollManager = $pollManager;
}
/**
* @return PollControl
*/
public function create($pollId)
{
return new PollControl($pollId, $this->pollManager);
}
}
\--
Takhle továrnu zaregistrujeme do našeho kontejneru v konfiguračním souboru:
/--neon
services:
- PollControlFactory
\--
a nakonec ji použijeme v našem presenteru:
/--php
class PollPresenter extends Nette\UI\Application\Presenter
{
/** @var PollControlFactory */
private $pollControlFactory;
public function __construct(PollControlFactory $pollControlFactory)
{
$this->pollControlFactory = $pollControlFactory;
}
protected function createComponentPollControl()
{
$pollId = 1; // můžeme si předat náš parametr
return $this->pollControlFactory->create($pollId);
}
}
\--
Naštěstí Nette takovéhle jednoduché továrny umí generovat, takže místo ní můžeme napsat jenom její interface a továrnu nám vygeneruje DI kontejner:
/--php
interface IPollControlFactory
{
/**
* @return PollControl
*/
public function create($pollId);
}
\--
Interface zaregistrujeme do našeho kontejneru v konfiguračním souboru:
/--neon
services:
- IPollControlFactory
\--
a v presenteru si ho opět necháme vstříknout a pracujeme s ním jako s původní továrnou.
A to je vše. Nette vnitřně tento interface naimplementuje a vstříkne nám ji do presenteru, kde ji už můžeme používat. Magicky nám právě do naší komponenty přidá i parametr `$pollId` a instanci třídy `App\Model\PollManager`.
Další použití komponent v souvislosti s DI je [popsáno tady | di-usage#komponenty]
Komponenty do hloubky
=====================
Komponenty bývají ve většině případů vykreslitelné. Vedle nich však existují i nevykreslitelné komponenty. Stejně tak některé komponenty mohou mít potomky, jiné zase ne. Nette Framework pro všechny tyto typy komponent zavádí několik tříd a rozhraní.
Dědičnost objektů nám umožňuje třídy zařadit do hierarchické struktury, stejně jako je to v reálném světě. Můžeme totiž vytvářet nové třídy odvozením od jiných. Tyto odvozené třídy jsou pak potomkem původní třídy a dědí jeho členské proměnné a metody. Odvozená třída může přidávat další funkcionalitu (metody a členské proměnné) k již zděděným schopnostem.
Ke správnému pochopení "jak věci pracují", je potřeba vědět, kde má která třída své kořeny.
/--
Nette\ComponentModel\Component { IComponent }
|
+- Nette\ComponentModel\Container { IContainer }
|
+- Nette\Application\UI\Component { ISignalReceiver, IStatePersistent }
|
+- Nette\Application\UI\Control { IPartiallyRenderable }
|
+- Nette\Application\UI\Presenter { IPresenter }
\--
Nette\ComponentModel\IComponent .{toc: IComponent}
--------------------------------------------------
Rozhraní [api:Nette\ComponentModel\IComponent] musí implementovat každá komponenta. Vyžaduje metodu `getName()` vracející její název a metodu `getParent()` vracející jejího rodiče. Obojí lze nastavit metodou `setParent()` - první parametr je rodič a druhý název komponenty.
Nette\ComponentModel\Component .{toc: Component}
------------------------------------------------
[api:Nette\ComponentModel\Component] je standardní implementací `IComponent`. Je společným předkem všech komponent, vycházejí z ní všechny prvky formulářů. Obsahuje metody zjišťují příbuznost objektů a hlavně propojení (provázání) s rodiči:
[lookup($type)|api:Nette\ComponentModel\Component::lookup()] vyhledá v hierarchii směrem nahoru objekt požadované třídy nebo rozhraní. Například `$component->lookup(Nette\Application\UI\Presenter::class)` vrací presenter, pokud je k němu, i přes několik úrovní, komponenta připojena.
[lookupPath($type)|api:Nette\ComponentModel\Component::lookupPath()] vrací tzv. cestu, což je řetězec vzniklý spojením jmen všech komponent na cestě mezi aktuální a hledanou komponentou. Takže např. `$component->lookupPath(Nette\Application\UI\Presenter::class)` vrací jedinečný identifikátor komponenty vůči presenteru.
Nette\ComponentModel\IContainer .{toc: IContainer}
--------------------------------------------------
Rodičovské komponenty kromě rozhraní `IComponent` implementují i [api:Nette\ComponentModel\IContainer], které obsahuje metody pro přidání, odebrání, získání a iterací nad komponentami. Komponenty pak mohou vytvářet hierarchii - např. presentery mohou obsahovat formuláře obsahující textová políčka a tlačítka. Celý strom komponent je tedy tvořen větvemi v podobě objektů `IContainer` a listů `IComponent`.
Nette\ComponentModel\Container .{toc: Container}
------------------------------------------------
[api:Nette\ComponentModel\Container] je standardní implementací rozhraní `IContainer`. Je předkem například formuláře a tříd `Control` či `Presenter`.
Disponuje metodami pro snadné přidávání, získávání a odstraňování objektů a samozřejmě iteraci nad svým obsahem. Při pokusu o získání nedefinovaného potomka je zavolána továrna `createComponent($name)`. Metoda `createComponent($name)` zavolá v aktuální komponentě metodu `createComponent<název komponenty>` a jako parametr jí předá název komponenty. Vytvořená komponenta je poté přidána do aktuální komponenty jako její potomek. Těmto metodán říkáme továrny na komponenty a mohou je implementovat potomci třídy `Container`.
Nette\Application\UI\Component .{toc: Component}
------------------------------------------------
Třída [api:Nette\Application\UI\Component] je předek všech komponent používaných v presenteru. Komponenty presenteru jsou objekty, které si presenter uchovává počas svého [životního cyklu |presenters#zivotni-cyklus-presenteru].
Mají schopnost vzájemně ovlivňovat ostatní poděděné komponenty, ukládat své stavy do URL a odpovídat na uživatelské příkazy (signály) a nemusí být vykreslitelné.
Nette\Application\UI\Control .{toc: Control}
--------------------------------------------
[Control |api:Nette\Application\UI\Control] je vykreslitelná komponenta, znovupoužitelná součást webové aplikace, které se věnuje celá tato kapitola. Tuto třídu (nebo její potomky) máme obvykle na mysli, když hovoříme o komponentách. Navíc si umí pamatovat, kterou svou část má vykreslit při [AJAXovém požadavku |ajax#invalidace], jak [jsme si už ukázali|#invalidace a snippety].
`Control` nepředstavuje výseč stránky, ale její logickou část. Je možné ji renderovat opakovaně, nebo podmíněně a klidně pokaždé s jinou šablonou.
Znovupoužitelná součást aplikace.
Strom komponent
---------------
Uvedením stejné komponenty pod různými jmény se dá dosáhnout například zobrazení jedné komponenty na stránce vícekrát. Rodičem může být presenter, nějaká komponenta nebo jakýkoliv jiný objekt implementující rozhraní `IContainer`.
Hierarchie pak může vypadat nějak takto:
/--
Nette\Application\UI\Presenter { kořenem ve stromu komponent je vždy presenter }
|
--Nette\Application\UI\Control { implementuje IContainer => může být rodičem }
|
--Nette\ComponentModel\Component
|
--Nette\ComponentModel\Component { neimplementuje IContainer => nemůže být rodičem }
|
--Nette\Application\UI\Control
|
--Nette\ComponentModel\Component
\--
Zpožděné provázání
------------------
Komponentový model Nette umožňuje velmi dynamickou práci se stromem (komponenty můžeme vyjímat, přesouvat, přidávat), proto by byla chyba se spoléhat na to, že po vytvoření komponenty je hned (v konstruktoru) znám rodič, rodič rodiče atd. Většinou totiž rodič při vytvoření vůbec známý není.
/--php
$control = new NewsControl;
// ...
$parent->addComponent($control, 'shortNews');
\--
Monitorování předků
-------------------
Jak poznat, kdy byla komponenta připojena do stromu presenteru? Sledovat změnu rodiče nestačí, protože k presenteru mohl být připojen třeba rodič rodiče. Pomůže metoda [monitor($type)|api:Nette\ComponentModel\Component::monitor()]. Každá komponenta může monitorovat libovolný počet tříd a rozhraní. Připojení nebo odpojení je ohlášeno zavoláním metody `attached($obj)` resp. `detached($obj)`, kde `$obj` je objekt sledované třídy.
Pro lepší pochopení příklad: třída `UploadControl`, reprezentující formulářový prvek pro upload souborů v Nette Forms, musí formuláři nastavit atribut `enctype` na hodnotu `multipart/form-data`. V době vytvoření objektu ale k žádnému formuláři připojena být nemusí. Ve kterém okamžiku tedy formulář modifikovat? Řešení je jednoduché - v konstruktoru se požádá o monitoring:
/--php
class UploadControl extends Nette\Forms\Controls\BaseControl
{
public function __construct($label)
{
$this->monitor(Nette\Forms\Form::class);
// ...
}
// ...
}
\--
a jakmile je formulář k dispozici, zavolá se metoda attached:
/--php
protected function attached($form)
{
parent::attached($form);
if ($form instanceof Nette\Forms\Form) {
$form->getElementPrototype()->enctype = 'multipart/form-data';
}
}
\--
Od `nette/component-model` v2.4 je preferovaný způsob předat callbacky přímo metodě `monitor($type, $attached = null, $detached = null)`:
/--php
class UploadControl extends Nette\Forms\Controls\BaseControl
{
public function __construct($label)
{
$this->monitor(Nette\Forms\Form::class, function ($form) {
$form->getElementPrototype()->enctype = 'multipart/form-data';
});
}
}
\--
Iterování nad dětmi
-------------------
K iterování slouží metoda [getComponents($deep = false, $type = null)|api:Nette\ComponentModel\Container::getComponents()]. První parametr určuje, zda se mají komponenty procházet do hloubky (neboli rekurzivně). S hodnotou `true` tedy nejen projde všechny komponenty, jichž je rodičem, ale také potomky svých potomků atd. Druhý parametr slouží jako volitelný filtr podle tříd nebo rozhraní.
Například takto nějak se "interně"((dělá to framework sám v jádru, není třeba tento kód volat explicitně)) provádí ověření validace prvků:
/--php
$valid = true;
foreach ($form->getComponents(true, Nette\Forms\IControl::class) as $control) {
if (!$control->getRules()->validate()) {
$valid = false;
break;
}
}
\--