Nauczymy się dzisiaj korzystać z zewnętrznego API w naszych własnych blokach WordPressa. Ponadto nauczymy się tworzyć WordPressowe bloki, które z Reacta (i wszystkich jego możliwości) korzystają zarówno w edycji jak i po zapisaniu, poznamy sposób na ich stylowanie. Nauczymy się również korzystać z wewnętrznego API WordPressa.

Chuck Norris API – pierwsze kroki

Standardowo, przechodzimy do naszej instalacji WordPressa, w niej do folderu wp-content, następnie plugins. Tworzymy dynamiczny blok w tamtym folderze przy pomocy komendy:

npx @wordpress/create-block@latest external-api-block --variant=dynamic

Po wszystkim zmieniamy folder komendą change directory:

cd ./external-api-block

W edit.js dokonujemy niezbędnych importów:

import { useBlockProps } from '@wordpress/block-editor';
import apiFetch from '@wordpress/api-fetch';
import { useState, useEffect } from '@wordpress/element';

Skorzystamy z Chuck Norris API – darmowe API do testów, zwraca dowcipy o Chucku Norrisie. Użyjemy hook useEffect z pustą tablicą zależności, aby wykonać funkcję zaciągnięcia dowcipu jeden raz – przy dodaniu komponentu:

useEffect(() => {
		apirequiest();
	}, []);

Teraz musimy tylko tę asynchroniczną funkcję napisać:

const apirequiest = async () => {
		const res = await apiFetch({ url: 'https://api.chucknorris.io/jokes/random' });
		console.log(res);
	  };

Gdybyśmy korzystali z wewnętrznego API (/wp-json na naszej stronie) to zamiast argumentu 'url’ podalibyśmy 'path’ (z endpointem naszego API). Tutaj jednak korzystamy z API zewnętrznego. Odpowiedź logujemy do konsoli.

Teraz możemy w panelu admina włączyć plugin i wykonać komendę:

npm run build

Teraz próbując dodać nasz 'external-api-block’ w panelu edycji/dodawania wpisu, w konsoli zobaczymy (po pewnej chwili od dodania) taki mniej więcej komunikat (zakładając, że to API będzie nadal istniało):

{
    "categories": [],
    "created_at": "2020-01-05 13:42:25.352697",
    "icon_url": "https://assets.chucknorris.host/img/avatar/chuck-norris.png",
    "id": "Tzt0kDxgRlSrc8v3MPF64w",
    "updated_at": "2020-01-05 13:42:25.352697",
    "url": "https://api.chucknorris.io/jokes/Tzt0kDxgRlSrc8v3MPF64w",
    "value": "Your odds of surviving a Chuck Norris attack are slightly less than: -1/100."
}

Nasz dowcip jest pod „value”. Teraz możemy zaimplementować wyświetlanie go (w panelu edycyjnym):

export default function Edit() {
    const [joke, setJoke] = useState('');

	const apirequiest = async () => {
		const res = await apiFetch({ url: 'https://api.chucknorris.io/jokes/random' });
		setJoke(res.value);
	  };
	useEffect(() => {
		apirequiest();
	}, []);
	return (
		<p { ...useBlockProps() }>
			{joke}
		</p>
	);
}

Teraz (po kompilacji) nasz dowcip pojawiać się będzie wewnątrz tagu <p>.

API po stronie zapisanego posta – render.php

Nadal nie mamy możliwości zapisania naszego komponentu. To znaczy – zapisać możemy. Będzie on wyglądał mniej więcej tak:

<p class="wp-block-create-block-external-api-block">External Api Block – hello from a dynamic block!</p>

Wynika to prosto z naszego pliku render.php:

<p <?php echo get_block_wrapper_attributes(); ?>>
	<?php esc_html_e( 'External Api Block – hello from a dynamic block!', 'external-api-block' ); ?>
</p>

