Zanim przejdziemy do bardziej zaawansowanych projektów, chciałbym przybliżyć dwie istotne kwestie. Jedną z nich są podstawowe hooki Reacta, których możemy nie znać, jeżeli skupiamy się tylko na WordPressie. Druga to przynajmniej podstawowe obsługiwanie atrybutu viewScript i pliku view.js służącego do frontendowego JS dla naszego bloku.

Blok zegar – tworzymy plugin

W folderze plugins, wewnątrz wp-contnet w naszej instalacji WordPressa używamy następującej komendy:

npx @wordpress/create-block@latest clock-block --variant=dynamic

Gdy wszystko się zainstaluje poprawnie, przechodzimy do pliku z pluginem przy pomocy komendy cd (change directory):

cd clock-block

Teraz przechodzimy do pliku edit.js:

export default function Edit() {
	let time = new Date();
	return (
		<p { ...useBlockProps() }>
			{time.getHours()}:{time.getMinutes()}:{time.getSeconds()}
		</p>
	);
}

Taki kod powinien nam z grubsza wyświetlać czas. Teraz postarajmy się osiągnąć to samo w pliku render.php:

<?php
/**
 * @see https://github.com/WordPress/gutenberg/blob/trunk/docs/reference-guides/block-api/block-metadata.md#render
 */
?>
<p <?php echo get_block_wrapper_attributes(); ?>>
	<?php echo date("H:m:s");?>
</p>

Teraz możemy wykonać komendę:

npm run build

Zegar w edytorze – useState, useEffect

O hookach useState i useEffect możemy sporo naczytać się w tutorialu Reacta, który jest dostępny na tej stronie. Jeżeli jednak nie chcemy się w to bawić, możemy spróbować zrozumieć to na poniższym przykładzie.

Po pierwsze, w edit.js zaimportujmy te dwa hooki:

import { useState, useEffect } from "react";

Hook useState zwraca dwie rzeczy – stan i funkcję do ustawiania nowego stanu. Przyjmuje zaś, jako argument, stan początkowy. Oto poprawne użycie useState, które nie popsuje naszej aplikacji:

export default function Edit() {
	let [time, setTime] = useState(new Date());
	
	return (
		<p { ...useBlockProps() }>
			{time.getHours()}:{time.getMinutes()}:{time.getSeconds()}
		</p>
	);
}

Mamy tutaj tak samo jak wcześniej zmienną time, zawierającą aktualny czas, ale mamy też funkcję setTime, dzięki której możemy ten czas aktualizować. Na przykład – co sekundę.

Możemy to robić hookiem useEffect:

export default function Edit() {
	let [time, setTime] = useState(new Date());
	
	useEffect(()=> {
		 let interval = setInterval(() => {
			setTime(new Date());
		}, 1000);
		return () => clearInterval(interval);
	}, []);

	return (
		<p { ...useBlockProps() }>
			{time.getHours()}:{time.getMinutes()}:{time.getSeconds()}
		</p>
	);
}

Więcej o tym hooku napisałem w poradniku Reacta, epizodzie pierwszym. Tam można znaleźć obszerny opis jak działa hook useEffect w różnych konfiguracjach.

Teraz tylko pokrótce nadmienię – ustawiamy interwał na zmianę czasu na nowy co 1 sekundę, zwracamy funkcję czyszczącą (która w razie odmountowania naszego komponentu wyczyści niepotrzebny interwał – to tylko dobra praktyka), pusta tablica zależności – wykonaj efekt (podpięcie interwału) jeden raz, przy pierwszym pojawieniu się komponentu.

Możemy teraz wykonać komendę npm run build i zobaczyć w edytorze, że nasz zegar – przynajmniej po stronie edytora – działa. Co sekundę się aktualizuje.

Zegar na frontendzie – view.js

Wydawać by się mogło, że blok dynamiczny (z render.php zamiast save.js) to złe rozwiązanie – mamy przecież działający JS w edit.js, wystarczy tylko przekopiować do save.js, prawda?

Nie do końca – w save.js nie możemy tak się bawić hookami. Save.js służy do tego, aby coś wyrenderować, ewentualnie odczytując wcześniej z zapisanych atrybutów, których w tym pluginie nie używamy.

