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 primer http://localhost/CD-collection)
  • $user je objekt, ki predstavlja uporabnika
  • $presenter je trenutni predstavnik
  • $control je trenutna komponenta
  • $flashes seznam sporočil, poslanih z metodo flashMessage()

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?

  1. mogoče jo je izrisati v predlogi
  2. ve, kateri del sebe naj prikaže med zahtevo AJAX (fragmenti).
  3. ima možnost shranjevanja svojega stanja v naslovu URL (trajni parametri).
  4. ima sposobnost odzivanja na dejanja uporabnika (signali)
  5. 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.

različica: 4.0