Postarajmy się zatem, aby nasz render.php również łączył się z zewnętrznym API, zaciągał losowy dowcip i go prezentował. Oto, jak to robimy:

<?php 
$api = wp_remote_get('https://api.chucknorris.io/jokes/random');
$res = json_decode($api['body']);
?>
<p <?php echo get_block_wrapper_attributes(); ?>>
	<?php echo $res->value; ?>
</p>

Oczywiście blok musimy skompilować:

npm run build

Teraz blok zapisany we wpisie będzie nam dawał losowy dowcip zaciągnięty z zewnętrznego API. Możemy odświeżyć kilka razy i wypróbować.

Query Parameters – klient i serwer

Poza głównym adresem, Chuck Norris API obsługuje tzw. query parameters. Mają na przykład parametr „category” pozwalający ustalić z jakiej kategorii chcemy dowcip. Na przykład poniższy adres to losowy dowcip z kategorii „film”:

https://api.chucknorris.io/jokes/random?category=movie

Oczywiście te query params nie są takie łatwe, jak się wydają. Na przykład = URL nie może mieć spacji. Jeżeli zatem kategoria miałaby dwa wyrazy co najmniej to już widzimy, że „doklejanie” treści po znaku „=” nie przejdzie. Te query params trzeba odpowiednio kodować.

Do robienia tego po stronie edycji dodajmy import do edit.js:

import { addQueryArgs } from '@wordpress/url';

Teraz możemy z tego skorzystać:

const queryParams = { category: 'movie' };
const apirequiest = async () => {
	const res = await apiFetch({url: addQueryArgs('https://api.chucknorris.io/jokes/random', queryParams) });
	setJoke(res.value);
};

Możemy w ten sposób zakodować wiele parametrów, nie martwiąc się jak zostaną rozdzielone, co ze spacjami i tak dalej. Rozwiązanie dużo lepsze od prymitywnego:

let cat = 'movie';
let url = `'https://api.chucknorris.io/jokes/random?category=${cat}`;

Uczmy się zatem od razu dobrych rozwiązań. Teraz możemy już blok skompilować i w widoku edycji zobaczymy dowcip tylko z kategorii „movie”.

Warto jeszcze w render.php wykonać analogiczną akcję, aby zakodować tam nasz parametr category:

<?php 
$url = "https://api.chucknorris.io/jokes/random";
$data = array('category' => 'movie');
$query_url = $url.'?'.http_build_query($data);
$api = wp_remote_get($query_url);
$res = json_decode($api['body']);
?>
<p <?php echo get_block_wrapper_attributes(); ?>>
	<?php echo $res->value; ?>
</p>

Teraz tylko kompilacja i już nasz blok działa – zaciąga tylko te dowcipy, które pochodzą z kategorii „movie”.

Tworzymy dynamiczny komponent – widok edycji

Na razie nasz blok nie jest przesadnie skomplikowany, więc da się go obsłużyć poprzez render.php. Problem pojawi się wtedy, gdy zechcemy bardziej dynamiczny blok. Bo ani render.php ani jego statyczny odpowiednik save.js nie może korzystać z hooków Reacta i innych funkcji wykraczających poza odczytywanie atrybutów i renderowanie treści.

Zróbmy sobie blok, który poza już istniejącym działaniem doda nam opcję wciśnięcia przycisku i zaciągnięcia kolejnego dowcipu w miejsce starego. Będziemy do tego potrzebowali w edit.js komponentu Button, który zaimportujemy obok innych importów:

import { Button } from '@wordpress/components';

Teraz tylko musimy dostosować nasz komponent do obsługi przycisku:

export default function Edit() {
    const [joke, setJoke] = useState('');
	const apirequiest = async () => {
		const res = await apiFetch({url: 'https://api.chucknorris.io/jokes/random'});
		setJoke(res.value);
	  };
	  
	useEffect(() => {
		apirequiest();
	}, []);

	function handleClick() {
		apirequiest();
	}
	return (
		<div { ...useBlockProps() }>
			<p>{joke}</p>
			<Button onClick={() => handleClick()}>Losuj kolejny</Button>
		</div>
	);
}

