We’ve accomplished a bunch of stuff in this series! We created a custom WordPress block that fetches data from an external API and renders it on the front end. Then we took that work and extended it so the data also renders directly in the WordPress block editor. After that, we created a settings UI for the block using components from the WordPress InspectorControls package.
There’s one last bit for us to cover and that’s saving the settings options. If we recall from the last article, we’re technically able to “save” our selections in the block settings UI, but those aren’t actually stored anywhere. If we make a few selections, save them, then return to the post, the settings are completely reset.
Let’s close the loop and save those settings so they persist the next time we edit a post that contains our custom block!
We’re working with an API that provides us with soccer football team ranking and we’re using it to fetch for displaying rankings based on country, league, and season. We can create new attributes for each of those like this:
Next, we need to set the attributes from LeagueSettings.js. Whenever a ComboboxControl is updated in our settings UI, we need to set the attributes using the setAttributes() method. This was more straightfoward when we were only working with one data endpoint. But now that we have multiple inputs, it’s a little more involved.
This is how I am going to organize it. I am going to create a new object in LeagueSettings.js that follows the structure of the settings attributes and their values.
In each of the handle______Change(), I am going to create a setLocalAttributes() that has an argument that clones and overwrites the previous localSettings object with the new country, league, and season values. This is done using the help of the spread operator.
// LeagueSettings.js function handleCountryChange(value) { // Initial code setLocalAttributes({ ...localSettings, country: value }); // Rest of the code } function handleLeagueChange(value) { // Initial code setLocalAttributes({ ...localSettings, league: value }); // Rest of the code } function handleSeasonChange(value) { // Initial code setLocalAttributes({ ...localSettings, season: value }); // Rest of the code }
We can define the setLocalAttributes() like this:
// LeagueSettings.js function setLocalAttributes(value) { let newSettings = Object.assign(localSettings, value); localSettings = { ...newSettings }; setAttributes({ settings: localSettings }); }
So, we’re using Object.assign() to merge the two objects. Then we can clone the newSettings object back to localSettings because we also need to account for each settings attribute when there a new selection is made and a change occurs.
Finally, we can use the setAttributes() as we do normally to set the final object. You can confirm if the above attributes are changing by updating the selections in the UI.
Another way to confirm is to do a console.log() in DevTools to find the attributes.
Look closer at that screenshot. The values are stored in attributes.settings. We are able to see it happen live because React re-renders every time we make a change in the settings, thanks to the useState() hook.
Displaying the values in the blocks settings UI
It isn’t very useful to store the setting values in the control options themselves since each one is dependent on the other setting value (e.g. rankings by league depends on which season is selected). But it is very useful in situations where the settings values are static and where settings are independent of each other.
Without making the current settings complicated, we can create another section inside the settings panel that shows the current attributes. You can choose your way to display the settings values but I am going to import a Tip component from the @wordpress/components package:
// LeagueSettings.js import { Tip } from "@wordpress/components";
While I’m here, I am going to do a conditional check for the values before displaying them inside the Tip component:
Here’s how that winds up working in the block editor:
API data is more powerful when live data can be shown without having to manually update them each and every time. We will look into that in the next installment of this series.
So far, we’ve covered how to work with data from an external API in a custom WordPress block. We walked through the process of fetching that data for use on the front end of a WordPress site, and how to render it directly in the WordPress Block Editor when placing the block in content. This time, we’re going to bridge those two articles by hooking into the block editor’s control panel to create a settings UI for the block we made.
You know the control panel I’m referring to, right? It’s that panel on the right that contains post and block settings in the block editor.
See that red highlighted area? That’s the control panel. A Paragraph block is currently selected and the settings for it are displayed in the panel. We can change styles, color, typography… a number of things!
Well, that’s exactly what we’re doing this time around. We’re going to create the controls for the settings of the Football Rankings block we worked on in the last two articles. Last time, we made a button in our block that fetches the external data for the football rankings. We already knew the URL and endpoints we needed. But what if we want to fetch ranking for a different country? Or maybe a different league? How about data from a different season?
We need form controls to do that. We could make use of interactive React components — like React-Select — to browse through the various API options that are available to parse that data. But there’s no need for that since WordPress ships with a bunch of core components that we hook right into!
Before we hook into anything, it’s a good idea to map out what it is we need in the first place. I’ve mapped out the structure of the RapidAPI data we’re fetching so we know what’s available to us:
Seasons and countries are two top-level endpoints that map to a leagues endpoint. From there, we have the rest of the data we’re already using to populate the rankings table. So, what we want to do is create settings in the WordPress Block Editor that filter the data by Season, Country, and League, then pass that filtered data into the rankings table. That gives us the ability to drop the block in any WordPress page or post and display variations of the data in the block.
In order to get the standings, we need to first get the leagues. And in order to get the leagues, we first need to get the countries and/or the seasons. You can view the various endpoints in the RapidAPI dashboard.
There are different combinations of data that we can use to populate the rankings, and you might have a preference for which data you want. For the sake of this article, we are going to create the following options in the block settings panel:
Choose Country
Choose League
Choose Season
Then we’ll have a button to submit those selections and fetch the relevant data and pass them into the rankings table.
Load and store a list of countries
We can’t select which country we want data for if we don’t have a list of countries to choose from. So, our first task is to grab a list of countries from RapidAPI.
The ideal thing is to fetch the list of countries when the block is actually used in the page or post content. There’s no need to fetch anything if the block isn’t in use. The approach is very similar to what we did in the first article, the difference being that we are using a different API endpoint and different attributes to store the list of returned countries. There are other WordPress ways to fetch data, like api-fetch, but that‘s outside the scope of what we’re doing here.
We can either include the country list manually after copying it from the API data, or we could use a separate API or library to populate the countries. But the API we’re using already has a list of countries, so I would just use one of its endpoints. Let’s make sure the initial country list loads when the block is inserted into the page or post content in the block editor:
We have a state variable to store the list of countries. Next, we are going to import a component from the @wordpress/block-editor package called InspectorControls which is where all of the components we need to create our settings controls are located.
import { InspectorControls } from "@wordpress/block-editor";
The package’s GitHub repo does a good job explaining InspectorControls. In our example, we can use it to control the API data settings like Country, League, and Season. Here’s a preview so that you get an idea of the UI we’re making:
Here, I am making sure that we are using conditional rendering so that the function only loads the component after the list of countries is loaded. If you’re wondering about that LeagueSettings component, it is a custom component I created in a separate components subfolder in the block so we can have a cleaner and more organized Edit function instead of hundreds of lines of country data to deal with in a single file.
We can import it into the edit.js file like this:
import { LeagueSettings } from "./components/LeagueSettings";
Next, we’re passing the required props to the LeagueSettings component from the parent Edit component so that we can access the state variables and attributes from the LeagueSettings child component. We can also do that with other methods like the Context API to avoid prop drilling, but what we have right now is perfectly suitable for what we’re doing.
The other parts of the Edit function can also be converted into components. For example, the league standings code can be put inside a separate component — like maybe LeagueTable.js — and then imported just like we imported LeagueSettings into the Edit function.
Inside the LeagueSettings.js file
LeagueSettings is just like another React component from which we can destructure the props from the parent component. I am going to use three state variables and an additional leagueID state because we are going to extract the ID from the league object:
There are other panel tags and attributes — it’s just my personal preference to use these ones. None of the others are required… but look at all the components we have available to make a settings panel! I like the simplicity of the PanelBody for our use case. It expands and collapses to reveal the dropdown settings for the block and that’s it.
Speaking of which, we have a choice to make for those selections. We could use the SelectControl component or a ComboBoxControl, which the docs describe as “an enhanced version of a SelectControl, with the addition of being able to search for options using a search input.” That’s nice for us because the list of countries could get pretty long and users will be able to either do a search query or select from a list.
Here’s an example of how a ComboboxControl could work for our country list:
When a country is selected from the ComboboxControl, the country value changes and we filter the data accordingly:
function handleCountryChange(value) { // Set state of the country setCountry(value); // League code from RapidAPI const options = { method: "GET", headers: { "X-RapidAPI-Key": "Your RapidAPI key", "X-RapidAPI-Host": "api-football-v1.p.rapidapi.com", }, }; fetch(`https://api-football-v1.p.rapidapi.com/v3/leagues?country=$ {value}`, options) .then((response) => response.json()) .then((response) => { return response.response; }) .then((leagueOptions) => { // Set state of the league variable setLeague(leagueOptions); // Convert it as we did for Country options setupLeagueSelect = leagueOptions.map((league) => { return { label: league.league.name, value: league.league.name, }; }); setFilteredLeagueOptions(setupLeagueSelect); }) .catch((err) => console.error(err)); }
Note that I am using another three state variables to handle changes when the country selection changes:
I will show the code that I used for the other settings but all it does is take normal cases into account while defining errors for special cases. For example, there will be errors in some countries and leagues because:
there are no standings for some leagues, and
some leagues have standings but they are not in a single table.
This isn’t a JavaScript or React tutorial, so I will let you handle the special cases for the API that you plan to use:
In the last article, we made a button in the block editor that fetches fresh data from the API. There’s no more need for it now that we have settings. Well, we do need it — just not where it currently is. Instead of having it directly in the block that’s rendered in the block editor, we’re going to move it to our PanelBody component to submit the settings selections.
So, back in LeagueSettings.js:
// When countriesList is loaded, show the country combo box { countriesList && ( <ComboboxControl label="Choose country" value={country} options={filteredCountryOptions} onChange={(value) => handleCountryChange(value)} onInputChange={(inputValue) => { setFilteredCountryOptions( setupCountrySelect.filter((option) => option.label .toLowerCase() .startsWith(inputValue.toLowerCase()) ) ); }} /> )} // When filteredLeagueOptions is set through handleCountryChange, show league combobox { filteredLeagueOptions && ( <ComboboxControl label="Choose league" value={league} options={filteredLeagueOptions} onChange={(value) => handleLeagueChange(value)} onInputChange={(inputValue) => { setFilteredLeagueOptions( setupLeagueSelect.filter((option) => option.label .toLowerCase() .startsWith(inputValue.toLowerCase()) ) ); }} /> )} // When filteredSeasonOptions is set through handleLeagueChange, show season combobox { filteredSeasonOptions && ( <> <ComboboxControl label="Choose season" value={season} options={filteredSeasonOptions} onChange={(value) => handleSeasonChange(value)} onInputChange={ (inputValue) => { setFilteredSeasonOptions( setupSeasonSelect.filter((option) => option.label .toLowerCase() .startsWith(inputValue.toLowerCase() ) ); } } /> // When season is set through handleSeasonChange, show the "Fetch data" button { season && ( <button className="fetch-data" onClick={() => getData()}>Fetch data</button> ) } </> </> )}
Here’s the result!
We’re in a very good place with our block. We can render it in the block editor and the front end of the site. We can fetch data from an external API based on a selection of settings we created that filters the data. It’s pretty darn functional!
But there’s another thing we have to tackle. Right now, when we save the page or post that contains the block, the settings we selected for the block reset. In other words, those selections are not saved anywhere. There’s a little more work to make those selections persistent. That’s where we plan to go in the next article, so stay tuned.
This is a tremendous CSS-focused tutorial from Adam Argyle. I really like the “just for gap” concept here. Grid is extremely powerful, but you don’t have to use all its abilities every time you reach for it. Here, Adam reaches for it for very light reasons like using it as an in-between border alternative as well as more generic spacing. I guess he’s putting money where his mouth is in terms of gap superseding margin!
I also really like calling out Una Kravet’s awesome name for flexible grids: RAM. Perhaps you’ve seen the flexible-number-of-columns trick with CSS grid? The bonus trick here (which I first saw from Evan Minto) is to use min(). That way, not only are large layouts covered, but even the very smallest layouts have no hard-coded minimum (like if 100% is smaller than 10ch here):
There is a ton more trickery in the blog post. The “color pops” with :focus-within is fun and clever. So much practical CSS in building something so practical! 🧡 more blog posts like this, please. Fortunately, we don’t have to wait, as Adam has other component-focused posts like this one on Tabs and this one on Sidenav.
This is a tremendous CSS-focused tutorial from Adam Argyle. I really like the “just for gap” concept here. Grid is extremely powerful, but you don’t have to use all its abilities every time you reach for it. Here, Adam reaches for it for very light reasons like using it as an in-between border alternative as well as more generic spacing. I guess he’s putting money where his mouth is in terms of gap superseding margin!
I also really like calling out Una Kravet’s awesome name for flexible grids: RAM. Perhaps you’ve seen the flexible-number-of-columns trick with CSS grid? The bonus trick here (which I first saw from Evan Minto) is to use min(). That way, not only are large layouts covered, but even the very smallest layouts have no hard-coded minimum (like if 100% is smaller than 10ch here):
There is a ton more trickery in the blog post. The “color pops” with :focus-within is fun and clever. So much practical CSS in building something so practical! 🧡 more blog posts like this, please. Fortunately, we don’t have to wait, as Adam has other component-focused posts like this one on Tabs and this one on Sidenav.
The title of this Sara Soueidan article speaks to me. I’m a big fan of the idea that some CSS is best applied globally, and some CSS is best applied scoped to a component. I’m less interested in how that is done and more interested in just seeing that conceptual approach used in some fashion.
Sara details an approach where components don’t have too much styling by default, but have CSS custom properties applied to them that are ready to take values should you choose to set them.
For each pattern, I’ve found myself modifying the same properties whenever I needed to use it — like the font, colors (text, background, border), box shadow, spacing, etc. So I figured it would be useful and time-saving if I created variables for those properties, define those variables in the ‘root’ of the component, and ‘pass in’ the values for these variables when I use the pattern as I need. This way I can customize or theme the component by changing the property values in one rule set, instead of having to jump between multiple ones to do so.