Elementy interaktywne

Komponenty to samodzielne obiekty wielokrotnego użytku, które wstawiamy do stron. Mogą to być formularze, datagridy, ankiety, w rzeczywistości wszystko, co ma sens, aby używać wielokrotnie. Zobaczmy:

  • jak używać komponentów?
  • jak je napisać?
  • co to są sygnały?

Nette posiada wbudowany system komponentów. Memordziści mogą znać coś podobnego z Delphi lub ASP.NET Web Forms, a React lub Vue.js jest zbudowany na czymś zdalnie podobnym. W świecie frameworków PHP jest to jednak ewenement.

Jednak komponenty w zasadniczy sposób wpływają na podejście do tworzenia aplikacji. W rzeczywistości można komponować strony z gotowych jednostek. Czy potrzebujesz datagridu w swojej administracji? Można go znaleźć na Componette, repozytorium open-source'owych dodatków (nie tylko komponentów) dla Nette, i po prostu wstawić do prezentera.

W prezenterze można zawrzeć dowolną liczbę komponentów. A do niektórych komponentów można wstawić inne komponenty. Tworzy to drzewo komponentów z prezenterem jako korzeniem.

Metody fabryczne

Jak komponenty są wstawiane do prezentera, a następnie wykorzystywane? Zazwyczaj z wykorzystaniem metod fabrycznych.

Fabryka komponentów jest eleganckim sposobem tworzenia komponentów tylko wtedy, gdy są one rzeczywiście potrzebne (leniwe / na żądanie). Cała magia tkwi w implementacji metody o nazwie createComponent<Name>()gdzie <Name> jest nazwą tworzonego komponentu, który tworzy i zwraca komponent.

class DefaultPresenter extends Nette\Application\UI\Presenter
{
	protected function createComponentPoll(): PollControl
	{
		$poll = new PollControl;
		$poll->items = $this->item;
		return $poll;
	}
}

Ponieważ wszystkie komponenty są tworzone w osobnych metodach, kod zyskuje na przejrzystości.

Nazwy komponentów zawsze zaczynają się od małej litery, nawet jeśli w nazwie metody są pisane wielką literą.

Nigdy nie wywołujemy fabryk bezpośrednio, dzwonią one do siebie przy pierwszym użyciu komponentu. Dzięki temu komponent jest tworzony w odpowiednim czasie i tylko wtedy, gdy jest rzeczywiście potrzebny. Jeśli nie korzystamy z komponentu (np. podczas żądania AJAX, gdy przekazywana jest tylko część strony, lub podczas buforowania szablonu), nie jest on w ogóle tworzony i oszczędzamy wydajność serwera.

// przechodzimy do komponentu i jeśli był to pierwszy raz,
// wywołaj funkcję createComponentPoll(), aby go stworzyć
$poll = $this->getComponent('poll');
// alternatywna składnia: $poll = $this['poll'];

Możliwe jest renderowanie komponentu w szablonie za pomocą znacznika {control}, dlatego nie ma potrzeby ręcznego przekazywania komponentów do szablonu.

<h2>Please Vote</h2>

{control poll}

Styl hollywoodzki

Komponenty powszechnie wykorzystują jedną świeżą technikę, którą lubimy nazywać stylem hollywoodzkim. Na pewno znacie skrzydlate zdanie, które tak często słyszą osoby biorące udział w przesłuchaniach do filmów: “Nie dzwoń do nas, my zadzwonimy do ciebie”. I właśnie o to chodzi.

Bo w Nette, zamiast konieczności ciągłego zadawania pytań (“czy formularz został przesłany?”, “czy był ważny?” lub “czy użytkownik nacisnął ten przycisk?”), mówisz frameworkowi “kiedy to się stanie, wywołaj tę metodę” i pozostawiasz mu resztę pracy. Jeśli programujesz w JavaScript, jesteś zaznajomiony z tym stylem programowania. Piszesz funkcje, które są wywoływane w momencie wystąpienia zdarzenia. A język przekazuje im odpowiednie parametry.

To całkowicie zmienia sposób myślenia o pisaniu aplikacji. Im więcej zadań możesz zostawić ramom, tym mniej pracy musisz wykonać. I tym mniej można pominąć, np.

