Interaktivne komponente
Komponente so ločeni predmeti za večkratno uporabo, ki jih namestimo na strani. To so lahko obrazci, podatkovne mreže, ankete, pravzaprav vse, kar je smiselno uporabljati večkrat. Prikazali bomo:
- kako uporabljati komponente?
- kako jih napisati?
- kaj so signali?
Nette ima vgrajen sistem komponent. Starejši med vami se morda spomnite nečesa podobnega iz Delphija ali ASP.NET Web Forms. React ali Vue.js sta zgrajena na nečem zelo podobnem. Vendar pa je v svetu ogrodij PHP to povsem edinstvena lastnost.
Hkrati pa komponente temeljito spremenijo pristop k razvoju aplikacij. Strani lahko sestavite iz vnaprej pripravljenih enot. Ali v administraciji potrebujete podatkovno mrežo? Najdete jo lahko v Componette, skladišču odprtokodnih dodatkov (ne le komponent) za Nette, in jo preprosto prilepite v predstavnik.
V predstavitveni program lahko vključite poljubno število komponent. V nekatere komponente pa lahko vstavite druge komponente. Tako nastane drevo komponent, katerega koren je predstavnik.
Tovarniške metode
Kako se komponente namestijo in nato uporabijo v predstavitvenem programu? Običajno z uporabo tovarniških metod.
Tovarna komponent je eleganten način za ustvarjanje komponent samo takrat, ko jih resnično potrebujemo (lenoba / na zahtevo).
Celotna čarovnija je v izvajanju metode, imenovane createComponent<Name>()
, kjer <Name>
je
ime komponente, ki se ustvari in vrne.
class DefaultPresenter extends Nette\Application\UI\Presenter
{
protected function createComponentPoll(): PollControl
{
$poll = new PollControl;
$poll->items = $this->item;
return $poll;
}
}
Ker so vse komponente ustvarjene v ločenih metodah, je koda čistejša in lažje berljiva.
Imena komponent se vedno začnejo z malo črko, čeprav so v imenu metode zapisana z veliko začetnico.
Nikoli ne kličemo tovarn neposredno, pokličejo se samodejno, ko prvič uporabimo komponente. Zaradi tega se komponenta ustvari v pravem trenutku in le, če jo resnično potrebujemo. Če komponente ne bi uporabili (na primer pri kakšni zahtevi AJAX, kjer vrnemo le del strani, ali ko so deli v predpomnilniku), se sploh ne bi ustvarila in prihranili bi zmogljivost strežnika.
// dostopamo do komponente in ali je bilo to prvič,
// pokliče createComponentPoll(), da jo ustvari
$poll = $this->getComponent('poll');
// alternativna sintaksa: $poll = $this['poll'];
V predlogi lahko komponento prikažete z uporabo oznake {control}. Tako ni potrebe po ročnem posredovanju komponent predlogi.
<h2>Please Vote</h2>
{control poll}
Hollywoodski slog
Sestavni deli pogosto uporabljajo kul tehniko, ki jo radi imenujemo hollywoodski slog. Zagotovo poznate kliše, ki ga igralci pogosto slišijo na kastingih: „Ne kličite nas, mi bomo poklicali vas.“ In prav za to gre v tem primeru.
V Nette, namesto da bi nenehno postavljali vprašanja („je bil obrazec oddan?“, „je bil veljaven?“ ali „je kdo pritisnil ta gumb?“), ogrodju poveste „ko se to zgodi, pokliči to metodo“ in mu prepustite nadaljnje delo. Če programirate v jeziku JavaScript, ste seznanjeni s tem slogom programiranja. Napišete funkcije, ki se pokličejo, ko se zgodi določen dogodek. In gonilo jim posreduje ustrezne parametre.
To popolnoma spremeni način pisanja aplikacij. Več nalog, kot jih lahko prenesete na ogrodje, manj dela imate. In manj lahko pozabite.
Kako napisati komponento
S komponento običajno mislimo na potomce razreda Nette\Application\UI\Control. Tudi sam
predstavnik Nette\Application\UI\Presenter
je potomec razreda Control
.
use Nette\Application\UI\Control;
class PollControl extends Control
{
}
Prikazovanje
Vemo že, da se oznaka {control componentName}
uporablja za risanje komponente. Pravzaprav kliče metodo
render()
komponente, v kateri poskrbimo za upodabljanje. Podobno kot v predstavitvenem programu imamo tudi tu
v spremenljivki $this->template
predlogo Latte, ki ji posredujemo parametre. Za
razliko od uporabe v predstavitvenem programu moramo določiti datoteko predloge in ji omogočiti, da se izrisuje:
public function render(): void
{
// v predlogo bomo vnesli nekaj parametrov
$this->template->param = $value;
// in jo narisali
$this->template->render(__DIR__ . '/poll.latte');
}
Z oznako {control}
lahko metodi render()
posredujemo parametre:
{control poll $id, $message}
public function render(int $id, string $message): void
{
// ...
}
Včasih je lahko komponenta sestavljena iz več delov, ki jih želimo prikazati ločeno. Za vsakega od njih bomo ustvarili
svojo metodo upodabljanja, tu je na primer renderPaginator()
:
public function renderPaginator(): void
{
// ...
}
V predlogi jo nato pokličemo z uporabo:
{control poll:paginator}
Za boljše razumevanje je dobro vedeti, kako je oznaka sestavljena v kodo PHP.
{control poll}
{control poll:paginator 123, 'hello'}
To se sestavi v:
$control->getComponent('poll')->render();
$control->getComponent('poll')->renderPaginator(123, 'hello');
getComponent()
metoda vrne komponento poll
, nato pa se na njej kliče metoda render()
oziroma renderPaginator()
.
Če se kjer koli v delu parametrov uporabi =>
, se vsi parametri zavijejo v polje in
posredujejo kot prvi argument:
{control poll, id: 123, message: 'hello'}
se pretvori v:
$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']);
{control cartControl-someForm}
sestavi na:
$control->getComponent("cartControl-someForm")->render();
Komponente, kot so predstavniki, predlogam samodejno posredujejo več uporabnih spremenljivk:
$basePath
je absolutna pot URL do korenskega dirja (na primer/CD-collection
)$baseUrl
je absolutna pot URL do korenskega dirja (na primerhttp://localhost/CD-collection
)$user
je objekt, ki predstavlja uporabnika$presenter
je trenutni predstavnik$control
je trenutna komponenta$flashes
seznam sporočil, poslanih z metodoflashMessage()
Signal
Vemo že, da je navigacija v aplikaciji Nette sestavljena iz povezovanja ali preusmerjanja na pare
Presenter:action
. Kaj pa, če želimo izvesti dejanje samo na tekoči strani? Na primer, spremeniti vrstni red
razvrščanja stolpca v tabeli; izbrisati element; preklopiti svetlobni/temni način; oddati obrazec; glasovati
v anketi; itd.
Tovrstna zahteva se imenuje signal. In tako kot akcije kličejo metode action<Action>()
ali
render<Action>()
, signali kličejo metode handle<Signal>()
. Medtem ko se koncept akcije (ali
pogleda) nanaša samo na predstavnike, signali veljajo za vse komponente. Torej tudi za predstavnike, saj je
UI\Presenter
potomec UI\Control
.
public function handleClick(int $x, int $y): void
{
// ... obdelava signalov ...
}
Povezava, ki kliče signal, se ustvari na običajen način, tj. v predlogi z atributom n:href
ali oznako
{link}
, v kodi pa z metodo link()
. Več v poglavju Ustvarjanje povezav URL.
<a n:href="click! $x, $y">click here</a>
Signal se vedno pokliče v trenutnem predstavniku in pogledu, zato povezave do signala ni mogoče vzpostaviti v drugem predstavniku/ukrepu.
Tako signal povzroči ponovno nalaganje strani na popolnoma enak način kot v prvotni zahtevi, le da dodatno pokliče metodo za obdelavo signala z ustreznimi parametri. Če metoda ne obstaja, se vrže izjema Nette\Application\UI\BadSignalException, ki se uporabniku prikaže kot stran z napako 403 Forbidden.
Utrinki in AJAX
Signali vas morda malce spominjajo na AJAX: obdelave, ki se kličejo na trenutni strani. In prav imate, signale res pogosto kličemo z uporabo AJAX-a, nato pa brskalniku posredujemo le spremenjene dele strani. Imenujejo se odlomki (snippets). Več informacij lahko najdete na strani o AJAXu.
Sporočila Flash
Komponenta ima lastno shrambo sporočil flash, ki je neodvisna od predstavnika. To so sporočila, ki na primer obveščajo o rezultatu operacije. Pomembna lastnost bliskovnih sporočil je, da so na voljo v predlogi tudi po preusmeritvi. Tudi po prikazu bodo ostala živa še 30 sekund – na primer, če bi uporabnik nenamerno osvežil stran – sporočilo se ne bo izgubilo.
Pošiljanje se izvede z metodo flashMessage. Prvi
parameter je besedilo sporočila ali objekt stdClass
, ki predstavlja sporočilo. Neobvezni drugi parameter je njegova
vrsta (napaka, opozorilo, informacija itd.). Metoda flashMessage()
vrne primerek sporočila flash kot objekt
stdClass, ki mu lahko posredujete informacije.
$this->flashMessage('Item was deleted.');
$this->redirect(/* ... */); // in preusmerite
V predlogi so ta sporočila na voljo v spremenljivki $flashes
kot objekti stdClass
, ki vsebujejo
lastnosti message
(besedilo sporočila), type
(vrsta sporočila) in lahko vsebujejo že omenjene podatke
o uporabniku. Narišemo jih na naslednji način:
{foreach $flashes as $flash}
<div class="flash {$flash->type}">{$flash->message}</div>
{/foreach}
Preusmeritev po signalu
Po obdelavi signala komponente pogosto sledi preusmeritev. Ta situacija je podobna obrazcem – po oddaji obrazca prav tako preusmerimo, da preprečimo ponovno oddajo podatkov ob osvežitvi strani v brskalniku.
$this->redirect('this') // redirects to the current presenter and action
Ker je komponenta element za večkratno uporabo in običajno ne sme biti neposredno odvisna od določenih predstavnikov, metodi
redirect()
in link()
samodejno interpretirata parameter kot signal komponente:
$this->redirect('click') // redirects to the 'click' signal of the same component
Če želite preusmeriti na drug predstavnik ali dejanje, lahko to storite prek predstavnika:
$this->getPresenter()->redirect('Product:show'); // redirects to a different presenter/action
Trajni parametri
Trajni parametri se uporabljajo za ohranjanje stanja v komponentah med različnimi zahtevami. Njihova vrednost ostane enaka tudi po kliku na povezavo. Za razliko od podatkov seje se prenesejo v naslovu URL. In se prenesejo samodejno, vključno s povezavami, ustvarjenimi v drugih komponentah na isti strani.
Na primer, imate komponento za listanje vsebine. Na strani je lahko več takšnih komponent. Želite, da vse komponente
ostanejo na trenutni strani, ko kliknete povezavo. Zato je številka strani (page
) trajni parameter.
Ustvarjanje trajnega parametra je v programu Nette zelo enostavno. Ustvarite le javno lastnost in jo označite z atributom:
(prej je bila uporabljena /** @persistent */
)
use Nette\Application\Attributes\Persistent; // ta vrstica je pomembna
class PaginatingControl extends Control
{
#[Persistent]
public int $page = 1; // morajo biti javni.
}
Priporočamo, da pri lastnosti navedete podatkovno vrsto (npr. int
), vključite pa lahko tudi privzeto vrednost.
Vrednosti parametrov je mogoče potrditi.
Pri ustvarjanju povezave lahko spremenite vrednost trajnega parametra:
<a n:href="this page: $page + 1">next</a>
Lahko ga tudi resetirate, tj. odstranite iz URL-ja. Nato bo prevzel privzeto vrednost:
<a n:href="this page: null">reset</a>
Trajne komponente
Ne le parametri, tudi komponente so lahko trajne. Njihovi trajni parametri se prenašajo tudi med različnimi akcijami ali med
različnimi predstavniki. Trajne komponente označimo s temi opombami za razred presenter. Na primer, tukaj komponente
calendar
in poll
označimo na naslednji način:
/**
* @persistent(calendar, poll)
*/
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}
Podsestav ni treba označiti kot trajne, saj so trajne samodejno.
V PHP 8 lahko za označevanje trajnih komponent uporabite tudi atribute:
use Nette\Application\Attributes\Persistent;
#[Persistent('calendar', 'poll')]
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}
Komponente z odvisnostmi
Kako ustvariti komponente z odvisnostmi, ne da bi „zmotili“ predstavnike, ki jih bodo uporabljali? Zahvaljujoč pametnim funkcijam vsebnika DI v Nette, lahko tako kot pri uporabi tradicionalnih storitev večino dela prepustimo ogrodju.
Kot primer vzemimo komponento, ki je odvisna od storitve PollFacade
:
class PollControl extends Control
{
public function __construct(
private int $id, // Id ankete, za katero je ustvarjena komponenta
private PollFacade $facade,
) {
}
public function handleVote(int $voteId): void
{
$this->facade->vote($id, $voteId);
// ...
}
}
Če bi pisali klasično storitev, nam ne bi bilo treba skrbeti. Kontejner DI bi nevidno poskrbel za posredovanje vseh
odvisnosti. Vendar pa komponente običajno obravnavamo tako, da ustvarimo njihov nov primerek neposredno v predstavniku v tovarniških metodah createComponent...()
. Toda posredovanje vseh odvisnosti vseh
komponent predstavniku, da bi jih nato posredoval komponentam, je okorno. In količina kode, ki je napisana…
Logično vprašanje je, zakaj ne bi komponente preprosto registrirali kot klasično storitev, jo posredovali predstavniku in jo
nato vrnili v metodi createComponent...()
? Toda ta pristop je neprimeren, saj želimo imeti možnost, da komponento
ustvarimo večkrat.
Pravilna rešitev je, da za komponento napišemo tovarno, tj. razred, ki za nas ustvari komponento:
class PollControlFactory
{
public function __construct(
private PollFacade $facade,
) {
}
public function create(int $id): PollControl
{
return new PollControl($id, $this->facade);
}
}
Zdaj našo storitev registriramo v vsebniku DI za konfiguracijo:
services:
- PollControlFactory
Na koncu bomo to tovarno uporabili v našem predstavitvenem programu:
class PollPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private PollControlFactory $pollControlFactory,
) {
}
protected function createComponentPollControl(): PollControl
{
$pollId = 1; // lahko posredujemo naš parameter
return $this->pollControlFactory->create($pollId);
}
}
Odlično je, da lahko Nette DI ustvari tako preproste tovarne, tako da morate namesto celotne kode napisati le njen vmesnik:
interface PollControlFactory
{
public function create(int $id): PollControl;
}
To je vse. Nette interno implementira ta vmesnik in ga vbrizga v naš predstavnik, kjer ga lahko uporabimo. Prav tako
čudežno posreduje naš parameter $id
in primerek razreda PollFacade
v našo komponento.
Komponente poglobljeno
Komponente v aplikaciji Nette so ponovno uporabni deli spletne aplikacije, ki jih vgrajujemo v strani, kar je predmet tega poglavja. Kakšne natančno so zmožnosti takšne komponente?
- mogoče jo je izrisati v predlogi
- ve, kateri del sebe naj prikaže med zahtevo AJAX (fragmenti).
- ima možnost shranjevanja svojega stanja v naslovu URL (trajni parametri).
- ima sposobnost odzivanja na dejanja uporabnika (signali)
- ustvarja hierarhično strukturo (kjer je korenski element predstavnik)
Za vsako od teh funkcij skrbi eden od razredov dedne linije. Za upodabljanje (1 + 2) skrbi razred Nette\Application\UI\Control, za vključitev v življenjski cikel (3, 4) razred Nette\Application\UI\Component, za ustvarjanje hierarhične strukture (5) pa razreda Container in Component.
Nette\ComponentModel\Component { IComponent }
|
+- Nette\ComponentModel\Container { IContainer }
|
+- Nette\Application\UI\Component { SignalReceiver, StatePersistent }
|
+- Nette\Application\UI\Control { Renderable }
|
+- Nette\Application\UI\Presenter { IPresenter }
Življenjski cikel komponente
Življenjski cikel komponente
Potrjevanje trajnih parametrov
Vrednosti trajnih parametrov, prejetih z naslovov URL, se zapišejo v lastnosti
z metodo loadState()
. Preveri tudi, ali se podatkovna vrsta, določena za lastnost, ujema, sicer se odzove z napako
404 in stran se ne prikaže.
Nikoli ne zaupajte slepo trajnim parametrom, saj jih lahko uporabnik zlahka prepiše v naslovu URL. Tako na primer preverimo,
ali je številka strani $this->page
večja od 0. Dober način za to je, da prekrijemo zgoraj omenjeno metodo
loadState()
:
class PaginatingControl extends Control
{
#[Persistent]
public int $page = 1;
public function loadState(array $params): void
{
parent::loadState($params); // tukaj je nastavljen $this->page
// sledi preverjanju uporabniške vrednosti:
if ($this->page < 1) {
$this->error();
}
}
}
Nasprotni postopek, to je zbiranje vrednosti iz trajnih lastnosti, je obdelan z metodo saveState()
.
Signali v globino
Signal povzroči ponovno nalaganje strani kot prvotna zahteva (z izjemo AJAX) in kliče metodo
signalReceived($signal)
, katere privzeta implementacija v razredu Nette\Application\UI\Component
poskuša poklicati metodo, sestavljeno iz besed handle{Signal}
. Nadaljnja obdelava je odvisna od danega predmeta.
Objekti, ki so potomci Component
(tj. Control
in Presenter
), poskušajo poklicati
handle{Signal}
z ustreznimi parametri.
Z drugimi besedami: vzame se definicija metode handle{Signal}
in vsi parametri, ki so bili prejeti v zahtevi, se
ujemajo s parametri metode. To pomeni, da se parameter id
iz naslova URL ujema s parametrom metode
$id
, something
s parametrom $something
in tako naprej. In če metoda ne obstaja, metoda
signalReceived
vrže izjemo.
Signal lahko prejme vsaka komponenta, predstavnik objekta, ki implementira vmesnik SignalReceiver
, če je povezan
z drevesom komponent.
Glavni prejemniki signalov so Presenters
in vizualne komponente, ki razširjajo Control
. Signal je
znak za objekt, da mora nekaj storiti – anketa šteje glas uporabnika, polje z novicami se mora razviti, obrazec je bil poslan
in mora obdelati podatke itd.
URL za signal se ustvari z metodo Component::link(). Kot parameter
$destination
posredujemo niz {signal}!
, kot $args
pa polje argumentov, ki jih želimo
posredovati izvajalcu signala. Parametri signala so priključeni na naslov URL trenutnega predstavnika/ogleda. Parameter
?do
v naslovu URL določa klicani signal.
Njegova oblika je {signal}
ali {signalReceiver}-{signal}
. {signalReceiver}
je ime
komponente v predstavniku. Zato pomišljaj (nenatančno pomišljaj) ne sme biti prisoten v imenu komponent – uporablja se za
razdelitev imena komponente in signala, vendar je mogoče sestaviti več komponent.
Metoda isSignalReceiver()
preveri, ali je komponenta (prvi argument) sprejemnik signala (drugi argument). Drugi argument lahko izpustite – takrat
ugotovi, ali je komponenta sprejemnik katerega koli signala. Če je drugi parameter true
, preveri, ali je komponenta
ali njeni potomci sprejemniki signala.
V kateri koli fazi pred handle{Signal}
se lahko signal izvede ročno s klicem metode processSignal(), ki
prevzame odgovornost za izvedbo signala. Vzame komponento prejemnika (če ni nastavljena, je to sam predvajalnik) in ji pošlje
signal.
Primer:
if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) {
$this->processSignal();
}
Signal se izvede predčasno in ne bo več poklican.