Render.php to z kolei PHP. Funkcja date() pomogła nam z pierwszym, pierwotnym czasem (który jest pobierany z serwera tak w ogóle i nie musi odpowiadać czasowi po stronie klienta) dalej jednak musimy sobie radzić sami.

W block.json mamy taką zagadkową linijkę:

"render": "file:./render.php",
"viewScript": "file:./view.js"

Jeżeli zobaczymy, co się w tym pliku znajduje, ujrzymy taką oto linijkę:

console.log( 'Hello World! (from create-block-clock-block block)' );

Kiedy odwiedzamy post z takim blokiem, ta linijka się wykonuje. Mało tego – możemy tam mieć i 5 i 50 takich zegarowych bloków, zaś ta linijka wykona się jeden raz.

Jeżeli znamy nieco JavaScript, to już powinniśmy być na dobrym tropie jak to wykorzystać. Musimy tylko zobaczyć, jaką klasę ma nasz blok. W moim przypadku to:

wp-block-create-block-clock-block

A zatem tak – łapiemy wszystkie takie bloki w pliku view.js i ustawiamy im interwał:

console.log( 'Hello World! (from create-block-clock-block block)' );
window.addEventListener('DOMContentLoaded', function(){
 let blocks = document.querySelectorAll('.wp-block-create-block-clock-block');
 blocks.forEach((block) => {
       setInterval(() => {
            let time = new Date();
            block.textContent = `${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}`;
       }, 1000);
 });
});

Jest to najłatwiejszy sposób, za pomocą którego jesteśmy w stanie taki blok zegarowy stworzyć.

Zegar na frontendzie – skrypt z has_block

Istnieje inny sposób na dołączenie naszego skryptu z frontendem dla naszego bloku. Musimy wejść do pliku block.json w folderze src. Mamy tam coś takiego:

{
	"$schema": "https://schemas.wp.org/trunk/block.json",
	"apiVersion": 3,
	"name": "create-block/clock-block",
	"version": "0.1.0",
	"title": "Clock Block",
	"category": "widgets",
	"icon": "smiley",
	"description": "Example block scaffolded with Create Block tool.",
	"example": {},
	"supports": {
		"html": false
	},
	"textdomain": "clock-block",
	"editorScript": "file:./index.js",
	"editorStyle": "file:./index.css",
	"style": "file:./style-index.css",
	"render": "file:./render.php",
	"viewScript": "file:./view.js"
}

Jak widać pozycja viewScript określa plik, w którym znajduje się nasz obecny frontend. Nas jednak interesuje „name” czyli nazwa bloku.

Teraz, w folderze wyżej, tam gdzie clock-block.php, tworzymy plik clock-present.js i wklejamy następującą treść:

console.log("clock present!");

To nas poinformuje, gdy już skrypt dołączymy, że wszystko działa jak należy.

Teraz w pliku clock-block.php dodajemy ten nasz skrypt:

function enqueue_if_clock_is_present(){
	if(is_singular()){

	   $id = get_the_ID();
	   if(has_block('create-block/clock-block', $id)){
		  wp_enqueue_script('clock-script',plugin_dir_url( __FILE__ ) . 'clock-present.js' );
	   }
	}
}

  add_action('wp_enqueue_scripts','enqueue_if_clock_is_present');

Na stronie pojedynczego posta, jaki by on nie był, sprawdzamy czy post posiada blok o nazwie 'create-block/clock-block’ (po to potrzebna nam była nazwa).

Jeżeli posiadamy taki blok na stronie pojedynczego posta, dodajemy skrypt 'clock-present.js’ z głównego folderu naszego pluginu.

Teraz po wejściu na posta z naszym zegarem powinniśmy otrzymać wiadomość w konsoli, że zegar jest obecny na stronie.

Czyli krótko mówiąc – skopiowaliśmy właściwość pliku view.js na swój własny sposób.

Jest to o tyle ciekawe, że nie bazujemy na gotowcach z create-block oraz wp-scripts tylko napisaliśmy coś własnego, co może nam się przydać w innych miejscach i sytuacjach, związanych z pisaniem pluginu, który niekoniecznie dodaje nowe bloki, ale sprawdza obecność już istniejących we wpisach i jakiś skrypt dodaje.

Teraz możemy wyciąć z view.js ten fragment i przekopiować do clock-block.js:

window.addEventListener('DOMContentLoaded', function(){
 let blocks = document.querySelectorAll('.wp-block-create-block-clock-block');
 blocks.forEach((block) => {
       setInterval(() => {
            let time = new Date();
            block.textContent = `${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}`;
       }, 1000);
 });
});

React na frontendzie – w pliku view.js (jeden blok)

Wiemy, że w pliku save.js/render.php nie możemy używać Reacta. Wiemy też, że mamy do dyspozycji plik view.js, który jest odpalany (jeden raz) gdy mamy na stronie dany blok (celem zapewnienia frontendu dla naszego bloczka).

Istnieje natomiast możliwość pełnego wykorzystania React w naszym bloku za pomocą view.js. Aż dziwne, że niewiele jest tutoriali, które to pokazują.

Zobaczmy, w jakim folderze jesteśmy. Jeżeli w plugins – dobrze. Jeżeli w folderze jakiegoś pluginu, przejdźmy sobie folder wyżej komendą cd (change directory):

cd ..

Teraz utwórzmy sobie prosty blok dynamiczny WordPressa komendą:

npx @wordpress/create-block@latest counter-try --variant=dynamic

Teraz za pomocą change directory przechodzimy do tamtego folderu:

cd counter-try

W pliku block.json, pod supports, dodajemy:

"supports": {
		"html": false,
		"multiple": false
	},

Dzięki temu można będzie tylko jeden taki blok dodać do strony. Potrzebne nam będzie jeszcze jego ID, które dodamy w render.php:

<?php
/**
 * @see https://github.com/WordPress/gutenberg/blob/trunk/docs/reference-guides/block-api/block-metadata.md#render
 */
?>
<div <?php echo get_block_wrapper_attributes(); ?> id="counter-block">
	<?php esc_html_e( 'Counter Try – hello from a dynamic block!', 'counter-try' ); ?>
</div>

Teraz w edit.js tworzymy nasz licznik, korzystając z hooków Reacta. Na początek konieczne importy:

import { useBlockProps } from '@wordpress/block-editor';
import { useState } from '@wordpress/element';
import {  Button } from '@wordpress/components';

Teraz nasza funkcja edit:

export default function Edit() {
	const [counter, setCounter] = useState(0);
	const handleClick1 = () => {
        setCounter(counter + 1);
    };
	return (
		<div { ...useBlockProps() }>
			<p>Counter: {counter}</p>
			<Button onClick={() => handleClick1()}>Add</Button>
		</div>
	);
}

Jeżeli nie bardzo ogarniamy hooki Reacta, polecam pierwszą część tutoriala z podstaw Reacta na tej stronie. Na szybko jednak to wytłumaczę:

  • useState przyjmuje domyślny stan początkowy – w naszym przypadku jest to 0 (choć przyjąć może też funkcję taki stan wyliczającą)
  • useState zwraca dwie rzeczy – stan (w naszym przypadku pod nazwą 'counter’) oraz funkcję do zmiany stanu (w naszym przypadku 'setCounter’)
  • Funkcja handleClick zwiększa stan 'counter’ o 1
  • Stan jest wyświetlany w elemencie <p> przy pomocy {counter} (każda zmiana stanu to de facto re-render komponentu)
  • Element <Button> poprzez atrybut „onClick” wywołuje funkcję, zwiększającą stan o 1

Krótko mówiąc – mamy nasz licznik (na edycji). Teraz tylko wykonajmy komendę:

npm run build

Włączmy nasz plugin i postarajmy się dodać nasz blok do wpisu. Będzie działać. Można dodać tylko 1 element tego typu (co ustaliliśmy w pliku block.json).

Ten licznik (w oknie edycji) nam działa jak należy. Teraz zapiszmy post.

Po zapisaniu (czyli nie w polu edycji, ale widoku pojedynczego posta) otrzymamy wiadomość:

<div class="wp-block-create-block-counter-try" id="counter-block">Counter Try – hello from a dynamic block!</div>

Pochodzi ona z render.php:

<div <?php echo get_block_wrapper_attributes(); ?> id="counter-block">
	<?php esc_html_e( 'Counter Try – hello from a dynamic block!', 'counter-try' ); ?>
</div>