Pisanie komponentu

Przez komponent zwykle rozumiemy potomka klasy Nette\Application\UI\Control. (Dokładniej byłoby użyć terminu “kontrole”, ale “kontrole” mają zupełnie inne znaczenie w języku angielskim i “komponenty” przejęły je). Sam prezenter Nette\Application\UI\Presenter zresztą też jest potomkiem klasy Control.

use Nette\Application\UI\Control;

class PollControl extends Control
{
}

Rendering

Wiemy już, że znacznik {control componentName} służy do renderowania komponentu. W rzeczywistości wywołuje to metodę render() komponentu, w której wykonamy renderowanie. Mamy, podobnie jak w Presenterze, szablon Latte w zmiennej $this->template, do którego przekazujemy parametry. W przeciwieństwie do prezentera, musimy podać plik szablonu i zlecić jego renderowanie:

public function render(): void
{
	// wstawiamy kilka parametrów do szablonu
	$this->template->param = $value;
	// i renderować go
	$this->template->render(__DIR__ . '/poll.latte');
}

Znacznik {control} umożliwia przekazanie parametrów do metody render():

{control poll $id, $message}
public function render(int $id, string $message): void
{
	// ...
}

Czasami komponent może składać się z kilku części, które chcemy renderować osobno. Dla każdego z nich tworzymy własną metodę renderowania, tutaj w przykładzie dla przykładu renderPaginator():

public function renderPaginator(): void
{
	// ...
}

A następnie wywołaj go w szablonie za pomocą:

{control poll:paginator}

Dla lepszego zrozumienia dobrze jest wiedzieć, jak przetłumaczyć ten tag na język PHP.

{control poll}
{control poll:paginator 123, 'hello'}

tłumaczy się jako:

$control->getComponent('poll')->render();
$control->getComponent('poll')->renderPaginator(123, 'hello');

Metoda getComponent() zwraca komponent poll i wywołuje metodę render(), lub renderPaginator() nad tym komponentem, jeśli w znaczniku po dwukropku określono inną metodę renderowania.

Zauważ, że jeśli => pojawia się gdziekolwiek w parametrach, wszystkie parametry zostaną zawinięte w tablicę i przekazane jako pierwszy argument:

{control poll, id: 123, message: 'hello'}

zostanie przetłumaczone jako:

$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']);

Renderowanie podkomponentu:

{control cartControl-someForm}

tłumaczy się jako:

$control->getComponent("cartControl-someForm")->render();

