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}
Przekierowanie po sygnale
Po przetworzeniu sygnału komponentu często następuje przekierowanie. Sytuacja ta jest podobna do formularzy – po przesłaniu formularza również przekierowujemy, aby zapobiec ponownemu przesłaniu danych po odświeżeniu strony w przeglądarce.
$this->redirect('this') // redirects to the current presenter and action
Ponieważ komponent jest elementem wielokrotnego użytku i zwykle nie powinien mieć bezpośredniej zależności od
konkretnych prezenterów, metody redirect()
i link()
automatycznie interpretują parametr jako sygnał
komponentu:
$this->redirect('click') // redirects to the 'click' signal of the same component
Jeśli konieczne jest przekierowanie do innego prezentera lub akcji, można to zrobić za pośrednictwem prezentera:
$this->getPresenter()->redirect('Product:show'); // redirects to a different presenter/action
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\Application\UI\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?
- Jest renderowalny w szablonie
- wie , którą część siebie renderować podczas żądania AJAX (snippets)
- ma możliwość przechowywania swojego stanu w URL (trwałe parametry)
- posiada zdolność do reagowania na działania (sygnały) użytkownika
- 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 properties, 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.