Pozbyliśmy się query params (niech losuje ze wszystkich kategorii), zamieniliśmy <p> na <div>, dodaliśmy <Button>, który obsługuje onClick. Możemy już sobie skompilować i zobaczyć, że działa.

Tutaj warto pochylić się nad stylowaniem. Wykomentujmy te linie w pliku editor.scss:

.wp-block-create-block-external-api-block {
	// background-color: #21759b;
	// color: #fff;
	padding: 2px;
}

Znajdźmy jakiś darmowy loader (na przykład ze strony CSS Loaders) i dodajmy go do style.scss:

/* HTML: <div class="loader"></div> */
.loader {
  width: 50px;
  padding: 8px;
  aspect-ratio: 1;
  border-radius: 50%;
  background: #25b09b;
  --_m: 
    conic-gradient(#0000 10%,#000),
    linear-gradient(#000 0 0) content-box;
  -webkit-mask: var(--_m);
          mask: var(--_m);
  -webkit-mask-composite: source-out;
          mask-composite: subtract;
  animation: l3 1s infinite linear;
}
@keyframes l3 {to{transform: rotate(1turn)}}

Teraz dodajmy ten loader, jeżeli dowcip jest pusty:

<p>{joke === "" ? <div className="loader"></div> : joke }</p>

Przy pierwszym ładowaniu zadziała, natomiast aby zadziałał przy drugim musimy jeszcze przed naszym apirequest (w handleClick) wyczyścić dowcip – to stan pustego napisu aktywuje loader:

function handleClick() {
	setJoke('');
	apirequiest();
}

Swoją drogą – w JSX mamy className zamias class… Warto o tym pamiętać. Ponadto, skoro już jesteśmy w SCSS zamiast zwykłego CSS, oto pewna sztuczka:

.wp-block-create-block-external-api-block {
	// background-color: #21759b;
	// color: #fff;
	border: 1px dotted #f00;
	padding: 2px; 
	
		.loader {
			width: 50px;
			padding: 8px;
			aspect-ratio: 1;
			border-radius: 50%;
			background: #25b09b;
			--_m: 
			  conic-gradient(#0000 10%,#000),
			  linear-gradient(#000 0 0) content-box;
			-webkit-mask: var(--_m);
					mask: var(--_m);
			-webkit-mask-composite: source-out;
					mask-composite: subtract;
			animation: l3 1s infinite linear;
		  }
		  @keyframes l3 {to{transform: rotate(1turn)}}
	
	
}

W SCSS możemy zagnieżdżać selektory w sobie.

Tworzymy dynamiczny komponent – plik view.js

Mamy już nasz komponent po stronie edycji. Teraz pora na dopisanie w view.js odpowiedniego kodu. Najpierw jednak oczyśćmy redner.php – nie będziemy go specjalnie potrzebować:

<div <?php echo get_block_wrapper_attributes(); ?>>
	<p>Ładowanie treści</p>
</div>

Nasz komponent będzie obsługiwany przez plik view.js, zaś jego style – przez editor.scss (podobnie jak edit.js). Problem w tym, że musimy jeszcze dodać atrybut editorStyle w pliku block.json:

"viewScript": "file:./view.js",
"viewStyle": [ "file:./view.css", "example-view-style" ]

Do view.css (w folderze build) kompilowany jest editor.scss.

Teraz w pliku view.js potrzebujemy dokonać następujących importów (zwróćmy uwagę na createRoot, którego w edit.js nie używaliśmy):

import { useBlockProps } from '@wordpress/block-editor';
import apiFetch from '@wordpress/api-fetch';
import { useState, useEffect, createRoot } from '@wordpress/element';
import { Button } from '@wordpress/components';
import './editor.scss';

Teraz tworzymy funkcję JokesAPIComponent i kopiujemy do niej zawartość funkcji Edit z edit.js (działać ma tak samo, jak w edycji):

function JokeAPIComponent(){
    const [joke, setJoke] = useState('');
	const apirequiest = async () => {
		const res = await apiFetch({url: 'https://api.chucknorris.io/jokes/random'});
		setJoke(res.value);
	  };
	  
	useEffect(() => {
		apirequiest();
	}, []);

	function handleClick() {
		setJoke('');
		apirequiest();
	}
	return (
		<div { ...useBlockProps() }>
			<p>{joke === "" ? <div className="loader"></div> : joke }</p>
			<Button onClick={() => handleClick()}>Losuj kolejny</Button>
		</div>
	);
}

To jeszcze nie wszystko – musimy teraz „złapać” nasze elementy (najlepiej po klasie) i podmienić je na Reactowy komponent, bo na razie to mamy tam zawartość z pliku render.php.

Ta zawartość ma swoją klasę, którą możemy podejrzeć w devtoolsach. Ta klasa to 'wp-block-create-block-external-api-block’ (chyba że inaczej swój blok nazwaliśmy – wtedy musimy zapisać nasz blok w jakimś poście, podejrzeć go i sprawdzić klasę w devtoolsach).

Łapiemy i podmieniamy elementy o klasie 'wp-block-create-block-external-api-block’ na Reactowe komponenty:

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

Teraz możemy zapisać i kompilować. Blok zapisany działa tak samo, jak na edycji. Może nie wydawać się to szczególnie trudne, zwłaszcza jeżeli dopiero co uczymy się tego zagadnienia, ale powiem tak – jak sam się tego uczyłem, włosy z głowy rwałem.

Nigdzie nie jest opisane w dobry i sensowny sposób jak można używać Reacta po stronie zapisanego bloku (bo w edycji to proste, zwykły React, zero utrudnień). A gdy już człowiek się przez to przebije, zaczyna się zastanawiać jak dodać do tego style.

I najważniejsze – NIGDZIE nie jest w ogóle napisane, że się da. Człowiek zatem może się poddać, porzucić pomysł, zakładając, że skoro na StackOverflow odpowiedzi nie ma, to i pytania nie ma ani takiego zagadnienia.

A na WordPressie stoi 80% internetu. Także, jeżeli wykonujemy te tutoriale, to mamy swego rodzaju mega-dopalacz. Bo ja się przez te tematy przebijałem głową o ścianę i włosy z głowy rwałem, swego czasu.

Korzystamy z wewnętrznego API WordPressa

Na początku w folderze plugins tworzymy kolejny plugin blokowy poniższą komendą:

npx @wordpress/create-block@latest internal-api-block --variant=dynamic

Teraz przechodzimy do tego folderu komendą change directory:

cd ./internal-api-block

Korzystanie z wewnętrznego API niewiele różni się od tego, co już robiliśmy. Jeżeli już, jest jeszcze łatwiejsze i szybsze. Na początek przejdźmy do edit.js i zaimportujmy kilka rzeczy:

import { useBlockProps } from '@wordpress/block-editor';
import apiFetch from '@wordpress/api-fetch';
import { addQueryArgs } from '@wordpress/url';
import { useState, useEffect } from '@wordpress/element';

Przygotujmy sobie nasze query params:

const queryParams = {per_page: 2, orderby: 'date', order: 'desc'};

Te query params razem ze ścieżką '/wp/v2/posts’ powinny nas odesłać na stronę:

http://localhost/blocktry2/wp-json/wp/v2/posts?per_page=2&orderby=date&order=desc&_locale=user

Teraz nasz useEffect z pustą tablicą zależności (do wykonania jeden raz):

useEffect(() => {
		apirequest();
	}, []);

Wewnątrz apirequest umieszczamy apiFetch, ale zamiast 'url’ używamy 'path’ zaś zamiast ścieżki bezwzględnej, ścieżkę względną (część po /wp-json/):

const queryParams = {per_page: 2, orderby: 'date', order: 'desc'};
const apirequest = async () => {
	const res = await apiFetch({ path: addQueryArgs('/wp/v2/posts', queryParams) });
	console.log(res);
}
useEffect(() => {
	apirequest();
}, []);

Teraz możemy kompilować, włączać plugin i podejrzeć, co po dodaniu wypluwa nam konsola:

[
    {
        "id": 27,
        "date": "2024-05-19T11:56:07",
        "date_gmt": "2024-05-19T11:56:07",
        "guid": {
            "rendered": "http://localhost/blocktry2/?p=27"
        },
        "modified": "2024-05-21T12:57:23",
        "modified_gmt": "2024-05-21T12:57:23",
        "slug": "sadsad",
        "status": "publish",
        "type": "post",
        "link": "http://localhost/blocktry2/2024/05/19/sadsad/",
        "title": {
            "rendered": "sadsad"
        },
        (...)
    },
    {
        "id": 24,
        "date": "2024-05-19T08:34:31",
        (...)
    }
]

Dużo tego. A to i tak tylko dwa posty. Załóżmy, że chcemy tylko tytuły, ale i chcemy je wyświetlać. Oto, jak zmieniamy edit.js:

export default function Edit() {
	const [titles, setTitles] = useState([]);
	const queryParams = {per_page: 2, orderby: 'date', order: 'desc'};
	const apirequest = async () => {
		const res = await apiFetch({ path: addQueryArgs('/wp/v2/posts', queryParams) });
		const postTitles = await res.map(post=> {
			 	  return { title: post.title.rendered};
			});
		setTitles(postTitles);
	}
	useEffect(() => {
		apirequest();
	}, []);
	return (
		<ul { ...useBlockProps() }>
			{titles.length ? titles.map((item) => <li>{item.title}</li>) : null}
		</ul>
	);
}

Teraz w render.php wystarczy napisać kod odpowiadający naszemu query. Zobaczmy, jak różni się WP Query od query do wp-json, bo różnice są i potrafią być denerwujące.

WP Query:

$args = array(
  'post_type' => 'post',
  'posts_per_page' => '2',
  'orderby' => 'date',
  'order' => 'DESC'
);

Query params do WP Rest API:

const queryParams = {per_page: 2, orderby: 'date', order: 'desc'};

Pamiętam, że kiedyś przez to 'DESC’, które w jednym miejscu jest pisane wielką a w innym małą literą, sporo czasu zmarnowałem szukając co i dlaczego mi nie działa i nie rozumiejąc nic z tego.

Dobra, teraz piszemy nasz render.php:

<?php
$args = array(
  'post_type' => 'post',
  'posts_per_page' => '2',
  'orderby' => 'date',
  'order' => 'DESC'
);

$pages = new WP_Query( $args );

if ( $pages->have_posts() ) {

  $page_list = '<ul>';

  while ( $pages->have_posts() ) {
    $pages->the_post();
    $page_list .= '<li><a href="'. get_the_permalink() . '">' . get_the_title() . '</a></li>';
  }
  
  $page_list .= '</ul>';

} else {
  $page_list = 'No pages to display.';
}

wp_reset_postdata();
?>
<div <?php echo get_block_wrapper_attributes(); ?>>
	<?php echo $page_list; ?>
</div>

Tutaj nawet dodaliśmy linki, czego w poprzednim przykładzie (widok edycji) nie chciało mi się robić – zwłaszcza, że nikt nie powinien w nie klikać na edycji, żeby go jeszcze gdzieś przeniosło.

Oczywiście, nie musimy korzystać z WP Query. Możemy połączyć się ze swoim własnym API przez wp_remote_get – tylko po co? W plikach PHP mamy dostęp do naszego backendu, w tym bazy danych i WP Query.