Komponenty, podobnie jak prezentery, przekazują automatycznie kilka przydatnych zmiennych do szablonów:

  • $basePath to bezwzględna ścieżka URL do katalogu głównego (np. /eshop)
  • $baseUrl to bezwzględny adres URL do katalogu głównego (np. http://localhost/eshop)
  • $user jest obiektem reprezentującym użytkownika
  • $presenter jest obecnym prezenterem
  • $control to aktualny składnik
  • $flashes jest tablicą komunikatów wysyłanych przez funkcję flashMessage()

Sygnał

Wiemy już, że nawigacja w aplikacji Nette polega na łączeniu lub przekierowaniu do par Presenter:action. Ale co jeśli chcemy wykonać akcję tylko na bieżącej stronie? Na przykład zmienić kolejność kolumn w tabeli; usunąć element; przełączyć tryb jasny/ciemny; przesłać formularz; głosować w ankiecie; itp.

Tego typu żądania nazywane są sygnałami. I tak jak akcje, wywołują one metody action<Action>() lub render<Action>(), sygnały wywołują metody handle<Signal>(). O ile pojęcie akcji (lub widoku) dotyczy wyłącznie prezenterów, o tyle sygnały odnoszą się do wszystkich komponentów. A zatem także prezenterów, gdyż UI\Presenter jest potomkiem UI\Control.

public function handleClick(int $x, int $y): void
{
	// ... przetwarzanie sygnału ...
}

Link wywołujący sygnał tworzymy w zwykły sposób, czyli w szablonie za pomocą atrybutu n:href lub znacznika {link}, a w kodzie za pomocą metody link() Więcej informacji znajdziesz w rozdziale Tworzenie linków URL.

<a n:href="click! $x, $y">click here</a>

Sygnał jest zawsze wywoływany na bieżącym prezenterze i widoku, więc nie można go wywołać na innym prezenterze lub widoku.

Tak więc sygnał powoduje przeładowanie strony dokładnie w taki sam sposób, jak oryginalne żądanie, ale wywołuje metodę obsługi sygnału z odpowiednimi parametrami. Jeśli metoda nie istnieje, rzucany jest wyjątek Nette\Application\UI\BadSignalException, który jest wyświetlany użytkownikowi jako strona błędu 403 Forbidden.

Snippety i AJAX

Snippety mogą przypominać nieco AJAX: handlery, które są wywoływane na bieżącej stronie. I masz rację, sygnały są rzeczywiście często wywoływane za pomocą AJAX-a, a następnie tylko zmienione części strony są przekazywane do przeglądarki. Albo tzw. snippety. Więcej informacji można znaleźć na stronie poświęconej AJAX-owi.

Wiadomości błyskowe

Komponent posiada własne, niezależne od prezentera repozytorium wiadomości flash. Są to komunikaty, które np. informują o wyniku operacji. Ważną cechą wiadomości flash jest to, że są one dostępne w szablonie nawet po przekierowaniu. Nawet po ich wyświetleniu pozostaną na żywo przez kolejne 30 sekund – na przykład w przypadku, gdy użytkownik odświeży stronę z powodu błędu transmisji – więc komunikat nie zniknie natychmiast.

Wysyłanie jest obsługiwane przez metodę flashMessage. Pierwszym parametrem jest tekst wiadomości lub obiekt stdClass reprezentujący wiadomość. Opcjonalnym drugim parametrem jest jego typ (błąd, ostrzeżenie, info, itd.). Metoda flashMessage() zwraca instancję wiadomości flash jako obiekt stdClass, do którego można dodać dodatkowe informacje.

$this->flashMessage('Item has been deleted.');
$this->redirect(/* ... */); // i przekierować

Do szablonu wiadomości te są dostępne w zmiennej $flashes jako obiekty stdClass, które zawierają właściwości message (tekst wiadomości), type (typ wiadomości) i mogą zawierać wspomniane już informacje o użytkowniku. Na przykład wyrenderujmy je w następujący sposób:

{foreach $flashes as $flash}
	<div class="flash {$flash->type}">{$flash->message}</div>
{/foreach}

Trwałe parametry

Trwałe parametry są używane do utrzymania stanu w komponentach pomiędzy różnymi żądaniami. Ich wartość pozostaje taka sama nawet po kliknięciu linku. W przeciwieństwie do danych sesji, są one przekazywane w adresie URL. I są przekazywane automatycznie, łącznie z linkami utworzonymi w innych komponentach na tej samej stronie.

Na przykład, masz komponent stronicowania treści. Na stronie może być kilka takich komponentów. I chcesz, aby wszystkie komponenty pozostały na bieżącej stronie, gdy użytkownik kliknie na link. Dlatego czynimy numer strony (page) trwałym parametrem.

Tworzenie trwałych parametrów jest w Nette niezwykle proste. Wystarczy stworzyć właściwość publiczną i oznaczyć ją atrybutem: (poprzednio użyto /** @persistent */ )

use Nette\Application\Attributes\Persistent; // ta linia jest ważna

class PaginatingControl extends Control
{
	#[Persistent]
	public int $page = 1; // musi być publiczny
}

Zalecamy dołączenie typu danych (np. int) do właściwości, możesz także dołączyć wartość domyślną. Wartości parametrów mogą być walidowane.

Możesz zmienić wartość trwałego parametru podczas tworzenia linku:

<a n:href="this page: $page + 1">next</a>

Można też go resetować, czyli usunąć z adresu URL. Przyjmie on wtedy swoją domyślną wartość:

<a n:href="this page: null">reset</a>

Trwałe komponenty

Nie tylko parametry, ale także komponenty mogą być trwałe. Dla takiego komponentu, jego trwałe parametry są również przekazywane pomiędzy różnymi akcjami prezentera lub pomiędzy wieloma prezenterami. Oznaczamy trwałe komponenty poprzez adnotację klasy prezentera. Na przykład w ten sposób oznaczamy składniki calendar i poll:

/**
 * @persistent(calendar, poll)
 */
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}