Wydawać by się mogło, że popełniliśmy błąd, tworząc blok dynamiczny, bo w render.php nie możemy korzystać z Reacta. Problem w tym, że w bloku statycznym w pliku save.js również nie możemy używać hooków Reacta ani innych tego typu rzeczy. Możemy tylko odczytywać i wyświetlać atrybuty (no, może nie tylko, ale o dynamicznym liczniku możemy zapomnieć).

Mamy to nasze id. Teraz przejdźmy do pliku view.js i zaimportujmy kilka rzeczy:

console.log( 'Hello World! (from create-block-counter-try block)' );
/* eslint-enable no-console */
import { useState, createRoot } from '@wordpress/element';
import {  Button } from '@wordpress/components';

Postaramy się teraz odtworzyć plik edit.js jako komponent (jeżeli nie rozumiemy, odsyłam do tutoriali z Reacta):

function CounterItem() {
	const [counter, setCounter] = useState(0);
	const handleClick1 = () => {
        setCounter(counter + 1);
    };
	return (
		<div>
			<p>Counter: {counter}</p>
			<Button onClick={() => handleClick1()}>Add</Button>
		</div>
	);
}

Teraz pora w odpowiednim miejscu ten komponent wyrenderować, korzystając z createRoot:

document.addEventListener('DOMContentLoaded', () => {
    const block = document.querySelector('#counter-block');
    const root = createRoot(block);
    root.render(<CounterItem/>);
});

Już tylko komenda:

npm run build

Teraz możemy zaobserwować nasz dynamiczny licznik zapisany wewnątrz posta. Nie ma już elementu:

<div class="wp-block-create-block-counter-try" id="counter-block">Counter Try – hello from a dynamic block!</div>

Zamiast tego jest element, który wygląda i działa tak samo interaktywnie jak nasz licznik w polu edycji.

Frontend Reactowy dla wielu bloków – przystosujmy nasz blok

Pierwsze, co musimy zrobić, to usunąć „multiple” ustawione na false z pliku block.json (względnie zamienić na true):

"supports": {
		"html": false,
		"multiple": true
	},

Pamiętajmy, że nasz blok na razie używa ID. ID powinno być unikalne dla elementu HTML. Jeżeli mamy więcej elementów tego typu (a teraz możemy mieć) to wypada to ID usunąć z pliku render.php:

<div <?php echo get_block_wrapper_attributes(); ?>>
	<?php esc_html_e( 'Counter Try – hello from a dynamic block!', 'counter-try' ); ?>
</div>

Nasz blok (w zapisanym poście), zanim podmienimy go na komponent Reacta, powinien wyglądać mniej więcej tak:

<div class="wp-block-create-block-counter-try">Counter Try – hello from a dynamic block!</div>

Nie ma już ID, ale nadal ma swoją klasę. To już coś. Kopiujemy ją i przechodzimy do naszego view.jsm który powinien wyglądać tak (minus komentarze u góry):

/* eslint-disable no-console */
console.log( 'Hello World! (from create-block-counter-try block)' );
/* eslint-enable no-console */
import { useState, createRoot } from '@wordpress/element';
import {  Button } from '@wordpress/components';
function CounterItem() {
	const [counter, setCounter] = useState(0);
	const handleClick1 = () => {
        setCounter(counter + 1);
    };
	return (
		<div>
			<p>Counter: {counter}</p>
			<Button onClick={() => handleClick1()}>Add</Button>
		</div>
	);
}

document.addEventListener('DOMContentLoaded', () => {
    const block = document.querySelector('#counter-block');
    const root = createRoot(block);
    root.render(<CounterItem/>);
});

Zarówno importy są ok jak i nasz komponent. Musimy jedynie dostosować sposób, w jaki podmieniamy nasz blok – w tym wypadku bloki – na komponent Reactowy.

Wykorzystajmy to, że znamy klasę naszego bloczka:

document.addEventListener('DOMContentLoaded', () => {
    const blocks = document.querySelectorAll('.wp-block-create-block-counter-try');
    blocks.forEach((block) => {
        const root = createRoot(block);
        root.render(<CounterItem/>);
    });
});

Teraz musimy tylko skompilować blok:

npm run build

Być może będziemy musieli odświeżyć ekran edycji posta, aby móc dodać więcej niż 1. Tak czy inaczej – dodajemy kilka liczników, zapisujemy posta.

Wchodzimy do zapisanego wpisu i widzimy nasze liczniki, dynamicznie działające przy użyciu Reacta.