Podkomponenty wewnątrz tych komponentów nie muszą być oznaczone, stają się również trwałe.

W PHP 8 możesz również używać atrybutów do oznaczania trwałych komponentów:

use Nette\Application\Attributes\Persistent;

#[Persistent('calendar', 'poll')]
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}

Komponenty z zależnościami

Jak tworzyć komponenty z zależnościami, nie “brudząc” prezenterów, które będą z nich korzystać? Dzięki sprytnym funkcjom kontenera DI w Nette, podobnie jak w przypadku korzystania z tradycyjnych usług, możemy większość pracy pozostawić frameworkowi.

Weźmy jako przykład komponent, który ma zależność od serwisu PollFacade:

class PollControl extends Control
{
	public function __construct(
		private int $id, // Id ankiety, za pomocą której można tworzyć komponenty
		private PollFacade $facade,
	) {
	}

	public function handleVote(int $voteId): void
	{
		$this->facade->vote($id, $voteId);
		// ...
	}
}

Gdybyśmy pisali klasyczny serwis, nie byłoby się czym przejmować. Kontener DI w niewidoczny sposób zająłby się przekazaniem wszystkich zależności. Ale zazwyczaj obsługujemy komponenty tworząc ich nową instancję bezpośrednio w prezenterze w metodach fabrycznych createComponent…(). Ale przekazanie wszystkich zależności wszystkich komponentów do prezentera, aby następnie przekazać je do komponentów, jest uciążliwe. A ilość napisanego kodu…

Logicznym pytaniem jest, dlaczego po prostu nie zarejestrujemy komponentu jako klasycznej usługi, przekażemy go do prezentera, a następnie zwrócimy go w metodzie createComponent…()? Ale to podejście jest nieodpowiednie, ponieważ chcemy mieć możliwość wielokrotnego tworzenia komponentu.

Poprawnym rozwiązaniem jest napisanie fabryki komponentu, czyli klasy, która tworzy za nas komponent:

class PollControlFactory
{
	public function __construct(
		private PollFacade $facade,
	) {
	}

	public function create(int $id): PollControl
	{
		return new PollControl($id, $this->facade);
	}
}

W ten sposób rejestrujemy fabrykę w naszym kontenerze w konfiguracji:

services:
	- PollControlFactory

i w końcu użyć go w naszym prezenterze:

class PollPresenter extends Nette\UI\Application\Presenter
{
	public function __construct(
		private PollControlFactory $pollControlFactory,
	) {
	}

	protected function createComponentPollControl(): PollControl
	{
		$pollId = 1; // możemy przekazać nasz parametr
		return $this->pollControlFactory->create($pollId);
	}
}

Świetną rzeczą jest to, że Nette DI potrafi generować takie proste fabryki, więc zamiast pisać cały kod, wystarczy napisać jego interfejs:

interface PollControlFactory
{
	public function create(int $id): PollControl;
}

I to jest właśnie to. Nette wewnętrznie implementuje ten interfejs i przekazuje go do prezentera, gdzie możemy go użyć. W magiczny sposób dodaje również do naszego komponentu parametr $id oraz instancję klasy PollFacade.

Komponenty w głębi

Komponenty w aplikacji Nette to części aplikacji internetowej wielokrotnego użytku, które osadzamy w stronach, co jest tematem tego rozdziału. Jakie dokładnie są możliwości takiego komponentu?

  1. Jest renderowalny w szablonie
  2. wie , którą część siebie renderować podczas żądania AJAX (snippets)
  3. ma możliwość przechowywania swojego stanu w URL (trwałe parametry)
  4. posiada zdolność do reagowania na działania (sygnały) użytkownika
  5. tworzy strukturę hierarchiczną (gdzie korzeniem jest prezenter)

Każda z tych funkcji jest obsługiwana przez jedną z klas linii dziedziczenia. Renderingiem (1 + 2) zajmuje się Nette\Application\UI\Control, włączaniem do cyklu życia (3, 4) klasa Nette\Application\UI\Component, a tworzeniem struktury hierarchicznej (5) klasy Container i 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 }

Cykl życia komponentów

Cykl życia składników

Walidacja stałych parametrów

Wartości trwałych parametrów otrzymanych z adresów URL są zapisywane do właściwości przez metodę loadState(). Sprawdza ona również, czy typ danych określony dla właściwości pasuje, w przeciwnym razie odpowie błędem 404 i strona nie zostanie wyświetlona.

Nigdy ślepo nie ufaj trwałym parametrom, ponieważ mogą one zostać łatwo nadpisane przez użytkownika w adresie URL. Na przykład, w ten sposób sprawdzamy, czy numer strony $this->page jest większy niż 0. Dobrym sposobem na to jest nadpisanie metody loadState() wspomnianej powyżej:

class PaginatingControl extends Control
{
	#[Persistent]
	public int $page = 1;

	public function loadState(array $params): void
	{
		parent::loadState($params); // tutaj jest ustawione $this->page
		// następuje sprawdzenie wartości użytkownika:
		if ($this->page < 1) {
			$this->error();
		}
	}
}

Procesem przeciwnym, czyli pobieraniem wartości z persistent properites, zajmuje się metoda saveState().

Sygnały w głąb

Sygnał ten powoduje przeładowanie strony dokładnie tak samo jak oryginalne żądanie (z wyjątkiem wywołania przez AJAX) i wywołuje metodę signalReceived($signal), której domyślna implementacja w klasie Nette\Application\UI\Component próbuje wywołać metodę składającą się ze słów handle{signal}. Dalsze przetwarzanie należy do obiektu. Obiekty dziedziczące po Component (czyli Control i Presenter) odpowiadają próbując wywołać metodę handle{signal} z odpowiednimi parametrami.

Innymi słowy, bierze definicję funkcji handle{signal} i wszystkie parametry, które przyszły z żądaniem, dołącza parametry z adresu URL do argumentów po nazwie i próbuje wywołać metodę. Na przykład wartość z parametru id w adresie URL jest przekazywana jako źródło $id, something z adresu URL jest przekazywany jako $something itd. A jeśli metoda nie istnieje, metoda signalReceived podnosi wyjątek.

Sygnał może odebrać każdy komponent, prezenter lub obiekt, który implementuje interfejs SignalReceiver i jest podłączony do drzewa komponentów.

Głównymi odbiorcami sygnałów będą Presentery oraz komponenty wizualne dziedziczące po Control. Sygnał ma służyć jako sygnał dla obiektu, że powinien coś zrobić – ankieta powinna zliczyć głos od użytkownika, blok wiadomości powinien się rozwinąć i wyświetlić dwa razy więcej wiadomości, formularz został przesłany i powinien przetworzyć dane, i tak dalej.

Tworzymy adres URL dla sygnału za pomocą metody Component::link(). Przekazujemy ciąg {signal}! jako parametr $destination oraz tablicę argumentów, które chcemy przekazać do sygnału jako $args. Sygnał jest zawsze wywoływany na bieżącym widoku z bieżącymi parametrami, parametry sygnału są po prostu dodawane. Dodatkowo na samym początku dodawany jest parametr ?do, który określa sygnał.

Jego format to albo {signal}, albo {signalReceiver}-{signal}. {signalReceiver} to nazwa komponentu w prezenterze. Dlatego w nazwie komponentu nie może być myślnika – służy on do oddzielenia nazwy komponentu od sygnału, ale możliwe jest osadzenie w ten sposób kilku komponentów.

Metoda isSignalReceiver() sprawdza, czy komponent (pierwszy argument) jest odbiornikiem sygnału (drugi argument). Drugi argument może być pominięty – wtedy sprawdza, czy dany komponent jest odbiornikiem jakiegokolwiek sygnału. Drugi parametr może być true, aby sprawdzić, czy nie tylko określony komponent jest odbiornikiem, ale także dowolny z jego potomków.

Na dowolnym etapie przed handle{signal} możemy wykonać sygnał ręcznie, wywołując metodę processSignal(), która zajmuje się obsługą sygnału – bierze komponent, który określił się jako odbiorca sygnału (jeśli nie określono odbiorcy sygnału, jest nim sam prezenter) i wysyła mu sygnał.

Przykład:

if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) {
	$this->processSignal();
}

Oznacza to, że sygnał jest przedwcześnie wykonany i nie zostanie ponownie wywołany.

wersja: 4.0