WordPress Development Lecture Notes
WordPress Development Lecture Notes
In this section, we get started with introductions, WordPress installation, and just some nice to know things. Glad to
have you on board. 😄
Lecture 1: Introduction
Welcome!👋 Glad to have you on board! In this lecture, we talked about what you can expect from this course, its
prerequisites, and some tips for getting the most out of this course.
Prerequisites
Basic WordPress Administrative Tasks
HTML
CSS
PHP (Optional)
JavaScript (Optional)
My second tip is to ask questions in the Q&A section. I'm here to help you with your journey of becoming a
WordPress developer. I usually respond within 24 hours.
Please refrain from asking questions related to personal projects. Keep your questions limited to the content of the
course.
Production - A live site that is publicly accessible to the world. Should store a stable copy of your site.
Development - A site that is privately accessible to developers. Perfect for performing experiments and
testing plugins/themes.
For this course, we're going to set up a development environment on our machine. Setting up an environment can
be a tedious task.
Luckily, there are programs available for quickly installing WordPress with the necessary programs and
configurations. The program I recommend is called Local. Installing Local is like installing any other program. Install
Local before proceeding to the next lecture.
Resources
Local
You don’t have to use the same editor, but I recommend it for consistency. If you’d like to use a different editor, be
sure it supports HTML, CSS, JavaScript, and PHP. These are the languages we’ll be using in this course. As long as
those languages are supported, you’re good to go.
Section 2: PHP Fundamentals
In this section, we got started with the fundamentals of PHP. It's the language WordPress is written with. So it's an
essential language to learn.
We were able to access the file via an HTTP URL. By installing a web server, like Nginx, it'll allow us to access the files
in our project. Before a file is sent over to the browser, the server will process the PHP code in a file. This is why we're
allowed to access PHP files from the browser.
Lecture 8: Variables
In this lecture, we explored the first feature of PHP, which are variables. A variable can be thought of as a container
for your data. You can store any type of data from numbers, names, file data, etc. In our PHP file, we wrote the
following code:
<?php
$age = 28;
?>
We can write PHP code by adding a pair of PHP tags. This is where PHP starts to become different from HTML. Inside
the PHP tags, we can start giving instructions to our machine. We are not allowed to write HTML. In addition, we're
allowed to add as many PHP tags in our file as we'd like. It's common practice not to close a PHP tag if you don't
intend on writing HTML after the PHP tag.
Variables
Variables can be declared by typing the $ symbol followed by the name. There are rules for PHP names. They're the
following:
A variable starts with the $ sign, followed by the name of the variable
A variable name must start with a letter or the underscore character
A variable name cannot start with a number
A variable name can only contain alpha-numeric characters and underscores (A-z, 0-9, and _ )
Variable names are case-sensitive ($age and $AGE are two different variables)
Syntax
There are rules for writing PHP code called its syntax. Just like the English language, we must adhere to these rules.
One of the rules is to end every line of code with a ; character. This indicates we're finished with a line of code.
PHP will run the next line of code.
Outputting Variables
We can output a variable with the following code:
echo $age;
The echo keyword instructs the language to output the value that's written after it. In this example, the $age
variable will get outputted. PHP will search for this variable and output the value assigned to the variable. If we didn't
include this line of code, nothing would get added to our HTML document.
Integer
Float
String
Boolean
Array
Object
Null
Null
The null data type is a special type of data type for variables that aren't declared with a value. It's a great data
type to use when you want to have a variable available without giving it a value.
String
The string data type is used for storing text. We can store names, addresses, gibberish, or anything that we can
type on our keyboard. Strings can be created with single quotes or double-quotes. Here's an example of both
syntaxes
$name = 'Luis';
$name = "Luis";
Booleans
Booleans are another type of data type that can only have two possible values, true or false . They're a useful
feature for telling your program if something should be on or off. We'll come across practical situations throughout
the course. Here's an example of both values:
$isLoggedIn = true;
$isLoggedIn = false;
Notice how we're using multi-worded variable names. There are two naming conventions for multi-worded variables
in PHP called camel casing or snake casing. Camel casing is when every word in a variable is capitalized except the
first word. Snake casing is when each word is separated with underscores.
Either convention is valid. It's all preference. Whichever you use, be consistent.
function greeting() {
echo 'Hello';
}
greeting();
Functions are defined with the function keyword. Code inside the curly brackets makes up the function's body.
By default, PHP will not run the code inside a function. We must call it. Calling a function can be done by writing its
name followed by parentheses.
Parameters/Arguments
Functions can have local variables called parameters. They can be added to a function's parentheses like so:
function greeting($message) {
echo $message;
}
greeting('hello');
This feature can be useful for passing on custom data from the function call to the function. In this example, the
string, ``hello' , will be used as the value for the $message` variable.
Items in an array can be accessed by their index. Indexes are zero-based. This means the first item in the array has an
index of 0 . The second item in the array has an index of 1 . So on and so forth. We can output a specific item in
the array with the following syntax:
echo $food[0];
$count = $count + 1;
}
In this example, we're looping through the $food array with the while() loop. The while() loop must be
passed in a condition inside its parentheses. In this example, we're checking if the $count variable is less than the
items in the $food array. We're counting the items in the array with the count() function, which is another PHP
defined functions. If this condition evaluates to true , the code inside the block will execute.
Inside the curly brackets, we are outputting the item in the array by using the $count variable. After outputting
the item, we are incrementing the $count variable by one.
Alternative syntax
Alternatively, you can use the increment operator to update a numeric value by one like so:
$count++;
Either syntax is valid. Beginner developers are encouraged to use the array() function over the square bracket
syntax to make it easier to identify an array declaration.
Resources
Comparison Operators
define("NAME", "Luis");
echo NAME;
It's common practice for constant names to have all uppercase letters to differentiate them from regular variables.
echo NAME;
Type - The type of error. Warnings will not stop the script from running.
Description - A summary of the error and why PHP couldn't properly process your code.
File Location/Line Number - Self-explanatory, the location where the error was produced.
With this critical information, you'll be able to quickly resolve your issues by carefully reading the error. Life won't
always be this easy, but it's the best place to start when you run into trouble.
Single-line comments allow us to add a comment to a single line. Subsequent lines are unaffected.
// Single-line example
/*
Multiline
Example
*/
In some cases, developers may add an * on every line. This does not affect the comment. It's just a style
preference.
/*
* Multiline
* Example
*/
WordPress uses constants for creating its configuration settings. In addition, constant names are written with all
uppercase letters. It's a standard convention.
define('WP_DISABLE_FATAL_ERROR_HANDLER', true);
Security Keys
In addition, the configuration file contains security keys for securely logging users in. In case of an emergency, you
should reset these values. If you can't change these values, WordPress has a tool for generating new keys here
In most cases, you will be working inside the wp-content folder for creating the theme and plugin. You should never
modify files outside this directory. Otherwise, your changes may be overridden. The wp-config.php file is an
exception.
1. index.php
2. style.css
3. File Header
It's recommended you create your themes in the wp-content/themes directory. A separate folder should host your
theme's files. For this course, we created a folder called udemy.
As for the file header, this should be created in the style.css file. File headers provide information to WordPress on
your theme's details, such as the name, author, and license. Here's an example of a file header.
/*
Theme Name: Twenty Twenty
Theme URI: https://wordpress.org/themes/twentytwenty/
Author: the WordPress team
Author URI: https://wordpress.org/
Description: Our default theme for 2020 is designed to take full advantage of the
flexibility of the block editor. Organizations and businesses have the ability to
create dynamic landing pages with endless layouts using the group and column blocks.
The centered content column and fine-tuned typography also makes it perfect for
traditional blogs. Complete editor styles give you a good idea of what your content
will look like, even before you publish. You can give your site a personal touch by
changing the background colors and the accent color in the Customizer. The colors of
all elements on your site are automatically calculated based on the colors you pick,
ensuring a high, accessible color contrast for your visitors.
Tags: blog, one-column, custom-background, custom-colors, custom-logo, custom-menu,
editor-style, featured-images, footer-widgets, full-width-template, rtl-language-
support, sticky-post, theme-options, threaded-comments, translation-ready, block-
styles, wide-blocks, accessibility-ready
Version: 1.3
Requires at least: 5.0
Tested up to: 5.4
Requires PHP: 7.0
License: GNU General Public License v2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
Text Domain: twentytwenty
This theme, like WordPress, is licensed under the GPL.
Use it to make something cool, have fun, and share what you've learned with others.
*/
/*
Theme Name: Udemy
Theme URI: https://udemy.com
Author: Udemy
Author URI: https://udemy.com
Description: A simple WordPress theme.
Version: 1.0
Requires at least: 5.8
Tested up to: 6.0
Requires PHP: 8.0
Text Domain: udemy
*/
💰 Expensive
🔖 Gotta manage licenses
😕 Not Standardized
WordPress saw an opportunity to resolve these issues by extending the Gutenberg editor from a post content editor
to a full-blown page builder. With the introduction of full-site editing (FSE), WordPress resolves the issues of past
page builders. Completely free, no licenses, and standardized methodology of building pages.
Love it or hate it, FSE is here to stay. This course will cover full-site editing to its fullest extent. Don't worry, classic-
site editing will be covered too in a future section.
The HTML file has priority over the PHP file. If the HTML file does not exist, WordPress will fall back to the PHP file. By
adding an templates/index.html file, WordPress will switch over to full-site editing.
You should keep in mind that full-site editing only supports blocks. If you were to attempt to write plain HTML,
WordPress will throw an editor from within its editor.
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo( 'charset' ); ?>" />
<?php wp_head(); ?>
</head>
(template)
We're going to recreate this template so that we can fully understand what's going on behind the scenes. This is also
a great opportunity to practice writing PHP.
For example, let's say a site is in Arabic; text should appear from right to left. CSS developers would use the following
to change the direction:
:lang(ar) {
direction: rtl;
}
Without the lang attribute set to the correct language code, the above code snippet would not work. For these
reasons, you should always add the lang attribute to your documents.
Hardcoding this attribute is not recommended. Your theme should be compatible with various languages. Luckily,
WordPress has a function called language_attributes() for checking the language of the current WordPress
installation and outputting it onto the document. Here's an example:
<html <?php language_attributes(); ?>>
Resources
Language Codes
language_attributes() Function
You can refer to the documentation for a list of possible values. In our case, we can pass in the value 'charset'
to instruct this function to get the character set for the current site. Here's an example:
Resources
bloginfo() Function
There are two functions that we should use called wp_head() and wp_footer() . The wp_head() function
can be added at the end of the <head> section of a document. Here's an example:
<head>
<meta charset="<?php bloginfo('charset'); ?>">
<?php wp_head(); ?>
</head>
It's recommended that you add this function after you've added tags for your theme. Your theme's tags should have
priority over external tags.
As for the wp_footer() function, this function should be called before the closing <body> tag. Here's an
example:
<body>
...
Same as before, this function will allow plugins and the WordPress core to inject content at the bottom of the
document.
Lecture 28: The Body Tag
In this lecture, we got started with adding additional functions for modifying the <body> tag of the document. The
first function is called body_class() . This function will add a series of classes to the <body> tag. We even have
the option of adding our own classes. Here's an example:
The second function is called wp_body_open() . This function will allow the WordPress core and 3rd party plugins
to inject content into the opening tag of the <body> tag. This is the complete opposite of the wp_footer()
function, which will inject content at the bottom of the <body> tag. Here's an example
<body>
<?php wp_body_open(); ?>
</body>
wordpress.com - A hosting platform for WordPress. Buy servers, connect your domains, and manage your
sites.
wordpress.org - Hosts the original source code of WordPress along with basic usage information.
developer.wordpress.org - A resource dedicated to programmers for learning about WordPress's functions
and classes along with guides for developing themes/plugins.
codex.wordpress.org - A resource for technical and non-technical users. It was the original resource for
developers but is currently being moved over to developer.wordpress.org.
Resources
Official WordPress Site (.org)
WordPress Hosting Platform
Developer Documentation
Codex
Automaticc
However, you should try your best to adhere to these standards. They can reduce confusion among teams and make
the development cycle easier.
Resources
Coding Standards
Section 4: Global Styles
In this section, we modified WordPress's global styles to enhance the editing experience for our theme.
JSON was introduced as a way to store data in a separate file. It stands for JavaScript Object Notation. Heavily
inspired by JavaScript but not required to know.
JSON Supports 5 data types: strings, numbers, booleans, arrays, and objects. Here's an example of some JSON code.
{
"name": "John",
"age": 20,
"isHungry": true,
"hobbies": ["hockey", "baksetball"],
"job": {
"company": "Udemy"
}
}
We learned about a new data type called objects. The object data type allows us to group pieces of information
together. A similar concept to array. The main difference is that objects follow a key-value format whereas arrays can
just contain values.
Resources
JSON Playground
We can access the global styles in the full-site editor at the top left corner:
WordPress allows theme developers to modify the features available in the global styles through a file called
theme.json. This file is optional, but if provided, WordPress will load its configuration settings.
Schemas can be added by adding the $schema property. The value should be a URL to the schema. Here's an
example of our schema:
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 2,
}
By adding this property to your JSON file, your editor should tell you that the version property is missing and
that it should be an integer.
It's possible that you may get an error after adding the schema. If the error states the certificate has expired, try the
following fixes:
Resources
Schema
The default palette can be toggled by setting the color.defaultPalette property. For example, here's how you
would disable the default palette on a global level
{
"settings": {
"color": {
"defaultPalette": false,
},
}
}
Resources
Global Styles
Theme.json Reference
{
"settings": {
"blocks": {
"core/site-title": {
"color": {
"defaultPalette": true,
}
}
}
}
}
Keep in mind, that settings can only be enabled/disabled if the block originally supports the setting. If a block does
not support a feature, forcibly enabling it will not do anything.
Resources
Core Blocks Reference
{
"settings": {
"color": {
"palette": [
{ "slug": "u-white", "color": "rgb(255,255,255)", "name": "Udemy White" },
{ "slug": "u-gray-100", "color": "rgb(243 244 246)", "name": "Udemy Gray
100" },
{ "slug": "u-gray-200", "color": "rgb(229 231 235)", "name": "Udemy Gray
200" },
{ "slug": "u-gray-300", "color": "rgb(209 213 219)", "name": "Udemy Gray
300" },
{ "slug": "u-gray-400", "color": "rgb(156 163 175)", "name": "Udemy Gray
400" },
{ "slug": "u-gray-500", "color": "rgb(107 114 128)", "name": "Udemy Gray
500" },
{ "slug": "u-gray-600", "color": "rgb(75 85 99)", "name": "Udemy Gray 600"
},
{ "slug": "u-gray-700", "color": "rgb(55 65 81)", "name": "Udemy Gray 700"
},
{ "slug": "u-gray-800", "color": "rgb(31 41 55)", "name": "Udemy Gray 800"
},
{ "slug": "u-gray-900", "color": "rgb(17 24 39)", "name": "Udemy Gray 900"
},
{ "slug": "u-primary", "color": "rgb(239 68 68)", "name": "Udemy Primary" },
{ "slug": "u-accent", "color": "rgb(67 56 202)", "name": "Udemy Accent" },
{ "slug": "u-success", "color": "rgb(52 211 153)", "name": "Udemy Success"
},
{ "slug": "u-info", "color": "rgb(96 165 250)", "name": "Udemy Info" },
{ "slug": "u-warning", "color": "rgb(251 146 60)", "name": "Udemy Warning"
},
{ "slug": "u-danger", "color": "rgb(236 72 153)", "name": "Udemy Danger" }
]
},
}
}
The palette property should be an array of objects. In each object you can add the following properties:
slug - A unique ID for the color. It's recommended to add a prefix to help WordPress identify your colors
among its own preset and plugin colors.
color - A valid CSS color value. (Hex, RGB, RGBA, Color Names, Keywords, HSL, HSLA)
name - A human readable name that will be displayed on the front end.
We can even add colors to specific blocks. Here's an example that adds the color pink to the Site Title block.
{
"settings": {
"blocks": {
"core/site-title": {
"color": {
"palette": [
{ "slug": "u-pink", "color": "#f1c0e8", "name": "Udemy Pink"}
]
}
}
}
}
}
Keep in mind, WordPress will prioritize block-specific settings over global settings. This palette will override the
global palette.
Resources
Core Blocks Reference
{
"settings": {
"color": {
"background": true,
"link": true,
"text": true
},
"blocks": {
"core/site-title": {
"color": {
"background": true,
"link": true,
"text": true
}
}
}
},
}
Duotones are an experimental feature. There are some bugs, so be careful when using them. We can apply duotones
to images and videos. Here's an example of how to add duotones to a theme.
{
"settings": {
"color": {
"duotone": [
{
"slug": "u-pink-sunset",
"colors": ["#11245E", "#DC4379"],
"name": "Udemy Pink Sunset"
}
]
}
}
}
A duotone can be added by adding a series of objects to the duotone array. The format for each object is the
following:
In some cases, you may want to disable the duotone feature for a block. You can do so by setting the duotone
property to an empty array. WordPress will not recommend presets by doing so. However, the editor will allow users
to select custom colors. We can disable custom selection by adding the customDuotone property to false. Here's
an example of disabling duotones for the cover block.
{
"settings": {
"blocks": {
"core/cover": {
"color": {
"duotone": [],
"customDuotone": false
}
}
}
}
}
{
"settings": {
"color": {
"defaultGradients": false
}
}
}
Gradients can be added through the gradients array under the colors object. The format for adding
gradients is the following:
{
"settings": {
"color": {
"gradients": [
{
"slug": "u-summer-dog",
"gradient": "linear-gradient(#a8ff78, #78ffd6)",
"name": "Udemy Summer Dog"
}
]
},
"blocks": {
"core/site-title": {
"color": {
"gradients": [],
"customGradient": false
}
}
}
}
}
In some cases, you may want to completely disable the gradients option. There are two settings you will need to add.
Firstly, the gradients property should be an empty array to prevent WordPress from recommending colors.
Secondly, the customGradient property must be set to false to prevent users from selecting colors. Here's an
example of disabling the gradients option from the Site Title block.
{
"settings": {
"blocks": {
"core/site-title": {
"color": {
"gradients": [],
"customGradient": false
}
}
}
}
}
We can modify the styles by adding the styles object to our JSON file. This object is separate from the
settings property, which is primarily used for enabling/disabling features.
{
"styles": {
"color": {
"text": "var(--wp--preset--color--u-gray-700)",
"background": "var(--wp--preset--color--u-white)"
}
}
}
In the above example, we're changing the colors of the text and background to CSS variables. Any valid CSS value
will work.
WordPress will store custom colors in variables. Their values are extracted for further use in our CSS or options.
Colors are prefixed with --wp--preset--color-- followed by the slug of the color.
Duotones and gradients are also converted to variables. The prefix for duotones is --wp--preset--duotone-- .
The prefix for backgrounds are --wp--preset--gradient-- .
Lecture 42: Applying Gradients
In this lecture, we learned how to apply gradients to a block. As a reminder, the prefix for gradients is --wp--
preset--gradient-- followed by the slug. Styles can be applied to a block. For this example, we applied the
gradient to the button block. Here's how we did it.
{
"styles": {
"blocks": {
"core/button": {
"color": {
"gradient": "var(--wp--preset--gradient--u-summer-dog)"
}
},
}
}
}
Similar configuring the settings for a block. The blocks object can be added to the styles object with the
name of the block as a child. The gradient can be configured through the gradient property.
{
"styles": {
"elements": {
"link": {
"color": {
"text": "var(--wp--preset--color--u-primary)"
}
}
}
}
}
In this example, we're changing the color for the link element, which is the selector for <a> tags. For headings,
we must use h1 , h2 , etc.
Border settings can be enabled by adding the border object to the settings object. However, you may
enable/disable this feature for specific blocks too. There are four options we can enable/disable. They're the color,
radius, style, and width of a border.
After making those changes, you have the option of setting a default border through the styles property like so.
{
"styles": {
"blocks": {
"core/pullquote": {
"border": {
"width": "2px",
"radius": "10px",
"style": "solid",
"color": "var(--wp--preset--color--u-primary)"
}
}
},
}
}
In this example, we're modifying the pull-quote block. The values for the width, radius, style, and color must be valid
CSS values.
{
"settings": {
"typography": {
"fontFamilies": [
{
"fontFamily": "Rubik, sans-serif",
"slug": "u-rubik",
"name": "Udemy Rubik"
}
]
}
},
}
The fontFamilies is an array of objects where each object represents a font. There are three properties that
must be added for each font
fontFamily - A valid CSS value for the font family name. Can be multiple fonts (recommended)
slug - A unique ID for the font
name - A human-readable name for the font that will be displayed in the editor.
Alternatively, you can modify the font family options for a specific block. Here's an example of a set of custom font
options for the Site Title block.
{
"settings": {
"blocks": {
"core/site-title": {
"typography": {
"fontFamilies": [
{
"fontFamily": "Pacifico, sans-serif",
"slug": "u-pacifico",
"name": "Udemy Pacifico"
}
]
}
}
}
}
}
The typography object can be added to any blocks with the same settings for the global-level settings. Keep in
mind, the font family for blocks will override the global blocks even if they're custom font families. If you would like
the same fonts to appear, you will need to read them for the block.
This solution is not complete. You must also load the font families, which are not supported through the theme.json
file. This topic is covered in the next section. As long as the font families appear as an option in the sidebar, you're
good to go.
Resources
Pacifico
Rubik
We can override these values by updating the theme.json file with the following:
{
"settings": {
"typography": {
"fontSizes": [
{ "slug": "small", "size": "0.75rem", "name": "Small" },
{ "slug": "medium", "size": "1.25rem", "name": "Medium" },
{ "slug": "large", "size": "2.25rem", "name": "Large" },
{ "slug": "x-large", "size": "3rem", "name": "Extra Large" },
{ "slug": "gigantic", "size": "3.75rem", "name": "Gigantic"}
]
}
}
}
New font sizes can be made by adding the fontSizes array. In this array, we must add objects that represent
each font size. The following properties must be present.
{
"settings": {
"blocks": {
"core/preformatted": {
"typography": {
"fontSizes": [],
"customFontSize": false
}
}
}
}
}
Resources
Standardizing Theme.json Font Sizes
{
"settings": {
"typography": {
"lineHeight": true,
"dropCap": true,
"fontWeight": true,
"fontStyle": true,
"textTransform": true,
"letterSpacing": true,
"textDecoration": true
}
}
}
{
"styles": {
"typography": {
"fontFamily": "var(--wp--preset--font-family--u-rubik)",
"fontSize": "16px",
"fontStyle": "normal",
"fontWeight": "normal",
"lineHeight": "inherit",
"textDecoration": "none",
"textTransform": "none"
},
}
}
Most of these are self-explanatory. For the fontFamily property, we're setting the value to a variable called --
wp--preset--font-family--u-rubik . All custom font families will have variables that are prefixed with --wp-
-preset--font-family-- followed by the slug name.
In some cases, you may want to add font families for a specific block. Here's how we added one for the Site Title
block.
{
"styles": {
"blocks": {
"core/site-title": {
"typography": {
"fontFamily": "var(--wp--preset--font-family--u-pacifico)"
}
}
}
}
}
{
"settings": {
"layout": {
"contentSize": "840px",
"wideSize": "1100px"
}
},
}
The contentSize property should be set to the maximum width of your content. The value must be a valid CSS
value. In addition, you have the option of adding the wideSize property for supporting wide alignment. If added,
WordPress will add the option to the alignment toolbar. The value for this property should be a size bigger than the
contentSize property. The wideSize option will allow blocks to stretch outside the width of the content.
{
"settings": {
"spacing": {
"margin": true,
"padding": true
}
}
}
You can also configure the margin and padding values for the document or for specific blocks. Here's an example of
how we can apply margin and padding:
{
"styles": {
"spacing": {
"margin": {
"top": "0px",
"right": "0px",
"bottom": "0px",
"left": "0px"
},
"padding": {
"top": "0px",
"right": "0px",
"bottom": "0px",
"left": "0px"
}
}
}
}
{
"settings": {
"spacing": {
"units": ["px", "rem", "%", "vw", "vh"]
}
}
}
You can configure the gap by updating the blockGap property to your desired space. The value must be a valid
CSS value.
{
"styles": {
"spacing": {
"blockGap": "4rem"
}
}
}
{
"settings": {
"appearanceTools": true
}
}
Section 5: Managing Asset Files
In this section, we got started with loading asset files for our theme. It's one of the next steps you'll take to create a
theme.
So, what about GitHub? Git runs on your machine, but you may want to publish your code online. There are various
platforms that have integration with Git. The most popular platform is GitHub.
I highly recommend watching the video to learn how to upload your project to GitHub to share it with me.
Resources
GitHub Desktop
Git and GitHub Tutorial
1. Users are able to change the WordPress directory structure, which can render absolute links useless.
2. Hard coding links doesn’t allow for checking if SSL is enabled. (https:// & http://)
3. You’ll end up loading all scripts and stylesheets on every page, even if you don’t need them.
WordPress introduces some solutions for avoiding these issues, which we'll explore in the upcoming lecture.
Events can range from form submissions to sending emails. WordPress offers hundreds of hooks. On top of the
hooks defined by WordPress, we can create custom hooks.
We're discussing hooks because WordPress offers a hook for loading stylesheets and script files. Let's try using the
hook API for loading our assets.
Organization
We added a couple of sections to our functions.php file to keep things organized. We'll discuss each section as we
need them. We're adding them ahead of time.
<?php
// Variables
// Includes
// Hooks
// Hooks
add_action('wp_enqueue_scripts', 'u_enqueue');
A hook can be registered by using the add_action() function. This function has two arguments, which are the
name of the hook and the name of the function to run during the hook. In this example, we are prefixing the name of
our function with the letter u , which is short for Udemy.
Prefixing function names is a common practice in the WordPress community. PHP will throw an error if two functions
have the same name. It's possible site owners may install plugins, which can cause duplicate function names. To
avoid this issue, it's recommended to prefix ALL your function names.
If you're new to PHP, functions can accept multiple values. If it does, like the add_action() function, you can
separate each value with a comma.
One thing to keep in mind is that PHP will stop running functions if a value is returned. If you want to run additional
logic, it should be performed before returning a value.
PHP has a function called include() that performs this task. It has one argument, which is the path to the file.
First, we need to create a file that we want to include. We created a new file in the includes/front directory called
enqueue.php. It contains the following code:
function u_enqueue() {
In the functions.php file, we include this file by using the include() function like so:
// Includes
include( get_theme_file_path('/includes/front/enqueue.php') );
We're using a function called get_theme_file_path() that's defined by WordPress. This function will generate
a full path to the current activated theme. We can pass in an optional path relative to the theme directory that'll get
appended to the final path.
By using the include() function, the contents of the enqueue.php file are added to the functions.php file,
which gives us access to the u_enqueue() function.
function u_enqueue() {
wp_register_style(
'u_font_rubik_and_pacifico',
'https://fonts.googleapis.com/css2?
family=Pacifico&family=Rubik:wght@300;400;500;700&display=swap'
);
wp_register_style(
'u_bootstrap_icons',
get_theme_file_uri('assets/bootstrap-icons/bootstrap-icons.css')
);
wp_register_style(
'u_theme',
get_theme_file_uri('assets/public/index.css')
);
}
The first two arguments of this function are required. Firstly, we must provide a handle name, which can be thought
of as an ID for the file. Handle names should be prefixed since plugins are capable of registering styles too. To
prevent naming conflicts, prefixing can avoid this issue.
The second argument must be a valid HTTP URL. In this example, we're loading files from external and local sources.
To get a valid URL for a local source, you can use the get_theme_file_uri() function. This function is not to be
confused with the get_theme_file_path() function, which returns a system path. The
get_theme_file_uri() functions returns an HTTP URL.
It's advantageous to use this function because it'll always point to the correct directory and check for SSL. It has an
optional argument, which is a path relative to the theme directory. In this example, we're passing in a path to a CSS
file.
Resources
wp_register_style() Function
At the end of the u_enqueue() function, we're calling this function to queue all our stylesheets.
wp_enqueue_style('u_font_rubik_and_pacifico');
wp_enqueue_style('u_bootstrap_icons');
wp_enqueue_style('u_theme');
This function requires the handle name of the file that should be loaded. It should correspond to the value passed
into the first argument of the wp_register_style() function.
Resources
wp_enqueue_style() Function
A query parameter is a feature for adding values to the URL. This is useful for sending data to the server without the
user having to submit a form. Query parameters are added to a URL by adding a ? character followed by key-value
pairs. Multiple key-value pairs can be separated with the & character.
Example: example.com?key=value&anotherKey=anotherValue
We can fix this issue by preventing the ver parameter from being added. The 4th argument of the
wp_register_style() function is a custom version. You can set this to a string. Alternatively, you can pass in
null to disable WordPress from adding a parameter. Therefore, our parameters are never filtered. Here's the
updated registration:
wp_register_style(
'u_font_rubik_and_pacifico',
'https://fonts.googleapis.com/css2?
family=Pacifico&family=Rubik:wght@300;400;500;700&display=swap',
[],
null
);
}
include( get_theme_file_path('/includes/front/head.php') );
We are defining the function. Before looking at the definition, we are taking advantage of the third argument of the
add_action() function, which is the priority. We are running the function early so that the tags can appear earlier
than the style links.
function u_head() {
?>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<?php
}
In our function, we're outputting the tags by leaving PHP mode. This is preferable to using the echo statement so
that we can benefit from syntax highlighting.
Overall, caching is a good thing. However, it can be a pain to deal with during the development lifecycle of a plugin.
As an example, we created a file called custom.css with the following code:
body{
background-color: yellow !important;
}
wp_register_style(
'u_custom',
get_theme_file_uri('assets/custom.css')
);
wp_enqueue_style('u_custom');
Let's say we changed the file. The browser may still be storing the old copy of the file. You will need to manually clear
the cache every time a file changes. Luckily, its easy to make working with caching easier with WordPress. We will talk
about a solution in the next lecture.
The wp_register_style() function has an argument for modifying the version of the URL. The purpose of
adding a version is to encourage the browser to download the latest version. By default, the version is set to the
current version of the WordPress installation. In our enqueue.php file, we updated the custom.css file registration
with the following code:
wp_register_style(
'u_custom',
get_theme_file_uri('assets/custom.css'),
[],
filemtime(get_theme_file_path('assets/custom.css'))
);
We're using a function called filemtime() to retrieve a file's modification time. This function accepts a system
path to the file. To help us, we're using the get_theme_file_path() function to help us grab a path to our
theme's files.
After making those changes, anytime our CSS files change, the browser will always download the latest copy.
After calling this function, we can start registering and queueing files. We took care of this step in the enqueue.php
file.
wp_register_script(
'u_bootstrap',
get_theme_file_uri('assets/vendor/bootstrap/dist/js/bootstrap.bundle.min.js'),
[],
false,
true
);
wp_register_script(
'u_simplebar',
get_theme_file_uri('assets/vendor/simplebar/dist/simplebar.min.js'),
[], false, true
);
wp_register_script(
'u_smooth_scroll',
get_theme_file_uri('assets/vendor/smooth-scroll/dist/smooth-
scroll.polyfills.min.js'),
[], false, true
);
wp_register_script(
'u_theme',
get_theme_file_uri('assets/js/theme.min.js'),
[], false, true
);
In the above code snippet, we're calling the wp_register_script() function 4 times. Once for each script. The
parameters for the function are the handle name, source, dependencies, version, and location. The last parameter is
the most important one. By default, WordPress will load script files in the header. By setting this parameter to
true , files will be loaded in the footer.
The last step is to queue the files by calling the wp_enqueue_script() function.
wp_enqueue_script('u_bootstrap');
wp_enqueue_script('u_simplebar');
wp_enqueue_script('u_smooth_scroll');
wp_enqueue_script('u_theme');
This function has one argument, which is the handle name of the script. This value must correspond to the first
argument of the wp_register_script() function.
Registering jQuery is highly discouraged. Since jQuery is popular, WordPress registers a version for you. If developers
used different versions of jQuery, this could lead to issues. It's better to let WordPress handle registering jQuery. The
only step you need to take care of is queueing it. It can be queued with the following code:
wp_enqueue_script('jquery');
Lecture 27: Creating Header, Footer, and Sidebar Areas
In this lecture, we separated the index.php template into separate files. Three files were created called header.php,
footer.php, and sidebar.php. We were able to retrieve each of these sections with the get_header() ,
get_footer() , and get_sidebar() functions, respectively.
Behind the scenes, these functions are including the files. So, what's the difference between them and the
include() function. Firstly, we don't need to provide the full system path to our files. WordPress will
automatically search for these files in our theme directory.
Secondly, we're allowed to pass in a string that allows us to grab different files. For example, the following function
will get_header('v2') will search for a file called header-v2.php. This behavior applies to the get_footer()
and get_sidebar() functions too.
The main benefit of separating your code into separate files is that it makes it easier to read and maintain. Template
parts can also be used in multiple templates.
The first step is to create a folder called parts in your theme folder. By creating template parts in this folder,
WordPress will autoload them when users try adding template blocks to their template.
Afterward, we created two template parts called header.html and footer.html. You can use whatever filename you
prefer. However, it's recommended to keep your names short, concise, and descriptive. Inside these files, you must
insert blocks, not raw HTML.
Lastly, the index.html file was updated to include these template parts.
We're using the template part block to help us load our template parts into the template.
Lecture 58: Brief Anatomy of a Block
In this lecture, we briefly looked at the markup of a block. There are two types of blocks you'll come across which are
blocks with content and blocks without content. A block with content will look like this:
A block is surrounded by HTML comments that contain the name of the block. The closing block has / character
before the name to indicate the end of the block. Inside the block, you will find static content that will be rendered in
the browser.
Same as before, the comment contains the name of the block. However, the comment is self-closing by adding the
/ at the end of the comment. In addition, the block has its settings stored in a JSON object. Normally, you will
never need to edit this info. In most cases, you can edit these settings from the editor.
We updated the header.html and footer.html template parts by letting the template part block generate the root
element. Here's what the header.html file looks like:
<!-- wp:template-part
{"slug":"header","theme":"udemy","tagName":"header","className":"shadow"} /-->
<!-- wp:template-part
{"slug":"footer","theme":"udemy","tagName":"footer","className":"bg-gray-700 p-16"}
/-->
The classes and tag names will appear in both comments. WordPress will take care of the rest of applying these
settings to the template part. By doing this, we reduce bloat in our document with unnecessary markup. Whenever
possible, you should let WordPress generate the markup for your template instead of using raw HTML.
We have two options at our disposal. We can apply these classes directly to the block. Alternatively, we can
manipulate the block's settings to achieve the same effects. Either solution is valid. It entirely depends on the
flexibility you want to give your clients. Applying classes are fast and easy, but they're not going to be easy to modify
for clients. Whereas modifying the block settings is slower but allows clients to modify them without knowledge of
HTML and CSS.
For this course, we decided to use block settings. We modified the following settings of the row block.
We inserted this block into the row block with the following classes: container , !mx-auto , flex , items-
center , and justify-between . These classes will center the blocks inserted into the block, which will align
them with the rest of the page. In addition, for testing purposes, we added a paragraph block to test the centering of
the content.
The first step to loading our styles in the editor is to hook into the right event. The name of the event recommended
for loading styles in the Gutenberg editor is called after_setup_theme This hook runs after the theme has been
loaded. WordPress recommends this hook for setting up your theme.
include( get_theme_file_path('/includes/setup.php') );
add_action('after_setup_theme', 'u_setup_theme');
In the above example, we're running a function called u_setup_theme() , which will be responsible for handling
the setup of our theme. Instead of defining this function in the same file, we're going to define it in a file called
includes/setup.php. Here's what the code looks like for this file:
function u_setup_theme() {
add_theme_support('editor-styles');
add_editor_style([
'https://fonts.googleapis.com/css2?
family=Pacifico&family=Rubik:wght@300;400;500;700&display=swap',
'assets/bootstrap-icons/bootstrap-icons.css',
'assets/public/index.css'
]);
}
We're running two functions. The first function is called add_theme_support() , which will enable support for
custom styles for the Gutenberg editor. We must always enable this feature if we want to load styles. Otherwise, we
may run into problems.
WordPress has various features that can be enabled. Therefore, we must pass in the name of the feature we'd like to
enable. In this example, the string editor-styles can be passed in to enable custom styles.
Afterward, we're calling the add_editor_style() function, which can accept a single or array of CSS files to
load. External and local paths are supported. If an HTTP URL is passed in, WordPress will load it as-is. Otherwise,
WordPress will assume the file can be found in our theme's folder. You do not need to prepend the path or generate
the URL. It'll be generated for you.
Encapsulation
Behind the scenes, WordPress performs encapsulation on your CSS code. This feature only applies to styles added to
the Gutenberg editor, not the frontend. Encapsulation is the idea of isolating your styles to the blocks inserted into
the template. This is to prevent your styles from affecting the entire Gutenberg editor.
WordPress achieves this by adding the .editor-styles-wrapper select to your styles. For example, this:
.example{
color: red;
}
Changes to this:
.editor-styles-wrapper .example {
color: red;
}
In addition, the paragraph block was modified to remove the margins. The following class was added: !mt-0
Up next, we added the Site Title block with the following changes:
Link color set to Udemy Primary
Font size set to 1.875rem
Appearance set to bold
Line height set to 2.25
Afterward, the Search block was added with the following changes:
Lastly, an HTML block was added for handling the login and cart links. Unfortunately, it's not possible to recreate
every UI element with WordPress's blocks. In these cases, you can resort to the HTML block or create a custom block.
In the future, we are going to create a custom block, but for now, we'll stick to HTML blocks.
For this section of the header, we added everything under the <!-- Header Tools --> HTML comment.
To quickly resolve these issues, we updated the add_editor_style() function in the u_setup_theme()
function to the following:
add_editor_style([
'https://fonts.googleapis.com/css2?
family=Pacifico&family=Rubik:wght@300;400;500;700&display=swap',
'assets/bootstrap-icons/bootstrap-icons.css',
'assets/public/index.css',
'assets/editor.css'
]);
The editor.css file does not exist in the assets directory. You can find a copy of it in the resource section below.
Resources
Editor CSS
Afterward, we added a Navigation block with five dummy links to random pages. The following settings were
applied:
After making those changes, we transferred the blocks into the parts/header.html file. In total, there should be three
root blocks with children blocks in each of them.
Lecture 68: Tailwind
Hey everyone! Some of you have asked me if I use a CSS framework for creating templates. It just so happens that I
do! I use Tailwind for creating static templates before transforming them into a block template. You can check out
Tailwind here: https://tailwindcss.com/
It’s not necessary to know Tailwind. As long as you have a good grasp of HTML and CSS, the static template should
be easy to understand with the developer tools.
Two Columns
70/30 Width
Applied the container !mx-auto classes
Top and bottom margins set to 4rem
Along with the Query Loop block, a Post Template block will be added. This block will be responsible for displaying
the template of each post requested by the query.
By default, the query will be performed by the block. However, behind the scenes, WordPress will always perform a
query on every page request. The URL is scanned by WordPress to determine what posts should be displayed. We
can configure the Query Loop block to use the query from the page instead of creating another query.
Resources
Query Loop Block
Column
Post Featured Image
Row
Post Tags
Post Comment Count
Post Excerpt
Post Author
Enable show avatar option
Set image size to 24x24
Set the text color to Udemy Gray 700
Set the bottom margin to 0.5rem
Set padding on all sides to 0
Applied the post-author class
Post Title
Enable make title a link option
Set the text color to Udemy Gray 700
Set the font size set to 1.25rem
Set the appearance to Bold
Set the line height to 1.75
Set the bottom margin to 0.5rem
Post Date
Enable link to post option
Set the link color to Udemy Gray 500
Row
Set the justification to space between
Disable the wrap option
Set the text color to Udemy Gray 500
Set the font size to 0.875rem
Set the bottom padding to 1rem
Post Tag
Set the link color to Udemy Gray 500
Post Excerpt
Set the font size to 0.875rem
Columns
Applied the following classes: border-b
Set the border color to Udemy Gray 200
Bottom margin and padding set to 2.5rem
Second Column
Applied the following class: !mt-0
Resources
Boostrap Icons
In addition, the Pagination block acts as a wrapper for the actual links. The actual links are rendered with the
Previous Page, Page Numbers, and Next Page links. For this example, we removed the Page Numbers block to be
consistent with our theme. As for the other two classes, we applied the following classes to add a hover effect:
rounded-md py-2 px-4 block transition-all hover:bg-gray-100
Heading
Set the font size to 1.25rem
Set the appearance to Medium
Set the bottom margin to 1.25rem
Add Text: Blog Tags
A duplicate heading was added with the same settings, but the text says: Blog Categories
Tag Clouds
Set the number of tags to 15
Applied a class called sidebar-tags
Custom HTML
The contents of this block was the <div> tag under the comment that says Highly Rated Posts
Categories
Enable the Show only top level categories option
Applied a class called sidebar-categories
Lecture 75: Exercise: Footer Blocks
In this lecture, we worked on the footer section of the template as an exercise. To save time, I've provided the
completed block, which you can find in the resources section. Check it out for the completed code.
Resources
Footer Template
1. front-page.html – Used for both “your latest posts” or “a static page” as set in the front page displays
section of Settings → Reading.
2. home.html – If WordPress cannot find front-page.html and “your latest posts” is set in the front page
displays section, it will look for home.html. Additionally, WordPress will look for this file when the posts
page is set in the front page displays section.
3. page.html – When “front page” is set in the front page displays section.
4. index.html – When “your latest posts” is set in the front page displays section but home.html does not
exist, or when the front page is set, but page.html does not exist.
Note: I've replaced the .php extension with the .html extension. While the documentation shows examples with PHP,
the same rules apply to block themes.
By default, all pages will load the index.html file if an appropriate template doesn't exist. In most cases, you won't
need to define a template for every page. Only define what's necessary. For the rest of this section, we're going to
explore the most common templates to create.
Resources
Template Hierarchy
According to the documentation, WordPress will search for the following templates in this order.
1. 404.html
2. index.html
Resources
404 Template
1. category-{slug}.html – If the category’s slug is news, WordPress will look for category-news.html.
2. category-{id}.html – If the category’s ID is 6, WordPress will look for category-6.html.
3. category.html
4. archive.html
5. index.html
In some cases, you may see a portion of the URL wrapped with curly brackets. The brackets indicate a placeholder.
For example, the category-{slug}.html has a placeholder for the slug of a category. This placeholder must be
replaced.
We decided to go with the category.html template to act as a generic template for all categories. The category
template is nearly identical to the index template with the addition of the page header. We added the following
block above the columns.
1. search.html
2. index.html
We went with the search.html option. The search template is identical to the index template with the addition of a
search form. We added this form with an HTML block. Here's the following block:
1. single-{post-type}-{slug}.html – (Since 4.4) First, WordPress looks for a template for the specific post. For
example, if the post type is a product and the post slug is dmc-12, WordPress would look for single-
product-dmc-12.html.
2. single-{post-type}.html – If the post type is product, WordPress would look for single-product.html.
3. single.html – WordPress then falls back to single.html.
4. singular.html – Then it falls back to singular.html.
5. index.html – Finally, as mentioned above, WordPress ultimately falls back to index.html.
For our theme, we went with the single.html template to act as a generic template for all posts. As a base, we used
the index template. We've made several modifications to the template.
Row
This block will contain the Post Author, Post Date, and Post Comment Count blocks
Set the block spacing to 1rem
Post Author
Removed Margins
Post Title
Set the font size to 2.25rem
Post Tags
Added a class called post-content-tags
Previous Post
Applied the following classes: basis-0 grow max-w-full py-5 px-3 transition-all hover:bg-gray-100 text-
center border-r border-r-gray-200
Next Post
Applied the following classes: basis-0 grow max-w-full py-5 px-3 transition-all hover:bg-gray-100 text-
center !mt-0
Heading
Set the font size to 1.5rem
Set the appearance to Medium
Post Comments
Add a class called comments-section
1. custom template file – The page template assigned to the page. See get_page_templates().
2. page-{slug}.html – If the page slug is recent-news, WordPress will look to use page-recent-news.html.
3. page-{id}.html – If the page ID is 6, WordPress will look to use page-6.html.
4. page.html
5. singular.html
6. index.html
For our theme, we went with the page.html template file that'll act as a generic template for all pages. The page
template is nearly identical to the index template with the following blocks removed:
{
"customTemplates": [
{
"name": "full-width-page",
"title": "Full Width Page",
"postTypes": ["page"]
}
]
}
The customTemplates property is an array of objects where each object represents a custom template. Each
object can have the following properties.
We made a replica of the single template into a file called full-width-page.html. We modified the template by
updating the number of columns in the Columns block to 1 column.
Templates can be applied by editing a specific page. On the sidebar, there will be a panel called Templates with a
dropdown of custom templates in the theme.
Section 7: JavaScript and React Fundamentals
In this section, we learned about the JavaScript programming language and React library for building interactive
interfaces on the web.
Another area to run JavaScript is to use the Snippets section under the Sources Panel. Unlike the console, snippets
are files that are temporarily stored in the browser, not our project, that can be executed. This allows us to write
more complex code.
alert("Hello world!");
This will make a popup appear with the text Hello World .
Functions
JavaScript supports functions. The idea of a function is to allow developers to write reusable blocks of code. In
JavaScript, there are three types of functions
As opposed to PHP, JavaScript can run on the desktop or mobile device. It's not restricted to the browser. Each of
these locations is considered an environment. Most environments will define additional functions that will be
available from our script. In the above example, we're using a function called alert() , which is available in most
browsers.
Strings
JavaScript supports strings, which can be written with single or double-quotes. Either style is suitable. The program
doesn't care. Strings allow us to store random text such as names, addresses, or content.
Semicolons
Unlike PHP, semicolons are completely optional in JavaScript. JavaScript is more than capable of ending a line of
code for you. If we were to take the previous code example without ending it in a semicolon, it would look like this:
alert("Hello world!")
String
Number
Boolean
Null
Undefined
Objects
There are more data types, but these are the most common data types and the ones we'll be working with
throughout the course. Like PHP, JavaScript is a dynamically typed language. Therefore, we don't have to explicitly
assign data types to our variables. This process is handled for us behind the scenes.
Retyping the let keyword is not necessary. It's only necessary for initializing variables.
const age = 28
alert(`My age is ${age}`)
A template literal can be written with a backtick character. Inside the string, we can inject values by adding a
placeholder with the following syntax: ${} . Inside the curly brackets, you can enter a valid expression.
Expressions are lines of code that evaluate to a value. Refer to the video in the resource section for a further
explanation
String Concatenation
Another syntax is available for adding values to strings called string concatenation. Same goal, different syntax.
Ultimately, most developers prefer template literals over string concatenation. Just in case, here's what string
concatenation looks like:
const age = 28
alert('My age is ' + age)
The + operator is not limited to adding numbers. It can be used for adding strings together too.
Resources
Expressions
sum(10, 20)
Functions are defined with the function keyword followed by the name of the function, argument list, and body.
Multiple arguments can be added by separating them with a comma.
Lastly, we can call a function by writing its name followed by an argument list.
alert(states[1])
Arrays are zero-based indexed. This means the first item in the array can be accessed with the index of 0 . The
second item can be accessed with an index of 1 . So on and so forth.
const checkingAccount = {
name: 'John',
balance: 1000
}
alert(checkingAccount.balance)
Objects are created with {} syntax. The syntax is very similar to JSON, with a few exceptions. We don't need to
surround property names with quotes. They're completely optional. We can access an object's properties with dot
syntax.
const checkingAccount = {
name: 'John',
balance: 1000,
withdraw: function(amount) {
checkingAccount.balance = checkingAccount.balance - amount
}
}
checkingAccount.withdraw(200)
alert(checkingAccount.balance)
There's a shorthand way of writing functions by omitting the function keyword like so:
const checkingAccount = {
name: 'John',
balance: 1000,
withdraw(amount) {
checkingAccount.balance = checkingAccount.balance - amount
}
}
Another useful feature for writing less code is the this keyword. The this keyword will always point to the
object it's used in. This way, you don't have to type the full name of the object. However, it can only be used in
objects. You can't use it outside of an object. Here's an example:
const checkingAccount = {
name: 'John',
balance: 1000,
withdraw(amount) {
this.balance = this.balance - amount
}
}
Another term used for functions defined inside an object is called a method. The words function and method can be
interchangeable, but there is a difference.
A better solution is to use the console object, which provides methods for adding messages to the console. The
most common method for logging messages is the console.log() method. Here's an example:
console.log("Hello World!")
Resources
Console
<script>
console.log('Hello world!')
</script>
Both options are viable, but it's recommended to keep your JavaScript code separate from your HTML code for
maintainability. Regardless, you should always load a script at the bottom of the document. Problems can arise by
loading your JavaScript first. If you were to load a script early, your elements may not be ready by the time you need
to access them. Therefore, you should always load your scripts last unless you really need to load them early.
The entire tree can be found under the document object, which represents the document. If you would like to
select a specific element, you can use the querySelector() function.
document.querySelector('h1')
This function accepts a valid CSS query, which means you can select elements by classes, IDs, or other attributes. You
can even use nested selectors like so:
document.querySelector('ul li')
In some cases, you may want to select multiple elements. You can use the document.querySelectorAll()
function to do so.
document.querySelectorAll('ul li')
This function will return an array of elements. Overall, these two methods should cover all your use cases. There are
alternative methods like the getElementById() , getElementsByTagName() , and
getElementsByClassName() functions. As their names suggest, you can grab elements by their ID, tag name, or
class name.
Reading elements is not your only option. You can change an element's properties too. Here's an example of
changing an element's color property.
li[1].style.color = 'red'
Check out the resource section for a complete list of properties and methods available for objects selected by the
DOM API.
Resources
DOM API
Here's an example:
A couple of things worth mentioning. Firstly, the innerHTML property can be found on all DOM objects. It
contains the inner contents of an element, including children elements.
Secondly, we're using the strict equal operator to compare two values but also comparing their data types. Data
types can be tricky in JavaScript. In some cases, two values may get matched even if they're not necessarily a
complete match. To avoid errors like these, consider using the strict equal operator over the equal operator. For a
list of complete comparison operators, check out the resource section of this lecture.
Thirdly, we're chaining the conditional statements in a specific order. The if statement must come first, followed
by a series of else if statements. Lastly, the else statement must be last. You can have as many conditions as
you'd like. It's optional to add an else if statement if you only plan on checking for a single condition.
Resources
Comparison Operators
Check out the video for a complete breakdown of scope with examples.
Arrow functions are anonymous. So, you'll have to assign it to a variable or pass it into another function. Unlike
regular functions, adding {} is optional. Arrow functions can be written on the same line. An arrow function with
multiple lines would require the {} characters like so:
const hello = () => {
console.log('hello world')
}
Parameters
Arrow functions can have parameters just like regular functions. Multiple parameters can be added by separating
them with a comma. In some cases, you can add/remove the parentheses based on the number of parameters in
your functions.
Scope
Arrow functions do not have a scope. Instead, they inherit their parent's scope. Take the following example:
const foo = {
num: 10,
logNum() {
console.log(this.num)
}
}
This is an object with a regular function called logNum . It'll log the num property without a problem. If we used
an arrow function, things would be different.
const foo = {
num: 10,
logNum: () => {
console.log(this.num)
}
}
This wouldn't work because the this keyword no longer references the object. Therefore, we would receive
undefined from JavaScript. If we would like to grab the num property, we would need to reference it through
the foo property.
const foo = {
num: 10,
logNum: () => {
console.log(foo.num)
}
}
Overall, arrow functions are considered popular because they're easier to read and they can borrow from the parent
scope. It might not seem like it, but these features are incredibly helpful in larger applications. Once we get into
Gutenberg, it'll become clearer as to why this feature is popular.
Lecture 99: Destructuring
In this lecture, we learned about destructuring for quickly making our code readable by extracting properties from an
object. Oftentimes, we may need to access a property deeply nested inside an object. It would be annoying to
constantly type out the full path to the object. One solution would be to assign a property to a variable like so:
const foo = {
num: 10,
logNum: () => {
console.log(foo.num)
}
}
However, if you would like to extract more properties, you would need to create a variable for each one. This can
become tedious. Luckily, we can use destructuring to shorten the process. The previous example can be modified to
the following:
The variable name is wrapped with {} where the name of the properties are listed inside. The value for the variable
is where the properties can be found. Additional properties can be destructured by separating them with commands.
The first name gets mapped to the first item in the array. If we had a second name, the second item from the array
would get mapped to it. So on and so forth.
Overall, React is a great library to learn. On top of that, React is supported by WordPress. One of the benefits of using
React is that there are tools for optimizing your code. React will help make sure your code is ready for production.
For this course, we're going to learn about React along with the tools necessary for running React apps.
Resources
React
We dived deeper into the package.json file. This file will contain various settings for configuring your project. Most
importantly, you'll find a list of dependencies. You can download packages created by other developers to expose
features to your application.
There are objects for listing dependencies. They're dependencies and devDependencies . The
dependencies object should contain packages that should be shipped with your project for production. Whereas
the devDependencies should contain a list of packages that should be for development.
There's another property called scripts that we can safely ignore and will be revisiting in a future section.
Resources
React Stackblitz
Package File Docs
NPM
We're using the import statement to include code from external files. This is followed by a name to reference the
functions and variables exported by a file. Lastly, we added the from keyword to specify the location. In this
example, we're specifying the package name. Node will be intelligent enough to understand this package is installed
with your machine. It doesn't need a full path.
This won't insert the element into the document. That would be next step. First, we must import the ReactDOM
object like so:
React supports various platforms like desktop and mobile apps. The core React package handles creating and
updating elements. Whereas the ReactDOM package will handle inserting and updating elements in the DOM. We
can run the render() function to add the element to the DOM like so:
const app = document.querySelector('#app')
ReactDOM.render(h1, app)
ReactDOM.render(div, app)
function Page() {
return React.createElement('div', null, [
React.createElement('h1', null, `Hello ${new Date().toLocaleString()}`),
React.createElement('p', null, 'This is a paragraph'),
React.createElement('p', null, 'This is another paragraph'),
]);
}
Two things worth noting. Firstly, we're choosing to use a function to prevent memory leaks from happening. A
memory leak can happen whenever a variable occupies memory when it's no longer needed anymore. Memory leaks
can cause a visitor's machine to become sluggish.
Secondly, we're using a function called Date() , which is defined by the JavaScript language to return an object
that has information on the current. From this object, we're calling the toLocaleString() to return a human-
readable time. This time is inserted into the h1 element.
setInterval(() => {
ReactDOM.render(Page(), app);
}, 1000);
We're using the setInterval() function to run a function every second. The arguments are the following:
1. The function to execute.
2. The interval, which is measured in milliseconds.
In this example, we're running the Page() function every second, which is responsible for displaying a timer. Even
though the Page() function returns the entire document, only the h1 element gets updated. React is smart
enough to only update what's necessary. Thus, saving us from having to manage what needs to be updated.
function Page() {
return (
<>
<h1 className="orange">Hello world!</h1>
<p>This is a paragraph</p>
<p>This is another paragraph</p>
</>
);
}
For demonstration purposes, we created a style.css file with the following contents:
.orange {
color: orange;
}
At the top of the index.js file, we imported the CSS with the following code:
import './style.css'
Notice how we're importing the CSS file. Normally, JavaScript does not support CSS file imports. Thanks to Webpack,
we get the benefit of being able to import CSS.
function Header() {
const clock = Date().toLocaleString();
return (
<h1 className="orange">
Hello World! {clock}
</h1>
);
}
In this example, we are creating another component called Header . Inside this component, we are injecting a
variable into our HTML by using {} . Inside these brackets, we can add a valid JavaScript expression. The value
evaluated from the expression will appear in the contents of the template.
Lastly, we updated the Page component by swapping the <h1> tag with our <Header> component:
function Page() {
return (
<>
<Header />
<p>This is a paragraph</p>
<p>This is another paragraph</p>
</>
);
}
In the Page component, we updated our use of the Header component by passing on a name:
function Page() {
return (
<>
<Header name="John" />
<p>This is a paragraph</p>
<p>This is another paragraph</p>
</>
);
}
Custom prop names can be whatever you want. Props can be accepted by adding the props parameter to your
component's function like so:
function Header(props) {
const clock = Date().toLocaleString();
return (
<h1 className="orange">
Hello {props.name}! {clock}
</h1>
);
}
In some cases, you may want to pass on dynamic data. You can do so by setting the value of a prop to a pair of curly
brackets like so:
function Page() {
const name = "John"
return (
<>
<Header name={name} />
<p>This is a paragraph</p>
<p>This is another paragraph</p>
</>
);
}
A couple of things worth mentioning. Firstly, we importing React since Webpack will convert JSX elements into
the React.createElement() function. Without this function, our code wouldn't work. So, we need to import it
even though we're not writing it out in our function.
Secondly, we're creating a variable called age . This variable will never be exported. Data must be explicitly
exported with the export keyword like the function. Our function has been exported under the default
namespace. A namespace is a programming concept where data is exported under a specific location.
The default namespace is a location that allows you to export data without assigning a name. This means you can
export an anonymous function. After exporting the function, we can import it like so:
Local files can be imported by providing a local path. We're not finished yet. The Home component should be
exported from a separate file.
return (
<h1 className="orange">
Hello {props.name}. {clock}
</h1>
);
}
In this example, we're creating a named export. We can import this file by using the following syntax:
By using a named export, we must import the data by its name. By splitting our code into separate files, our project
will be more maintainable and easier to read.
Rather than updating the heading from the index.js file, we should update it from the Header component. In the
index.js file, remove the setInterval() function:
const app = document.querySelector('#root');
setInterval(() => {
setClock(Date().toLocaleString());
}, 1000);
console.log('Component updated');
return (
<h1 className="orange">
Hello {props.name}. {clock}
</h1>
);
}
We're using the useState() function to add state to our component. State refers to the data of an application. Its
data that React should care about and update the component if the state changes. This function has one argument,
which is the default value.
It'll return an array where the first item is the current value and the second value is a function to update the state. It's
considered good practice to destructure the items for readability.
From within our component, we're updating the state a combination of the setInterval() and setClock()
functions. React will automatically rerun the function to render the latest template to the page.
We created a Counter component for this example. This component was added to the Page component.
return (
<>
<Header name={name} />
<p>This is a paragraph</p>
<p>This is another paragraph</p>
<Counter />
</>
);
}
function handleClick(event) {
event.preventDefault();
return (
<a href="#" onClick={handleClick}>
Count: {count}
</a>
);
}
An event can be listened to by adding an attribute to the element that should have the event. You can refer to the
resource section for a complete list of events. In this example, we're using the onClick event.
Next, we're setting the event to a function called handleClick . Take note that we're not adding the ()
characters after the function name. We want to pass on a reference to the event, not call the function. React will be
able to call our function on our behalf.
The handleClick() function is performing two actions. Firstly, it's accepting the event object, which comes
with every event. It'll contain information on the current event. We're using it to prevent the default behavior, which
is to update the URL in the address bar.
Secondly, we're updating the current click count by using the setCount() function. We have the option of
passing in a value, but we can also pass in a function that'll be provided the previous value. The return value of this
function will be the new value of the count variable.
Resources
JavaScript Events
React Events
Event Object
In addition to the Local Storage, we're using a function called useEffect() , which will allow us to run a function
whenever data changes from within our component. It has two arguments, a function to call when data changes and
a list of dependencies.
React.useEffect(() => {
localStorage.setItem('count', count);
}, [count]);
In the above example, we're watching the count variable for updates. If it's updated, we're storing an item in the
Local Storage API with the localStorage.setItem() function. The localStorage exposes to the browser's
local storage with various methods. One of the methods is called setItem() , which has two arguments. The first
argument is the name of the item, and the second argument is the value.
Afterward, we created another useEffect() function. This time, we're not supplying a list of dependencies. This
tells React to execute the function immediately after the component has been rendered:
React.useEffect(() => {
if (localStorage.getItem('count')) {
setCount(parseInt(localStorage.getItem('count')));
}
}, []);
In the above example, we're grabbing the count item from local storage by using the
localStorage.getItem() method. If the item exists, we'll update the state with the setCount() method.
Before doing so, we're wrapping the value with the parseInt() function to convert the data type to an integer.
Otherwise, we may receive unexpected behavior if we're working with the wrong data type.
Resources
Local Storage
You can open the command line by searching for a program called Powershell on a Windows machine. If you're on a
Mac/Linx, you can search for a program called Terminal.
There are dozens of commands available. Luckily, it's not required to be a master of the command line. You can get
away with the following commands:
pwd - Short for Present Working Directory. This command will output the full path you're currently in.
ls - Short for List. This command will output a full list of files and folders that are in the current directory.
cd - Short for Change Directory. This command will change the current directory. You can use two dots
( .. ) to back up a directory instead of moving into a directory.
In Visual Studio Code, you can open the command line by going to Terminal > New Terminal. By default, the
command line will point to your project directory, which can make things easier. This saves you time from moving
the command line to your project.
You should download the latest version of Node. You may find older versions, which are available for developers who
developed their apps on older versions and need support. Installing Node is like installing any other program.
After installing Node, a program will be available for running JavaScript, but it's not necessary to use this tool. It was
provided for quickly testing JavaScript on your machine. In most cases, you will want to execute code from your JS
files.
Let's say we had a file called index.js with the following code:
console.log('test')
We can execute this file by running the following command: node index
By installing Node, a new command will become available called node , which will tell Node to run a JavaScript file.
After this command, we can provide the name of the file without the extension. Node will assume the file is already a
JS file.
The most important step to remember is that the file and command must be in the same directory. If your command
line is in a different directory than your file, the command will not work. Luckily, if you're using VSC, the editor should
point to your project's directory already.
Node can do so many things, from starting a server to interacting with a file system. In our case, we're going to be
installing tools for optimizing our JavaScript codebase.
Resources
Node.js
Section 8: Block Development Fundamentals
In this section, we created our first block for the Gutenberg editor. Along the way, we created a plugin with tooling
for proper development.
The PHP file can be called whatever you want. In most cases, developers will either use index.php or name the
plugin file after the name of the plugin. For our case, we'll use index.php. Inside this file, we added the following
code:
<?php
/**
* Plugin Name: Udemy Plus
* Plugin URI: https://udemy.com
* Description: A plugin for adding blocks to a theme.
* Version: 1.0.0
* Requires at least: 5.9
* Requires PHP: 7.2
* Author: Udemy
* Author URI: https://udemy.com
* Text Domain: udemy-plus
* Domain Path: /languages
*/
The file header will be scanned and extracted from the main plugin file. This information is displayed to the user on
the plugin page. We can add the following headers to our file:
Plugin Name: The name of your plugin, which will be displayed in the Plugins list in the WordPress Admin.
Plugin URI: The home page of the plugin, which should be a unique URL, preferably on your own website.
This must be unique to your plugin. You cannot use a WordPress.org URL here.
Description: A short description of the plugin, as displayed in the Plugins section in the WordPress Admin.
Keep this description to fewer than 140 characters.
Version: The current version number of the plugin, such as 1.0 or 1.0.3.
Requires at least: The lowest WordPress version that the plugin will work on.
Requires PHP: The minimum required PHP version.
Author: The name of the plugin author. Multiple authors may be listed using commas.
Author URI: The author’s website or profile on another website, such as WordPress.org.
License: The short name (slug) of the plugin’s license (e.g., GPLv2). More information about licensing can be
found in the WordPress.org guidelines.
License URI: A link to the full text of the license (e.g., https://www.gnu.org/licenses/gpl-2.0.html).
Text Domain: The gettext text domain of the plugin. More information can be found in the Text Domain
section of the How to Internationalize your Plugin page.
Domain Path: The domain path lets WordPress know where to find the translations. More information can
be found in the Domain Path section of the How to Internationalize your Plugin page.
Network: Whether the plugin can only be activated network-wide. It can only be set to true and should be
left out when not needed.
Update URI: Allows third-party plugins to avoid accidentally being overwritten with an update of a plugin
of a similar name from the WordPress.org Plugin Directory.
Resources
Header Requirements
The most common method of checking if a file is being visited directly is by using a function called
function_exists() , which is defined by PHP. It'll check if a function has been defined beforehand. Here's how
we used it in the index.php file.
if (!function_exists('add_action')) {
echo "Seems like you stumbled here by accident. 😛";
exit;
}
Firstly, we're checking if the add_action function was defined. This function is defined by WordPress. Since
WordPress loads first before our plugins, we can safely assume that the file is not being visited directly if this function
is defined. If it isn't available, we exit the script with the exit statement.
Inside our condition, we're using the ! (not) operator. This operator will check if a condition is false rather than
true .
npm init -y
The npm command is available after installing NodeJS. This command allows us to interact with NPM with various
subcommands. One of the commands is called init , which will create a package.json file. This file is required for
installing dependencies as it'll keep track of dependencies for your project. We're adding the -y flag to skip the
question process. NPM will fill your package.json file with default values.
We're using another subcommand called install , which will install a package from the NPM registry. After this
command, we must specify the name of the package that we'd like to install. In some cases, the name of a package is
the author name followed by the name of the package.
Lastly, the --save-dev flag will install the package as a development dependency. During the installation, NPM
will create a folder called node_modules. You may find dozens of packages when we only installed a single package.
Typically, packages will install additional packages. It's rare for a package to be installed. A comprehensive list of
packages can be found in the package-lock.json file. You will never need to edit this file or the node_modules
folder.
Resources
WordPress Scripts
{
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
}
}
The format for the command is the name of the command as the property name and the command itself as the
value. We can execute commands by running npm run <command name here> . So, for example, we can run the
test command with the following command: npm run test
The WordPress scripts package provides a convenient list of commands that we can add to our package file. They're
the following:
{
"scripts": {
"build": "wp-scripts build",
"check-engines": "wp-scripts check-engines",
"check-licenses": "wp-scripts check-licenses",
"format": "wp-scripts format",
"lint:css": "wp-scripts lint-style",
"lint:js": "wp-scripts lint-js",
"lint:md:docs": "wp-scripts lint-md-docs",
"lint:md:js": "wp-scripts lint-md-js",
"lint:pkg-json": "wp-scripts lint-pkg-json",
"packages-update": "wp-scripts packages-update",
"plugin-zip": "wp-scripts plugin-zip",
"start": "wp-scripts start",
"test:e2e": "wp-scripts test-e2e",
"test:unit": "wp-scripts test-unit-js"
}
}
From this long list of commands, we ran the packages-update command. This command will update your
packages to the latest version, including the scripts package.
console.log("Test")
Behind the scenes, the scripts package uses Webpack for processing our code. We won't have to configure Webpack
since it's already configured for WordPress. We can create a bundle by using the npm run start command.
This command should create a directory called build. There's another command that produces the same files called
npm run build . The difference between the commands is that the start command will watch our files for
changes and update the bundle, whereas the build command will produce the bundle for production.
Resources
Webpack
{
"$schema":
"https://raw.githubusercontent.com/WordPress/gutenberg/trunk/schemas/json/block.json",
"apiVersion": 2,
"name": "udemy-plus/fancy-header",
"title": "Fancy Header",
"category": "text",
"icon": "star-filled",
"description": "Adds a header with an underline effect",
"keywords": [ "header", "hover", "underline" ],
"version": "1.0.0",
"textdomain": "udemy-plus"
}
Resources
Metadata
Dashicons
// Setup
define('UP_PLUGIN_DIR', plugin_dir_path(__FILE__));
// Includes
include(UP_PLUGIN_DIR . 'includes/register-blocks.php');
// Hooks
add_action('init', 'up_register_blocks');
Firstly, we're defining a constant called UP_PLUGIN_DIR for providing a path relative to our plugin. We're using a
function called plugin_dir_path() to help us grab the path to our plugin. It has one argument, which is the
main plugin file. It needs this information since multiple plugins can be activated, and WordPress doesn't know
where to find our plugin.
The __FILE__ constant is defined by the PHP language. This constant's value is unique to each file that points to
the current file its being used.
Afterward, we're including the file with the include() function. We're appending two strings with the
concatenation operator ( . ).
Lastly, we're running a function called up_register_blocks when the init hook is triggered. We should
register our blocks as early as possible. This hook is triggered once WordPress is initialized.
We created a folder called includes. Not required to create this folder, but it can be useful to outsource a plugin's
logic into a separate folder for organization reasons. Inside this folder, we created a file called register-blocks.php
with the following code:
function up_register_blocks() {
register_block_type(
UP_PLUGIN_DIR . 'build/block.json'
);
}
We are using the register_block_type() function, which has one argument. It's the path to the block.json
file. From there, WordPress will use the information inside the block.json file to register a block.
Resources
register_block_type() Function
For this example, we used the editorScript property since we don't need to load the script on the frontend. In
the block.json file, we added the following code:
{
"editorScript": "file:./index.js"
}
The value must start with the word file . This will tell WordPress that the script can be found locally. Following this
word, we must provide a path relative to the block.json file. In our case, the block.json and index.js files sit in the
same directory.
Check out the resource section for a link to the complete list of packages that are available. All of them can be found
in the packages directory.
registerBlockType(block.name, {
edit() {
return <p>Fancy Header</p>;
}
});
We're calling the registerBlockType() function. It has two arguments. The first argument is the name of the
block. In this example, we're importing our block.json file and using it as an object in our file. We could hardcode
the name, but using the block file as a single source of truth will reduce the likelihood of a typo.
The second argument is an object of configuration settings. You can add the same settings as the block.json file.
However, its recommended to outsource settings to the block file. WordPress provides this option if you need to use
dynamic values since JSON is not a programming language.
Inside this object, we passed in a function called edit . This function will be treated as a component, so it should
return JSX. In this example, we're returning a <p></p> element. If you were to add this block to the Gutenberg
editor, a paragraph element should appear.
Resources
Gutenberg Repo
registerBlockType() Function
Lecture 122: The RichText Component
In this lecture, we got started with the RichText component to allow clients to edit a block's content. WordPress
defines dozens of components to help us build our block. They're completely optional to use but incredibly
beneficial for rapidly building blocks.
This component can be found under the @wordpress/block-editor package. We updated our file to the
following:
registerBlockType(block.name, {
edit() {
return (
<RichText
tagName="h2"
placeholder={ __('Heading', 'udemy-plus') }
/>
);
}
});
In addition to the RichText component, we imported the __() function from the @wordpress/i18n
package. This package will supply us with functions for translating our plugin. The value returned by this function is a
translated string.
Inside our edit() function, we're returning the <RichText /> component. We're adding two properties called
tagName and placeholder . The tagName property will change the tag surrounding the editable text. By
default, this property will be set to div . In our example, we're changing it to an h2 tag since we're developing a
header block.
As for the placeholder property, it functions the same as the placeholder attribute for the <input>
element. It'll output temporary text if the component is missing a value. For the value, we're using the __()
function, which will translate a string. The first argument is the text to translate, and the second argument is the text-
domain.
Resources
RichText Component
{
"attributes": {
"content": {
"type": "string"
}
}
}
The attributes property is a list of data our block will store. The property name will be the name of the attribute,
while the value will be settings for the attribute. In this example, we're setting the data type. The following data types
are supported: null , boolean , object , array , string , integer , and number .
Next, we updated our script to use this attribute. The edit() function has been modified to the following:
return (
<RichText
tagName="h2"
placeholder={ __('Heading', 'udemy-plus') }
value={ content }
onChange={ newVal => setAttributes({ content: newVal }) }
/>
);
}
Since we're dealing with a component, WordPress will provide our component with various properties and methods
via the props. We're destructuring this parameter to grab the attributes and setAttributes properties. The
attributes property will be an object of our data, while the setAttributes function will help us update our
attribute.
In the <RichText /> component, we added the value prop to set the value of the component. As for
updating the attribute, we're listening for the onChange event. This event will get triggered when the user types in
the component. We are passing in a function to accept the new value and using it to update the content
attribute.
Resources
Attributes
save({ attributes }) {
const { content } = attributes
return (
<RichText.Content
tagName="h2"
value={ content }
/>
);
}
From the above example, we're doing the same thing as our edit() function with one exception. We're using the
<RichText.Content> component. This component will render content but without the tools for modifying the
content. It's a barebones version of the <RichText> component.
There are pros and cons to each approach, which we'll talk about after exploring both options.
console.log(complete);
The spread operator is written with three dots( ... ). The array's values get spread into the current array. In this
example, we're merging the newlyRegistered and preregistered arrays into the complete array.
The order does impact how values get arranged. For example, we can swap the order of values by swapping the
variable like so:
const ticket = {
name: 'Luis',
price: 20,
};
const newTicket = {
...ticket,
name: 'John',
};
console.log(newTicket);
In this example, the ticket object's properties get merged into the newTicket object. The order does matter.
JavaScript will prioritize the last property that has a duplicate as the value to add to the object.
Resources
JavaScript Playground
Next, we can use this function by spreading the return value into the root element of our block. In this case, we
spread the properties on the <RichText> component in both the edit() and save() functions.
registerBlockType(block.name, {
edit({ attributes, setAttributes }) {
const { content } = attributes;
const blockProps = useBlockProps()
return (
<RichText
{ ...blockProps }
tagName="h2"
placeholder={ __('Heading', 'udemy-plus') }
value={ content }
onChange={ newVal => setAttributes({ content: newVal }) }
/>
);
},
save({ attributes }) {
const { content } = attributes
const blockProps = useBlockProps.save();
return (
<RichText.Content
{ ...blockProps }
tagName="h2"
value={ content }
/>
);
}
});
For the save() function, we're using a variation of the function called useBlockProps.save() . This function
will add props that are only necessary for the front end. It will not include props that would allow the visitor to
modify the block.
import './main.css';
Normally, JavaScript does not support CSS imports. However, by using Webpack, we can import CSS. It'll be extracted
into a separate file that is included with our build. Inside the build directory, a file called index.css will be created.
This file can be enqueued with the block.json file.
There are two properties for enqueueing files called style and editorStyle. The style property will enqueue a
stylesheet on the front end and editor. Whereas the editorStyle property will enqueue a file on the editor only. For
this example, we used the style property.
{
"style": "file:./index.css"
}
In addition, we passed in an object to the useBlockProps function to add a custom class. In the edit() function,
we changed it to the following:
Resources
Fancy Header CSS
We fixed our issue by surrounding the <RichText /> component like so:
We moved the blockProps object from the <RichText /> component to the <div> tag. Lastly, we added
the className property to the <RichText /> component. In addition, the useBlockProps function had to
be updated since we're not applying the fancy-header class to the root element.
This was only updated in the edit() function and not the save() function.
<RichText
className="fancy-header"
tagName="h2"
placeholder={ __('Heading', 'udemy-plus') }
value={ content }
onChange={ newVal => setAttributes({ content: newVal }) }
allowedFormats={ [ 'core/bold', 'core/italic' ] }
/>
core/bold
core/code
core/image
core/italic
core/keyboard
core/link
core/strikethrough
core/subscript
core/text-color
core/underline
return (
<>
<InspectorControls>
<PanelBody title={ __('Colors', 'udemy-plus') }>
Test
</PanelBody>
</InspectorControls>
<div { ...blockProps }>
<RichText
className="fancy-header"
tagName="h2"
placeholder={ __('Heading', 'udemy-plus') }
value={ content }
onChange={ newVal => setAttributes({ content: newVal }) }
allowedFormats={ [ 'core/bold', 'core/italic' ] }
/>
</div>
</>
);
}
A couple of things worth noting here. Firstly, we're wrapping the <InspectorControls /> component and
<div> tag with a fragment since only one root parent element is allowed. Secondly, we're placing the
<InspectorControls /> component outside of our block. This is so that WordPress grabs the correct content.
Inside this component, we are outputting a <PanelBody /> component, which will output a panel on the sidebar
that's toggleable. A title can be added by adding the title property. Lastly, we're inserting content inside this
component to render raw text. Overall, WordPress should be able to add this component to the sidebar without us
doing anything else.
To begin, we updated the block.json file to add an attribute for storing the color selected by the client. We called the
atrribute underline_color .
{
"attributes": {
"underline_color": {
"type": "string",
"default": "#F87171"
}
}
}
The type property is being set to string . In addition, we are setting a default value by adding the default
property. After updating the block file, we updated the edit() function for our block to destructure this attribute.
colors - An array of colors where each color is an object with a name and color.
value - The current value that should be selected.
onChange - An event that gets fired when a new color is selected. The function that gets called will be
provided with the new color.
Resources
Components Package
A style can be added by adding the style property. This property will be an object of CSS properties to apply to
the object where the name of the property is the CSS property, and the value is a valid CSS value. In this example, we
are setting the background-image property to a linear gradient. We also destructured the underline_color
attribute.
{
"attributes": {
"content": {
"type": "string",
"source": "html",
"selector": "h2"
}
}
}
The source property can be added to specify that the value can be found inside the block's HTML by setting this
property to html . Up next, we added the selector property to tell Gutenberg where in our HTML it can find
the value. Since blocks can be made up of several elements, we must specify exactly where to find the value. The
value can be a valid CSS selector. In this case, we're telling Gutenberg to select the h2 tag.
For our plugin, we created a blocks directory from within the src directory. In this directory, we are going to create a
folder for each block. We moved the current files into a folder called fancy-header.
foreach($blocks as $block) {
register_block_type(
UP_PLUGIN_DIR . 'build/blocks/' . $block['name']
);
}
}
We created a multidimensional array called $blocks . A multidimensional array is a fancy word for when an array
contains more arrays. Each array inside the $blocks array will represent a single block. In this example, the first
array represents the Fancy Header block. PHP arrays can have named indexes. We're not limited to numeric indexes.
We can assign a name by adding a string to represent the name followed by a => and the value.
Next, we looped through the array with the foreach keyword. This is another solution to looping through arrays.
This keyword accepts the array where each item in the array is looped through and assigned to a variable with the
as keyword. In this example, we're assigning each item to a variable called $blocks .
Lastly, we updated the register_block_type() function to load the block.json file by using the $block
array. We can reference an item with a named index by passing in a string to the [] portion. We're not providing
the block.json filename since WordPress will automatically search for a file called block.json if a directory is given.
After all that, our plugin is ready for multiple blocks. All we have to do is create a folder for that block, add a
block.json file, and update our array with the name of the folder.
Section 9: Server-Side Rendering
In this section, we started creating custom blocks that utilize server-side rendering.
Whereas server-side rendering takes place on the server. Unlike client-side rendering, an HTML comment of our
block is stored in the database as a placeholder. When a page is being requested with a block, the content is created
with a PHP function.
In most cases, client-side rendering is faster but does not allow for dynamic content. You will want to use server-side
rendering whenever your content changes from time to time. This way, clients don't need to update every post that
needs an update.
block.json
index.js
main.css
{
"$schema":
"https://raw.githubusercontent.com/WordPress/gutenberg/trunk/schemas/json/block.json",
"apiVersion": 2,
"name": "udemy-plus/search-form",
"title": "Search Form",
"category": "widgets",
"description": "Adds a search form",
"keywords": ["search form"],
"version": "1.0.0",
"textdomain": "udemy-plus",
"editorScript": "file:./index.js",
"style": "file:./index.css",
"attributes": {}
}
In addition, we created a code snippet for generating a barebones block.json file. Refer to the video of this lecture
for the entire process.
After adding the block metadata, we updated the register-blocks.php file to include this block. We updated the
$blocks array like so:
$blocks = [
['name' => 'fancy-header'],
['name' => 'search-form'] // <~ NEW BLOCK
];
Lastly, we updated the index.js file for the search form block with the following code:
registerBlockType(block.name, {
edit() {
return <p>Search Form</p>
}
})
In the resources section, I provide an SVG image to use as an icon. If you don't know what an SVG image is, that's
perfectly fine. It's an image created with code that's similar to HTML. Knowledge of SVG is not required.
We grabbed the code from the SVG image and stored it in a file called icons.js like so (The entire image was cut out
from the code snippet to prevent the PDF from becoming bloated.):
export default {
primary: <svg></svg>
}
registerBlockType(block.name, {
icon: icons.primary,
edit() {
return <p>Search Form</p>
}
})
Lastly, the icon property from the block.json file was removed since the icon was being added manually as a
property to the object passed into the registerBlockType() function.
Resources
Icon File
Lecture 138: Adding the Template
In this lecture, we added the template and imported a CSS file to our main block file like so:
registerBlockType(block.name, {
icon: icons.primary,
edit() {
const blockProps = useBlockProps()
return (
<div {...blockProps}>
<h1>Search: Your search term here</h1>
<form>
<input type="text" placeholder="Search" />
<div className="btn-wrapper">
<button type="submit">Search</button>
</div>
</form>
</div>
)
}
})
The CSS can be found in the resource section. It should be added to the main.css file. Not everything worked out of
the box. We had to add a selector for the search form to look the same as it did on the front end.
.wp-block-udemy-plus-search-form h1,
.editor-styles-wrapper .wp-block-udemy-plus-search-form h1 {
margin-bottom: 1rem;
font-size: 1.5rem;
line-height: 2rem;
font-weight: 700;
color: inherit;
}
This is normal since our blocks share CSS with the Gutenberg editor and theme.
Resources
Search Form CSS
{
"attributes": {
"bgColor": {
"type": "string",
"default": "#F87171"
},
"textColor": {
"type": "string",
"default": "#fff"
}
}
}
We're storing two colors for the background and the text color of the block. Next, we updated the index.js block file
for the Search Form block by importing the necessary components and functions:
import {
useBlockProps, InspectorControls, PanelColorSettings
} from '@wordpress/block-editor';
import { __ } from '@wordpress/i18n';
<>
<InspectorControls>
<PanelColorSettings
title={ __( 'Color Settings' ) }
colorSettings={[
{
label: __('Background Color', 'udemy-plus'),
value: bgColor,
onChange: newVal => setAttributes({ bgColor: newVal }),
},
{
label: __('Text Color', 'udemy-plus'),
value: textColor,
onChange: newVal => setAttributes({ textColor: newVal }),
},
]}
/>
</InspectorControls>
<div {...blockProps}>
<h1>Search: Your search term here</h1>
<form>
<input type="text" placeholder="Search" />
<div className="btn-wrapper">
<button type="submit">Search</button>
</div>
</form>
</div>
</>
We surrounded the template with fragments since we're using the InspectorControls component. Inside this
component, we added the <PanelColorSettings /> component. This component has two properties. The first
property is called title , which will appear in the panel's header.
The second property is an array of colors called colorSettings . Inside this array, we can add an object to
represent a color settings. The object must have the following properties:
Next, we had to apply custom styles to the <button> element on the page.
function up_register_blocks() {
$blocks = [
['name' => 'fancy-header'],
['name' => 'search-form', 'options' => [
'render_callback' => 'up_search_form_render_cb'
]]
];
foreach($blocks as $block) {
register_block_type(
UP_PLUGIN_DIR . 'build/blocks/' . $block['name'],
isset($block['options']) ? $block['options'] : []
);
}
}
In this example, I added an index called options to the Search Form block.. In this array, we added an option
called render_callback , which should contain the name of the function to run.
Next, we updated the loop by using a ternary operator in the register_block_type() function. A ternary
operator is a shorthand syntax for a conditional statement, except it'll be processed as an expression. To the right of
the ? symbol we must add a condition. In this case, we are using the isset() function to check if the
$block['options'] item is defined in the array. If it is, we are passing on this array to the function. Otherwise,
we are passing in an empty array.
After defining these functions, we created a new directory called blocks inside the includes directory. This directory
will contain our PHP functions for rendering blocks. Inside this newly created directory, we created a PHP file called
search-form.php with the following code:
function up_search_form_render_cb() {
return 'Search Form';
}
From our function, we must return a string that will serve as the content of the block. WordPress will handle calling
our function when rendering the page.
include(UP_PLUGIN_DIR . 'includes/blocks/search-form.php');
Resources
register_block_type()
We can enable this behavior by calling the ob_start() function like so:
ob_start();
In our up_search_form_render_cb() function, we used output buffers to store the HTML without it being sent
to the browser.
function up_search_form_render_cb() {
ob_start();
?>
<div {...blockProps}>
<h1>Search: Your search term here</h1>
<form>
<input type="text" placeholder="Search" />
<div className="btn-wrapper">
<button type="submit" style={{
"background-color": bgColor,
color: textColor
}}>Search</button>
</div>
</form>
</div>
<?php
$output = ob_get_contents();
ob_end_clean();
return $output;
}
Most importantly, we retrieved the content from the buffer by using a function called ob_get_contents() . This
function returns the content as a string, which we store in a variable called $output . Afterward, we cleaned and
closed the output buffer with the ob_end_clean() function. This step is very important. Otherwise, our buffer
may affect other plugins, or we may end up cluttering the output buffer.
Lastly, we returned the $content variable. By using output buffers, we can avoid using strings for storing HTML
that causes us to lose syntax highlighting from the editor.
function up_search_form_render_cb($atts) {
$bgColor = esc_attr($atts['bgColor']);
$textColor = esc_attr($atts['textColor']);
$styleAttr = "background-color:{$bgColor};color:{$textColor};";
}
The attributes are stored as items in an array. We are storing them into separate variables for readability. Before
doing so, the values are passed into the esc_attr() function, which will make sure that our code is safe to use
from within an attribute.
Cleaning data is common in programming. If you clean/filter data before inserting it into the database, this is
considered sanitization. If you clean/filter before inserting it into the browser, this is considered escaping.
After loading the attributes, we stored the value for the style attribute in a variable. We are using variable
interpolation, which is similar to JavaScript string templates. We can inject variables into a string by using double
quotes and surrounding variables with {} characters.
Resources
Data Sanitization/Escaping
First, we're using the esc_html_e() function to output translated text. There's another function for outputting
translated text called _e() . The differences are that the first function will escape the output while the second does
not.
Afterward, we used the the_search_query() function to grab the current search term. This is known as a
template tag, which is a function for outputting content. Most template tags can start with the word get. For
example, another function for grabbing the search query is called get_search_query() . The difference is that
get variation will return the value instead of outputting it onto the screen.
For the form, we added the action attribute to specify where the form data should be submitted to. We are
letting WordPress generate the URL with the home_url() function, which accepts a path relevant to the home
URL. In addition, we are passing on the value to the esc_url() function for security.
Lastly, we added the name attribute to the <input /> element since WordPress uses query parameters for
storing the search term.
Resources
Template Tags
the_search_query()
After creating those files, we updated the register-blocks.php by updating the $blocks variable to register the
new block:
$blocks = [
['name' => 'fancy-header'],
['name' => 'search-form', 'options' => [
'render_callback' => 'up_search_form_render_cb'
]],
['name' => 'page-header', 'options' => [
'render_callback' => 'up_page_header_render_cb'
]]
];
Next, we created a file called search-form.php inside the includes/blocks directory with the following code:
function up_page_header_render_cb() {
include(UP_PLUGIN_DIR . 'includes/blocks/page-header.php');
Resources
Page Header Starter Files
{
"attributes": {
"content": {
"type": "string"
}
}
}
import {
useBlockProps, RichText
} from '@wordpress/block-editor';
import { __ } from '@wordpress/i18n';
3. Update the edit() function with the arguments and RichText component:
return (
<>
<div { ...blockProps }>
<div className="inner-page-header">
<RichText
tagName="h1"
placeholder={ __('Heading', 'udemy-plus') }
value={ content }
onChange={ content => setAttributes({ content }) }
/>
</div>
</div>
</>
);
}
.wp-block-udemy-plus-page-header h1,
.editor-styles-wrapper .wp-block-udemy-plus-page-header h1 {
font-size: 1.875rem;
line-height: 2.25rem;
font-weight: 500;
padding: 0;
}
{
"attributes": {
"showCategory": {
"type": "boolean",
"default": false
}
}
}
Next, we imported the InspectorControls , PanelBody , and ToggleControl components from their
respective packages in the index.js file.
import {
useBlockProps, RichText, InspectorControls
} from '@wordpress/block-editor';
import { PanelBody, ToggleControl } from '@wordpress/components';
Next, we grabbed the showCategory attribute from the edit() function to apply to the control.
<InspectorControls>
<PanelBody title={ __('General', 'udemy-plus') }>
<ToggleControl
label={__("Show Category", 'udemy-plus')}
checked={ showCategory }
onChange={ showCategory => setAttributes({ showCategory }) }
/>
</PanelBody>
</InspectorControls>
Most of the code snippet is familiar to us. The newest thing we did was add the <ToggleControl /> component.
This component has three properties.
In the index.js file, we updated the <RichText /> component with the following expression:
{
showCategory ?
<h1>{__('Category: Some Category', 'udemy-plus')}</h1> :
<RichText
tagName="h1"
placeholder={ __('Heading', 'udemy-plus') }
value={ content }
onChange={ newVal => setAttributes({ content: newVal }) }
/>
}
It's completely valid to write JSX inside an expression. We are using the showCategory attribute to check if it's
enabled. If it is, we are displaying a generic category. Otherwise, we're rendering the <RichText /> component.
We did the same for the help property for the <ToggleControl /> component as an exercise. The help
property will render text below the control.
<ToggleControl
label={__("Show Category", 'udemy-plus')}
help={
showCategory ?
__('Category Shown.', 'udemy-plus') :
__('Custom Content Shown', 'udemy-plus')
}
checked={ showCategory }
onChange={ showCategory => setAttributes({ showCategory }) }
/>
function up_page_header_render_cb($atts) {
$heading = esc_html($atts['content']);
if($atts['showCategory']) {
$heading = get_the_archive_title();
}
ob_start();
?>
<div class="wp-block-udemy-plus-page-header">
<div class="inner-page-header">
<h1><?php echo $heading; ?></h1>
</div>
</div>
<?php
$output = ob_get_contents();
ob_end_clean();
return $output;
}
Most of this should be familiar to you. Here's what we did that's new. Firstly, we are using a function called
esc_html() for escaping the content from the attribute. Secondly, we checked if the showCategory attribute
is set to true . If it is, we are reassigning the $heading variable to the current category with the
get_the_archive_title() function.
After making these changes, we added this block to the Category template for the Udemy theme. However, we will
encounter issues if the client decides not to add a value for the content attribute.
During the video, we encountered an error that stated we had an Undefined Index, which is related to arrays. This
error gets thrown if an array is missing an index that is referenced, whether it's a named or numeric index. This issue
can arise if your attributes don't have a value during updates.
We can avoid this issue by always adding a default value to our attributes. In the block.json file, we updated the
content attribute.
{
"attributes": {
"content": {
"type": "string",
"default": ""
}
}
}
Output buffering can be enabled by editing the php.ini file, which can be found in the conf/php directory of a Local
project. You can add the following value to enable output buffering:
output_buffering = On
Alternatively, to enable output buffering and limit the buffer to a specific size, use a numeric value instead of on .
For example, to set the maximum size of the output buffer to 4096 bytes, modify the output_buffering
directive in the php.ini file as follows:
output_buffering = 4096
To disable output buffering, modify the output_buffering directive in the php.ini file as follows:
output_buffering = off
Resources
PHP Configuration File
The Glob tool can be used with the glob() function. We can pass in a path and pattern to search for files in. In this
example, we are using the * character to act as a wildcard. The first variable will store an array of files found in the
includes directory. The second variable will store an array of files found in subdirectories. Lastly, we're merging both
arrays with the array_merge() function to create a single loop.
Afterward, we're looping through the $allFiles array to begin including files. We're using the
include_once() function to include a file and prevent files from being included twice.
Include vs Require
There are two types of functions for including files called include() and require() . Both will accomplish the
same task. However, the include() function will throw a warning and allow the rest of the script to run. Whereas
the require() function will stop the script from running if a file can't be found. In most cases, you should use the
include() function unless you absolutely must stop WordPress from running.
Resources
Glob
The npx command will temporarily download and execute a package. It's a great way to use a package without
downloading it to your system and having to manually delete it. After running this command, a new project should
be created with the following files and folders:
.editorconfig - A file for configuring an editor with specific settings. Helpful for teams working on
different teams.
.gitignore - A list of files not to commit
example.php - The main plugin file
package.json - A file with settings for the packages downloaded in a project.
readme.txt - A file that contains a description, installation instructions, and screenshots that can be
displayed on a plugin's page if you plan on uploading your plugin to the official WordPress plugin repo.
src - Your block's code.
Inside the src directory, WordPress will structure blocks differently by outsourcing the edit() and save()
functions to separate files. Other than that, most of the code is stuff you should be familiar with.
In addition, the project will be set up with Sass. Sass is a language for writing CSS. For this course, we're gonna stick
with plain CSS, but you're more than welcome to check out Sass.
Resources
Create Block Package
Sass
There are two versions of each block example. An example with JSX and tooling and another without those tools. The
examples with tooling can be found with the word -esnext attached to the name. If you're not interested in using
JSX, you can check out the examples without. However, in most cases, using tooling is the best way to go.
React.creatElement vs wp.element.createElement()
React is a package maintained by Facebook/Meta. WordPress does not work on this package. Behind the scenes,
WordPress will define its own version of React's functions for backward compatibility. If React changes its API, your
blocks will continue to work.
Resources
Gutenberg Examples
Section 10: Authentication
In this section, we created a series of blocks for adding authentication to our site along with extending the REST API.
$blocks = [
['name' => 'fancy-header'],
['name' => 'search-form', 'options' => [
'render_callback' => 'up_search_form_render_cb'
]],
['name' => 'page-header', 'options' => [
'render_callback' => 'up_page_header_render_cb'
]],
['name' => 'header-tools', 'options' => [
'render_callback' => 'up_header_tools_render_cb'
]]
];
Next, we created a file called header-tools.php inside the includes/blocks folder. We defined the following function:
function up_header_tools_render_cb() {
Resources
Header Tools Starter Files
{
"attributes": {
"showAuth": {
"type": "boolean",
"default": true
}
}
}
Next, we updated the index.js file by importing the SelectControl component from the
@wordpress/components package.
import {
PanelBody, SelectControl
} from '@wordpress/components';
Lastly, we updated the template by inserting the SelectControl component inside the PanelBody
component:
<SelectControl
label={__("Show Login/Register Link", 'udemy-plus')}
value={ showAuth }
options={[
{ label: __('No', 'udemy-plus'), value: false },
{ label: __('Yes', 'udemy-plus'), value: true },
]}
onChange={ newVal => setAttributes({ showAuth: (newVal === "true") }) }
/>
During this process, we type cast the newVal argument to a boolean because the SelectControl component
will emit a string, which is not compatible with the data type in our attribute.
import {
PanelBody, SelectControl, CheckboxControl
} from '@wordpress/components';
Next, we updated the template by inserting the CheckboxControl component inside the PanelBody
component:
<CheckboxControl
label={__("Show Login/Register Link", 'udemy-plus')}
help={
showAuth ?
__('Showing Link', 'udemy-plus') :
__('Hiding Link', 'udemy-plus')
}
checked={ showAuth }
onChange={ showAuth => setAttributes({ showAuth }) }
/>
{
showAuth ?
// Template here...
: null
}
In this example, there is no alternative template. In these cases, you can set the else condition to null to prevent
JSX from rendering anything.
However, there's an alternative solution. If you don't plan on rendering an alternative template, you can use the &&
operator. This operator will allow you to add multiple conditions. For example, take the following:
If both conditions are truthy, the block of code will execute. Both conditions must be true. Otherwise, the block of
code will never run. We can use this in our JSX as a hack. JavaScript follows a fast-exit strategy. If the first condition
evaluates to false , the second condition is never checked even if it evaluates to true .
{
showAuth && // Template here...
}
JavaScript will check if the showAuth attribute is true . If it is, the second condition will run, which is our
template. This causes the template to be rendered. If the attribute evaluates to false , the template will never be
rendered.
Lecture 159: Finishing the Header Tools Block
In this lecture, we finished the Header Tools block by finishing the PHP render function. The render function contains
the typical code for creating output buffers. The unique part of this function was conditionally rendering the links.
We used traditional conditional statements to render these blocks.
if($atts['showAuth']) {
?>
<!-- Template -->
<?php
}
if($atts['showCart']) {
?>
<!-- Template -->
<?php
}
We are checking the $atts variable in both conditional statements. The attributes are already boolean values. So,
there's no need to do anything else besides checking them.
$blocks = [
['name' => 'fancy-header'],
['name' => 'search-form', 'options' => [
'render_callback' => 'up_search_form_render_cb'
]],
['name' => 'page-header', 'options' => [
'render_callback' => 'up_page_header_render_cb'
]],
['name' => 'header-tools', 'options' => [
'render_callback' => 'up_header_tools_render_cb'
]],
['name' => 'auth-modal', 'options' => [
'render_callback' => 'up_auth_modal_render_cb'
]]
]
Lastly, we created a file called auth-modal.php that contains the function for rendering the block.
Resources
Auth Modal Starter Files
Lecture 161: Toggling the Registration Form
In this lecture, we inserted the ToggleControl component for toggling the showRegister attribute for the
Authentication Modal block. This lecture was introduced as an opportunity for creating control components. First,
we imported the ToggleControl component from the @wordpress/components package.
Lastly, we inserted this component into the PanelBody component. Here's the finished code:
<ToggleControl
label={__("Show Register", 'udemy-plus')}
help={
showRegister ?
__('Showing Registration Form.', 'udemy-plus') :
__('Hiding Registration Form', 'udemy-plus')
}
checked={ showRegister }
onChange={ showRegister => setAttributes({ showRegister }) }
/>
{
"Block Output Buffer": {
"prefix": "block output buffer",
"body": [
"\tob_start();",
"\t?>",
"",
"\t<?php",
"\t\\$output = ob_get_contents();",
"\tob_end_clean();",
"",
"\treturn \\$output;"
],
"description": "Adds an Output Buffer for Blocks"
}
}
Afterward, we inserted the template, which can be found inside the resource section. The modal contains tabs where
each tab contains a login form and registration form. Since clients are able to toggle the registration form, we
wrapped the elements with conditional statements.
First, we updated the function to include the $atts argument. Next, you can find the registration tab by searching
for a comment that says Register Tab. We wrapped the <li> element with a conditional statement. The condition
is the $atts['showRegister'] attribute.
if($atts['showRegister']) {
?>
<!-- Register Tab -->
<?php
}
We applied the same condition to the <form> element with a comment above it that says Register Form.
Resources
Authentication Modal Template
console.log('Auth Modal');
This file was enqueued by updating the block.json file. We added the viewScript property to load this file. You
may need to restart the start command for the changes to take effect.
{
"viewScript": "file:./frontend.js"
}
JavaScript has an event system. This system allows us to listen for events on elements, such as clicks and keyboard
events. In this case, we're listening for an event called DOMContentLoaded on the document object.
document.addEventListener("DOMContentLoaded", () => {
console.log('test')
})
This event gets emitted when the document has finished loading. The first argument to the
addEventListener() function is the name of the event. The second argument is a callback function that'll get
called when the event is emitted.
The open-modal class can be applied to any element that should open the modal. Afterward, we looped through
the elements to add an event listener.
openModalBtn.forEach(el => {
el.addEventListener('click', (e) => {
})
})
We must loop through the elements since we can't apply event listeners to an entire array. Looping through
elements is as simple as using the forEach() function, which can be chained on an array. JavaScript provides
methods for looping through arrays. This syntax may seem strange since we're treating an array like an object, but
it's completely valid.
On each iteration, we can pass in a function to handle each item in the array. We are provided the element in the
function as an argument. From there, we can listen to the click event. Afterward, we added the following code:
e.preventDefault();
modalEl.classList.add('modal-show')
We are calling the preventDefault() method since the class can be applied to links, which can cause the page
to refresh. Afterward, we updated the classes on the modal element by accessing the classList object. This
object has a method called add() for adding a class.
We did the same thing for closing the modal. This time, we closed the modal by removing the class with the
remove() method like so:
modalEl.classList.remove('modal-show')
tabs.forEach(tab => {
tab.addEventListener('click', event => {
event.preventDefault()
tabs.forEach(currentTab => currentTab.classList.remove('active-tab'))
event.currentTarget.classList.add('active-tab')
1. First, we selected the tab links, login form, and registration form
2. Looped through the tabs to add an event listener
3. Removed the active-tab class from the tab links
4. Add the active-tab class to the tab link by using the event.currentTarget property. This
property contains the element triggering the event, which means the element should have the active-
tab class applied to it.
5. Lastly, we toggled the form's display property through the style object. This object can contain valid
CSS properties. JavaScript will handle adding the properties to the element.
Afterward, we can begin disabling the form and rendering a message in the form with the following code:
signupFieldset.setAttribute('disabled', true)
signupStatus.innerHTML = `
<div class="modal-status modal-status-info">
Please wait! We are creating your account.
</div>
`
We can disable the entire form by using the <fieldset> element. This element will affect children form elements.
On this element, we added the disabled attribute with the setAttribute() method. This method has two
arguments, which are the attribute name and value.
Lastly, we rendered the message by updating the innerHTML property on the signupStatus element. I
recommend using template strings to write multiline HTML.
An endpoint refers to the path at the end of a URL. Endpoints point to specific resources.
Resources
REST API Example
JSON Formatter
WordPress REST API Handbook
We looked through various endpoints to understand how they were documented. Generally, WordPress will provide
a schema, argument list, definition, description, and example for an endpoint. A schema will describe the type of
values returned by an endpoint. The argument list will provide a list of values we can send to WordPress.
In most examples, the definition will contain the endpoint and HTTP method. An HTTP method describes the type of
action you'll be performing. The most common HTTP methods in WordPress are the following:
The WordPress REST API is accessible under the /wp-json/ path. After this path, we must append the endpoint.
For example, if we were trying to access a list of posts, we would use the following URL:
https://example.com/wp-json/wp/v2/posts
Resources
Postman
If we're interested in registering users, we must have a custom endpoint, which we do in the next set of lectures.
Lecture 171: Adding a Custom Endpoint
In this lecture, we created a custom endpoint. A new endpoint must be created after the rest_api_init hook.
We added the following line to the index.php file.
add_action('rest_api_init', 'up_rest_api_init');
Next, we created a folder for handling our REST API logic inside the includes folder called rest-api. Inside this
directory, we added a new file called init.php with the following code:
function up_rest_api_init() {
// example.com/wp-json/up/v1/signup
register_rest_route('up/v1', '/signup', [
'methods' => 'POST',
'callback' => 'up_rest_api_signup_handler',
'permission_callback' => '__return_true'
]);
}
We're running a function called register_rest_route() to add a new endpoint. This function has three
arguments:
1. Namespace - The first URL segment after core prefix. It should be unique to your package/plugin.
2. Route - The base URL for the route you are adding.
3. Arguments - Either an array of options for the endpoint or an array of arrays for multiple methods.
In this example, the namespace for this and future endpoints will be up/v1 . The format for a namespace should
have a unique ID followed by the version of our API.
As for the third argument, we passed in an array with the following keys:
methods - The HTTP method for accessing this endpoint. We're creating a user, so therefore, we should
use the POST method.
callback - A function for handling the response of the endpoint.
permission_callback - A function for handling the permissions of the callback. For this key, we're
using a function called __return_true , which is defined by WordPress. It'll always return true since
our function should be available to everyone.
function up_rest_api_signup_handler() {
$response = ['status' => 1];
$response['status'] = 2;
return $response;
}
This variable will be an array with a key called status . This key will have a value of 1 if the request fails.
Otherwise, the value for this key should be 2 . We're returning this response. WordPress will handle converting the
array into a JSON object and sending it back to the browser.
class House {
class House {
public $color = 'red';
function setColor($newColor) {
$this->color = $newColor;
}
}
In the example above, we added the public keyword to a variable to allow the variable to be accessed outside of
the class. Inside the function, we are using the $this keyword to allow our function to access properties from
within the same object. We can access a property or method by using the -> operator.
We can start using our class by creating a new instance. An instance is a copy of a class. By default, PHP does not
allow us to access the properties and methods without a copy of the class.
echo $myHome->color;
$myHome->setColor('blue');
echo $myHome->color;
Resources
PHP Playground
Access Modifiers
function up_rest_api_signup_handler($request) {
// Code here
}
Afterward, we accepted the data by using the get_json_params() function and stored the data in a variable
called $params . This function will grab JSON data from the body section of a request. Requests/responses are
comprised of a header and body. They're very similar to the <head> and <body> sections of an HTML
document. The header contains info on the request/response, such as the IP. Whereas the body contains data, such
as form data.
$params = $request->get_json_params();
if(
!isset($params['email'], $params['password'], $params['username']) ||
empty($params['email']) ||
empty($params['password']) ||
empty($params['username'])
) {
return $response;
}
We're using two functions called isset() and empty() . The isset() function will check if a variable,
property, or array key has been added. It'll return a boolean value if the variable exists. The empty() function will
check if a variable has a value. In the example above, we're checking if the email , password , and email keys
are in the response with values. If they aren't, we're returning the $response to reject the request.
Resources
WP_REST_Request
$email = sanitize_email($params['email']);
$username = sanitize_text_field($params['username']);
$password = sanitize_text_field($params['password']);
We're using a function for sanitizing the email called sanitize_email() . This function will make sure malicious
data is not inserted into the database. As for the username and password, we're using the
sanitize_text_field() function, which is meant for regular text data.
Afterward, we created a conditional statement for checking the username and email:
if(
username_exists($username) ||
!is_email($email) ||
email_exists($email)
) {
return $response;
}
The first condition will check if the username exists with the username_exists() function. The second condition
will check if the email is correctly formatted with the is_email() function. Lastly, the final condition will check if
the email is taken with the email_exists() function. All functions are defined by WordPress.
In addition, we're using the || operator, which will allow multiple conditions. It's different from the && operator.
Unlike the other operator, it'll check if any of the conditions are true . Not all conditions must be true for the block
to be executed.
$userID = wp_insert_user([
'user_login' => $username,
'user_pass' => $password,
'user_email' => $email
]);
if (is_wp_error($userID)) {
return $response;
}
The wp_insert_user() function will register a new user. It accepts an array of values. You can refer to the
resources section for a link to this function. On the documentation page for this function, you will find a
comprehensive list of values. For this example, we are setting the user_login , user_pass , and user_email
keys.
The return value from this function will be the ID of the user created or an error. WordPress will wrap errors with an
object to allow us to handle errors instead of letting PHP crash the script. Before proceeding further, we are checking
if the $userID variable is an error by using the is_wp_error() function. If it is, we are returning the
$response variable, thus ending the response.
Once we've created the user, we sent an email to the user to let them know we've created their account with the
wp_new_user_notification() function.
This function has three arguments, which are the ID of the new user, the second argument is deprecated, and where
to send the email. We can set the third argument to user , admin , or both . In most cases, sending an email to
the user who registered will suffice. This function is pluggable. If you have a plugin for managing plugins, this plugin
should be able to override the default email sent with a custom email.
After sending an email, we logged the user in with the following code:
wp_set_current_user($userID);
wp_set_auth_cookie($userID);
$user = get_user_by('id', $userID);
do_action('wp_login', $user->user_login, $user);
The wp_set_current_user() function will log the user into the system. However, they're not logged in forever.
We can persist a user's login by using the wp_set_auth_cookie() function. This function will create a cookie
that will be stored in the user's browser. Users will be able to revisit the site while staying logged in. WordPress will
detect the cookie and use it to login.
Next, we're triggering an event with the do_action() function. So far, we've been running functions on events
with the add_action() function. We can trigger events by passing in the name of the event to the
do_action() function. In this case, we're emitting the wp_login event. Typically, this event is triggered by
WordPress. However, since we're manually logging the user in, we must trigger this event.
In addition, we're passing on the user data that was grabbed with the get_user_by() function. This function can
grab a user by their email, username, or ID. In this example, we're grabbing a user object by their ID.
Resources
wp_insert_user()
wp_login Hook
add_action('wp_enqueue_scripts', 'up_enqueue_scripts');
Next, we created a folder called front inside the includes directory. Files related to the front end will be added to
this folder. Inside this folder, we created the enqueue.php file with the up_enqueue_scripts function.
$authURLs = json_encode([
'signup' => esc_url_raw(rest_url('up/v1/signup'))
]);
The value for this variable is an array of URLs created with the rest_url() function. This function will return a
URL to WordPress's REST API. We can append append a path by passing in a string. Next, we escaped the URL with
the esc_url_raw() function. Lastly, we're wrapping the array with the json_encode() function for
compatibility with JavaScript.
Up next, we injected this object into the script with the wp_inline_script() function.
wp_add_inline_script(
'udemy-plus-auth-modal-script',
"const up_auth_rest = {$authURLs}",
'before'
);
This function has three arguments. The first argument is the handlename of another script. The inline script does not
get automatically added unless another script has been added along with it. We want this script to appear with the
frontend. file for the Authentication Modal block. In your document, the handlename for a script enqueued with
the block.json file can be found by its ID. At the end of the ID, WordPress will add -js, which can be excluded from
the handlename.
The second argument is the JavaScript code to inject into the script. WordPress will automatically wrap your code
with <script></script> tags. The last argument is the position of the script. Should it appear before or
after the handle script? In this example, we want the script to appear before the frontend.js file. Otherwise, the
file may not have access to our variables.
const formData = {
username: signupForm.querySelector("#su-name").value,
email: signupForm.querySelector("#su-email").value,
password: signupForm.querySelector("#su-password").value
}
If we're selecting form elements, we have access to the value through the value property. Afterward, we initiated
the request with the fetch() function. This function will send a request with JavaScript without refreshing the
page. We added the following code:
The first argument for this function is the URL. We passed in the variable that was injected from the previous lecture
that contains the full HTTP URL to our endpoint. The second argument is an object of configuration settings. We're
setting the following:
The value returned by the fetch() function is a promise, which means the function is asynchronous.
Asynchronous functions mean that the line of code takes time to execute. JavaScript will not wait for this function to
finish. It'll execute the next line. If we want to wait for the function to finish, we can add the await keyword. If we
do add this keyword, the function must have the async keyword prefixed to it.
Lastly, we're storing the value resolved by the promise in a variable called response . This variable contains
information on the response. We can grab the response body by using the response.json() function.
const responseJSON = await response.json();
Lastly, we verified the status of the response and rendered the appropriate message.
if (responseJSON.status === 2) {
signupStatus.innerHTML = `
<div class="modal-status modal-status-success">
Success! Your account has been created.
</div>
`;
location.reload();
} else {
signupFieldset.removeAttribute('disabled');
signupStatus.innerHTML = `
<div class="modal-status modal-status-danger">
Unable to create account! Please try again later.
</div>
`;
}
If the response was a success, we reloaded the page. Otherwise, we removed the disabled attribute from the
fieldset element to allow the user to modify their values.
Resources
Fetch Function
register_rest_route('up/v1', '/signin', [
'methods' => 'POST',
'callback' => 'up_rest_api_signin_handler',
'permission_callback' => '__return_true'
]);
The most noteworthy thing about this registration is the namespace. We're reusing the same namespace since our
routes should be grouped into a single route.
For the function, we created a new file called signup.php inside the includes/rest-api with the following code:
function up_rest_api_signin_handler($request) {
$response = ['status' => 1];
$params = $request->get_json_params();
if(
!isset($params['user_login'], $params['password']) ||
empty($params['user_login']) ||
empty($params['password'])
) {
return $response;
}
$response['status'] = 2;
return $response;
}
1. Grabbing the data from the request with the $request->get_json_params() function.
2. Validating the values from the parameters.
3. Returning the response.
$email = sanitize_email($params['user_login']);
$password = sanitize_text_field($params['password']);
Next, we signed into WordPress by using the wp_signon() function. This function accepts an array where we can
pass in the email, password, and if we should remember the user.
$user = wp_signon([
'user_login' => $email,
'user_password' => $password,
'remember' => true
]);
if (is_wp_error($user)) {
return $response;
}
This function returns a user object. Otherwise, it'll return an error. We're checking if the variable contains an error. If it
does, we are returning the response.
We're using a function defined by WordPress called is_user_logged_in() to help us check if the user is logged
in. If they are, we will return an empty string to hide the modal.
Next, we updated the header-tools.php file with the following code at the top of the function:
$user = wp_get_current_user();
$name = $user->exists() ? $user->user_login : 'Sign in';
$openClass = $user->exists() ? '' : 'open-modal';
In this example, we're using the wp_get_current_user() function to get the currently authenticated user's info.
This function returns an object, which has a method called exists() . This method will tell us if a user was found.
If there is a user logged in, we're storing their name in a variable.
In addition, we're creating a variable for storing the open-modal class. This class will trigger the modal. By
dynamically adding it to an element, we can prevent errors from appearing in the console when there isn't a modal.
We updated the <a> element for the login link with the following code:
class Car {
public static $brand = "Tesla";
}
This keyword should always be applied after the access modifier. Next, we can access the property like so:
echo Car::$brand;
Instead of using -> to access properties, static properties must be accessed with the :: characters. As you can
see, we did not have to create an instance. This removes an extra step in the process, but why?
Static properties can be useful when you don't care if a property is not unique to each instance. By using regular
properties, each instance will always have a unique copy of a property. Changing a property in one instance will not
be reflected in all instances. Whereas static properties are affected globally.
Resources
PHP Playground
These properties standardize methods for specific actions. There's CREATABLE , EDITABLE , READABLE , and
DELETEABLE . Rather than hardcoding the methods, we can use these properties to set the appropriate method.
It's not required, but it's always good practice to follow a standard. If the standard changes, you do not need to
make further changes. WordPress will be updated to the latest changes.
register_rest_route('up/v1', '/signup', [
'methods' => WP_REST_Server::CREATABLE,
'callback' => 'up_rest_api_signup_handler',
'permission_callback' => '__return_true'
]);
register_rest_route('up/v1', '/signin', [
'methods' => WP_REST_Server::EDITABLE,
'callback' => 'up_rest_api_signin_handler',
'permission_callback' => '__return_true'
]);
Resources
WP_REST_Server Class
Section 11: SQL Fundamentals
In this section, we took a break from WordPress to start exploring SQL. Learn how to interact with a database
through raw queries.
Local will provide us with a client for working with our database called Adminer. We'll be using it for this course. You
can look at alternative options in the resource section of this lecture.
Resources
MySQL
MariaDB
MySQL Workbench
HeidiSQL
Sequel Pro
You can refer to Local for the values of each field in your client. Next, we moved our discussion to tables. Tables allow
you to store groups of data. You can have a post for users, posts, comments, etc. Each table will have an engine and
collation.
An engine is responsible for processing queries. Each engine will perform certain actions better than others. The
most common engine is InnoDB . This is the default engine provided by the database, which is an all-around
general-purpose engine that will cover most use cases.
The collation is the character set supported by the database. If you're interested in storing various characters, you
should use the utf8_mb4_unicode_ci . This collation supports a wide range of characters from various
languages.
The CREATE TABLE keyword will create a table. This keyword is followed by the name of the table and the
columns. Inside the parentheses, we're creating two columns called ID and name . They're separated with a
comma.
After specifying a column name, we must add the data type. SQL languages offer various data types for the same
type of data to allow us to constrain the size. It's considered good practice to only specify a size that doesn't exceed
your needs. You can refer to the resource section of this lecture for a complete list of data types.
Lastly, we're using the NOT NULL keywords to prevent the ID column from accepting an empty value. Behind the
scenes, MariaDB/MySQL performs validation on your data. If a value does not match the data type of a column or is
empty, the value will be rejected.
Resources
Data Types for SQL
We started with creating data. Data can be created with the INSERT INTO keywords. This is followed by the name
of the table to insert data into. Next, we can specify values for each column in the table by using the VALUES
keyword like so:
We must provide a list of values for each column in the table. Alternatively, we can provide a list of columns to add
values for by adding a pair of () after the name of the table like so:
However, this will not work if you forget to add a column that does not allow null values.
SELECT ID
FROM products
We can select all columns by replacing the names of columns with a * character like so:
SELECT *
FROM products
In some cases, you may want to filter the records from a table using the WHERE keyword like so
SELECT *
FROM products
WHERE name="Hats"
You can find a complete list of comparison operators in the resource section of this lecture.
Resources
SQL Operators
UPDATE products
SET name="Shirts"
WHERE ID=2
It's very important to add the WHERE keyword to update specific records. Otherwise, all records will be updated.
Lastly, we deleted the table by using the DROP TABLE keyword like so:
add_action('init', 'up_recipe_post_type');
Inside the includes directory, we created a file called recipe-post-type.php. This file will contain the
up_recipe_post_type function definition. Inside this function, we pasted in the code example that can be found
in the example in the documentation. This should register a post type called book .
Resources
Registering Custom Post Types
However, the _x() function has an argument for providing a note to the translator. This can be helpful for
providing additional info to help translators understand the message. In the end, WordPress does not use this
argument. It's purely for translators.
The translations were updated to use the udemy-plus text domain, and the word book was replaced with
recipe . After modifying the labels, we updated the options:
$args = array(
'labels' => $labels,
'public' => true,
'publicly_queryable' => true,
'show_ui' => true,
'show_in_menu' => true,
'query_var' => true, // ?recipe=pizza
'rewrite' => array( 'slug' => 'recipe' ), // /recipe/pizza
'capability_type' => 'post',
'has_archive' => true,
'hierarchical' => false,
'menu_position' => 20,
'supports' => array( 'title', 'editor', 'author', 'thumbnail', 'excerpt'
),
);
Lastly, we called register_post_type() function that has two arguments. The name of the post type and the
array of arguments.
Resources
register_post_type()
$args = array(
'description' => __('A custom post type for recipes', 'udemy-plus'),
'show_in_rest' => true,
);
In addition, we added the description option to provide a human-readable description of our post type. After
adding this option, the Gutenberg editor should appear for our posts.
We can add support for these taxonomies by updating the arguments to include the taxonomies option. This
option will be an array of taxonomies. We updated this option to the following:
$args = array(
'taxonomies' => ['category', 'post_tag']
);
After adding this option, categories and tags should appear for our post type.
register_activation_hook(__FILE__, 'up_activate_plugin');
This function will register a hook for plugin activation. The first argument is the path to the main plugin file. We are
using the __FILE__ constant to grab the path to the current plugin file. Next, we must provide the name of a
function to run during activation. We defined this function from within a file called activate.php.
function up_activate_plugin(){
// 6.0 < 5.8
if(version_compare(get_bloginfo('version'), '5.8', '<')) {
wp_die(
__('You must update WordPress to use this plugin.', 'udemy-plus')
);
}
}
In this function, we added a conditional statement to compare the current version of WordPress with the minimal
required version for WordPress. While WordPress can check the compatibility of our plugin, this feature is still new.
On very old versions of WordPress, it'll be possible to activate our plugin. For extra security, we are going to perform
this process manually.
In a conditional statement, we are using the version_compare() function, which is defined by PHP. It's capable
of comparing versions with various formats. We are passing in the current version with the get_bloginfo()
function. Next, we are passing in the minimal supported version. Lastly, we are passing in a comparison operator.
If this function returns true , the plugin is installed on an unsupported version of WordPress. In this case, we are
killing the script with the wp_die() function, which will also output a neatly formatted message.
Resources
get_bloginfo()
We can add our rule by calling the flush_rewrite_rule() function like so from the activation file:
up_recipe_post_type();
flush_rewrite_rules();
First, we are calling the up_recipe_post_type() function since it contains the custom post type registration.
Our post type may not be registered during this hook. Just to make sure, we're manually registering it. Next, we
called the flush_rewrite_rules() function to update the entry in the database to include our custom path.
You can deactivate and reactivate the plugin to test the rules. If you refresh the database, the rewrite_rules
entry should contain our new path.
Resources
flush_rewrite_rules()
register_taxonomy('cuisine', 'recipe', [
'label' => __( 'Cuisine', 'udemy-plus' ),
'rewrite' => ['slug' => 'cuisine'],
'show_in_rest' => true
]);
This function has three arguments, which are the name of the cuisine, post type, and an array of settings. In this
example, we are adding a label, the unique URL for viewing a term and enabling this taxonomy for the REST API. The
show_in_rest option is very important if you want users to be able to add taxonomy terms to the post type from
the Gutenberg editor.
Resources
register_taxonomy()
add_action('cuisine_add_form_fields', 'up_cuisine_add_form_fields');
Next, we defined this function from a directory called includes/admin. The name of the file is called cuisine-
fields.php.
function up_cuisine_add_form_fields() {
?>
<div class="form-field">
<label><?php _e('More Info URL', 'udemy-plus'); ?></label>
<input type="text" name="up_more_info_url">
<p>
<?php
_e(
'A URL a user can click to learn more information about this cuisine.',
'udemy-plus'
);
?>
</p>
</div>
<?php
}
There's not much to say about this code aside from the fact that we're copying WordPress's classes for styling the
field. This step is optional, but the UI should be consistent with WordPress's UI. In addition, the <input />
element has a name. The name attribute is important as WordPress will use this name for sending the value to the
backend.
Resources
_add_form_fields Hook
add_action('create_cuisine', 'up_save_cuisine_meta');
Next, we defined this function inside a file called save-cuisine.php from the includes/admin folder.
function up_save_cuisine_meta($term_id) {
if(!isset($_POST['up_more_info_url'])) {
return;
}
add_term_meta(
$term_id,
'more_info_url',
sanitize_url($_POST['up_more_info_url'])
);
}
This function has one argument, which is the ID of the term created. Before saving the metadata, we validated the
post data. By default, PHP will store post data in a variable called $_POST . This is an array of value submitted by a
form. A value can be referenced by the name given in the name attribute. In this example, we are checking if this
variable contains the up_more_info_url value.
If it does, we are adding the metadata with the add_term_meta() function. This function has three arguments,
which are the ID, the name of the metadata, and the value. Before inserting this value into the database, we are
sanitizing it with the sanitize_url() function.
Resources
create_taxonomy
add_action('cuisine_edit_form_fields', 'up_cuisine_edit_form_fields');
We defined the up_cuisine_edit_form_fields function from within the cuisine-fields.php file like so:
function up_cuisine_edit_form_fields($term) {
$url = get_term_meta($term->term_id, "more_info_url", true);
?>
<tr class="form-field">
<th>
<label><?php _e('More Info URL', 'udemy-plus'); ?></label>
</th>
<td>
<input type="text" name="up_more_info_url"
value="<?php echo esc_attr($url); ?>">
<p class="description">
<?php
_e(
'A URL a user can click to learn more information about this origin.',
'udemy-plus'
);
?>
</p>
</td>
</tr>
<?php
}
In this function, we're using a different markup, which we created by studying the markup for the other fields. In this
function, we are accepting the WP_Term object as an argument called $term . This object contains information
on the current term being edited.
We used this object to retrieve the metadata. To get the metadata, we used the get_term_meta() function. This
function has three arguments which is the ID, the name of the metadata, and whether to return an array or a single
value. The ID was retrieved through the $term argument. This object has a property called term_id . It's the ID
from the database.
Once we grabbed the metadata, we used it as the value for the <input /> element.
The last step was to update the up_save_cuisine_meta() function. We replaced the add_term_meta()
function with the update_term_meta() function. These functions have the same arguments. The main difference
is that the first function will add metadata while the second function will add/update the metadata. We do not need
to check if the metadata already exists. The update_term_meta() function performs this check on our behalf.
Resources
edit_form_fields Hook
WP_Term
Resources
Starter Files
{
"usesContext": ["postId"]
}
This array will contain a list of values to accept from the editor. In this case, we're accepting the postId value.
Next, we can update the edit() function to destructure the props object for the context object like so:
edit({ context }) {
const { postId } = context
}
From this object, we're destructuring the postId property into a variable. For more info, you can refer to the
resource section of this lecture.
Resources
Block Context
Next, we used this function to grab the term IDs. This function has 4 arguments, which are the type of data to return,
the post type, the taxonomy, and lastly, the ID of the post. This function returns the values retrieved by WordPress
and a function for updating these values.
In our case, we're not going to grab the function for updating the values. We'll allow WordPress to handle this
process.
In WordPress, an entity is an object that represents data. This data can be anything from post data to metadata.
Next, we used this function to begin initiating a query. The function accepts a function that will be executed
whenever the block changes.
useSelect(() => {
console.log('useSelect() called')
}, []);
However, this is not ideal. If we were to leave the function as-is, the query would be constantly initiated, which can
impact performance. The query should only run when the cuisine taxonomy is updated on the post. The
useSelect() function has a second argument, which is an array of dependencies. We passed in the termIDs
variable to limit the number of times this function runs.
useSelect(() => {
console.log('useSelect() called')
}, [termIDs]);
Gutenberg manages massive amounts of data. To keep things organized, data is organized into stores, which is short
for storage. If we're interested in accessing post data, we must access the core store. A list of stores can be found
from the link in the resource section of this lecture. After accessing a store, we have access to functions for
interacting with that specific store.
return {
cuisines: getEntityRecords('taxonomy', 'cuisine', {
include: termIDs
})
}
}, [termIDs]);
In this example, we're accepting the select() function as an argument. This function will allow us to select a
store, which is passed in as a string. Next, we are destructuring the store to grab a specific function called
getEntityRecords() . This function allows us to grab specific records from a table in a database using the REST
API.
We are returning an object, which will be returned by the useSelect() function so that our data is accessible
outside of the function. We are using the getEntityRecords() function to grab terms from the database. We're
grabbing the cuisine taxonomy.
Lastly, we're filtering the results by passing in an object as a third argument. The object can be a list of arguments
supported by the REST API. In this case, we're limiting the results to terms with specific IDs.
Resources
Data Module Reference
Category Endpoint
register_term_meta('cuisine', 'more_info_url', [
'type' => 'string',
'description' => 'A URL for more information on a cuisine',
'single' => true,
'default' => '#',
'show_in_rest' => true,
]);
This function has three arguments. The first argument is the name of the taxonomy. The second argument is the
name of the metadata key. Lastly, we can provide an array of arguments to configure the metadata. We added the
following values:
Resources
register_term_meta()
return (
<>
<a href={term.meta.more_info_url}>
{term.name}
</a>
{comma}
</>
)
})}
First, we checked if there were cuisines before looping through the array. After checking the cuisines variable,
we're looping through this array with the map() function. Unlike the forEach() function, this function will
return an array with the modified items. In this case, we're going to create elements.
We're accepting the current item and the index in the callback function. The index will help us check if there's
another item in the array so that we can add a comma. In the template, we're rendering the term URL, name, and a
comma.
return {
cuisines: getEntityRecords(...taxonomyArgs),
isLoading: isResolving('getEntityRecords', taxonomyArgs)
}
}, [termIDs]);
First, we updated the useSelect() function. In this function, we're grabbing the isResolving() function.
This function will watch a request to let us know if it's complete. Next, we outsourced the arguments for the request
into a variable since the isResolving() function needs a copy of these arguments. We spread this array into the
getEntityRecords() function.
Lastly, we created a new property called isLoading with the isResolving() function as the value. We must
provide this function with the name of the function to watch along with the arguments since requests can come from
different plugins/blocks.
})}
In this example, we're checking the isLoading variable, which will be a boolean value. If true , the request is
pending. If that's the case, we displayed the <Spinner /> component, which was imported from the
@wordpress/components package.
Resources
Spinner
add_action('save_post_recipe', 'up_save_post_recipe');
Next, we defined this function inside the includes/admin folder called save-recipe.php.
function up_save_post_recipe($post_id) {
if (defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE) {
return;
}
$rating = get_post_meta($post_id, 'recipe_rating', true);
$rating = empty($rating) ? 0 : floatval($rating);
This function checks if the DOING_AUTOSAVE constant is defined with the defined() function. WordPress will
add this constant if the post is being autosaved. An autosaved post may not have a post ID, which means saving
metadata will not work. If the request is an autosave, we're returning the function to prevent it from running.
After checking this constant, we're grabbing the $post_id argument. Inside the function, we're grabbing the
metadata from the post with the get_post_meta() function. This function has three arguments, which are the ID
of the post, metadata key, and if the value is a single value.
Next, we're checking if the rating is empty; if it is, we'll convert it into a number. Otherwise, we'll save the original
value with the floatval() function for type casting the value as a number.
Lastly, we're updating the post metadata with the update_post_meta() function. This function has three
arguments which are the ID of the post, the name of the metadata, and the value itself.
Resources
save_post_ Hook
register_post_meta('recipe', 'recipe_rating', [
'type' => 'number',
'description' => 'The rating for a recipe',
'single' => true,
'default' => 0,
'show_in_rest' => true,
]);
This function has similar arguments to the register_term_meta() function. We must provide the name of the
post type, the name of the metakey, and a list of arguments. We're configuring the following options with our
metadata:
Lastly, we updated the arguments for the custom post type by including the custom-fields option in the
supports array. Without this support, the metadata may not be retrievable with the REST API.
$args = array(
'supports' => array(
'title', 'editor', 'author', 'thumbnail', 'excerpt', 'custom-fields'
)
);
return {
rating: getCurrentPostAttribute('meta').recipe_rating
}
})
In the object, we're grabbing the metadata by passing in meta to the function. Keep in mind, other plugins may
add metadata. This function will return an object of metadata associated with the post. We're only interested in our
metadata, so we accessed the recipe_rating property.
After installing the package, we imported it into our Recipe Summary block like so:
Next, we added this component to our block with the following template:
We're using the <Rating /> component to render a rating UI. Two properties are being added. The value
property will set the initial value. The readOnly property will prevent users from being clicked on to change the
rating. Rating should only be possible on the front end.
Resources
MUI
global $wpdb;
$tableName = "{$wpdb->prefix}recipe_ratings";
$charsetCollate = $wpdb->get_charset_collate();
require(ABSPATH . "/wp-admin/includes/upgrade.php");
dbDelta($sql);
First, we're grabbing the $wpdb object as a global variable. Global variables are variables available to all functions
in a script. We can use this variable for various properties and methods related to the database. In this case, we're
using it to get the prefix of the database and collation to be consistent with WordPress's database design.
Next, we stored the query in a variable called $sql . Inside our query, we're injecting the table name and collation.
The backticks have also been removed since they're not needed.
Lastly, we included a file called upgrade.php. This file contains a function called dbDelta() . This function will
execute the query and allow plugins to modify the query if necessary.
Resources
Recipe Summary Template
We stored the ID in a variable called $postID . Afterward, we grabbed the post's terms with the
get_the_terms() function. This function has two arguments, which are the ID of the post and the name of the
taxonomy.
$cuisines = '';
$lastKey = array_key_last($post_terms);
In our loop, we're using grabbing the term meta for the URL of the term. In addition, we're checking if the current
iteration is the last item in the array. First, we grabbed the last key by using the array_key_last() function.
Next, we checked if the $lastKey variable matches the $key variable from the loop. If it does, we can assume
it's the last item. Lastly, we outputted the comma after the link.
The last step was to render the $cuisines variable in our template like so:
We updated our template by adding a <div> element with a class called recipe-data .
In the above example, we're adding data attributes for the post ID, current rating, and if the user is logged in. After
adding this information, we created a script called frontend.js with the following code:
document.addEventListener("DOMContentLoaded", () => {
const block = document.querySelector('#recipe-rating')
const postID = parseInt(block.dataset.postId)
const avgRating = parseFloat(block.dataset.avgRating)
const loggedIn = !!block.dataset.loggedIn
In the above example, we're selecting the element with the data attributes with its ID. Next, we're extracting each
data attribute's value into a variable through an object called dataset . This object is available for all data
attributes. A value can be referenced without including the data attribute.
In addition, we're typecasting the variables with the parseInt() , parseFloat() and !! operator. The
parseInt() function will convert a value into a whole number. The parseFloat() function will convert a
value into a number with decimal values. Lastly, the !! operator will convert a value into a boolean value.
We're importing the render and useState functions from the @wordpress/element package. Behind the
scenes, this package reexports React's render and useState functions. They're a copy of these functions to
help us with backward compatibility. If React changes its API, we can continue using the old functions while
benefitting from the latest version of React. WordPress will handle the differences.
function RecipeRating(props) {
return (
<Rating
/>
);
}
This component is rendering the <Rating /> component. Next, we updated our event handler by rendering this
function with the render() function.
render(
<RecipeRating
postID={postID}
avgRating={avgRating}
loggedIn={loggedIn}
/>,
block
)
In the above example, we're passing on the data to this component as props. In addition, we're rendering this
component in our block. We updated our component to use this data by creating state. One state property for
handling the current rating and another for permissions.
Lastly, we updated the <Rating /> component to use these values like so:
<Rating
value={avgRating}
precision={0.5}
onChange={async () => {
if(!permission) {
return alert(
"You have already rated this recipe, or you may need to login."
)
}
setPermission(false)
}}
/>
In the above example, we added the value property to set the initial rating. The precision property allows us
to configure the precision of the rating by increments. Lastly, the onChange event will run when a new rating is
added.
The function passed into the onChange event is asynchronous since we'll be sending data to the WordPress server.
Before doing so, we're checking the user's permission. If they have rated before or are not logged in, they will not be
able to rate a recipe. Afterward, we're changing their permission to prevent users from rating the recipe multiple
times.
register_rest_route('up/v1', '/rate', [
'methods' => WP_REST_Server::CREATABLE,
'callback' => 'up_rest_api_add_rating_handler',
'permission_callback' => 'is_user_logged_in'
]);
We don't want anyone to rate a recipe. It should be limited to authenticated users. Next, we created the
up_rest_api_add_rating_handler() function:
function up_rest_api_add_rating_handler($request) {
$response = ['status' => 1];
$params = $request->get_json_params();
if (
!isset($params['rating'], $params['postID']) ||
empty($params['rating']) ||
empty($params['postID'])
) {
return $response;
}
$response['status'] = 2;
return $response;
}
Lastly, we updated the frontend.js file to handle the response if the rating was a successful:
if(response.status === 2) {
setAvgRating(response.rating)
}
If the rating was a success, we'll update the <Rating /> component to render the new average rating. However,
this data isn't available from the response. In a future lecture, we'll ad this to the response.
We're storing the new rating, ID of the post, and ID of the user. The rating is surrounded with the floatval() and
round() function. This is to make sure the rating will be suitable for inserting into the database. After setting up
the data, we wrote a query to select records from the recipe_ratings table.
global $wpdb;
$wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}recipe_ratings
WHERE post_id=%d AND user_id=%d",
$postID, $userID
));
We're using the $wpdb->get_results() function to perform a query. Before doing so, we're using the $wpdb-
>prepare() function to make sure that the query is secure. WordPress will strip malicious characters from the
values to prevent an SQL injection. This function accepts the query and the data that will be inserted into the query.
The query can be written with placeholders for the data. Three placeholders are supported:
%d - Number
%s - String
%f - Float
After creating the query, we're checking the num_row property to check if the query found any records. If it did,
that means the user has rated a recipe. If there's a record, we returned the response.
if($wpdb->num_rows > 0) {
return $response;
}
Resources
wpdb
$wpdb->insert(
"{$wpdb->prefix}recipe_ratings",
[
'post_id' => $postID,
'rating' => $rating,
'user_id' => $userID
],
['%d', '%f', '%d']
);
Next, we recalculated the average rating for a recipe. This time, we're using the $wpdb->get_var() function to
retrieve a single value from a query instead of columns. In this example, we're using the AVG() SQL function to
help us calculate the average of a column value.
After grabbing the average, we updated the post metadata with the update_post_meta() function.
$avgRating = round($wpdb->get_var($wpdb->prepare(
"SELECT AVG(`rating`)
FROM {$wpdb->prefix}recipe_ratings
WHERE post_id=%d",
$postID
)), 1);
Lastly, we updated the response by adding the $avgRating variable to the $response array.
$response['rating'] = $avgRating;
global $wpdb;
$userID = get_current_user_id();
$ratingCount = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*)
FROM {$wpdb->prefix}recipe_ratings
WHERE post_id=%d AND user_id=%d",
$postID, $userID
));
We're performing a standard query. However, the selection will return the value from the COUNT() function. This
function will count the results from the selection. This function accepts the name of the column to count. In this case,
we're counting all columns.
After grabbing this info, we updated our template by adding a data attribute with the count.
Lastly, we updated the frontend.js file by grabbing this information and passing it onto our component.
render(
<RecipeRating
ratingCount={ratingCount}
/>,
block
)
The component is using the useEffect() function to check this information to disable the permissions like so:
useEffect(() => {
if(props.ratingCount) {
setPermission(false)
}
}, [])
do_action('recipe_rated', [
'postID' => $postID,
'rating' => $rating,
'userID' => $userID
]);
Next, we tested our hook by creating a custom plugin that sends out an email. In a file called example.php that we
created in the wp-content/plugins directory, we added the following code:
/*
* Plugin Name: Udemy Plus Emailer
*/
add_action('recipe_rated', function($data) {
});
We can listen to our custom hook like any other hook. In this example, we're outsourcing the function into an
anonymous function to save time. Inside the function, we prepared the data:
$post = get_post($data['postID']);
$authorEmail = get_the_author_meta('user_email', $post->post_author);
$subject = 'Your recipe has received a new rating';
$message = "Your recipe {$post->post_title} has received a
rating of {$data['rating']}.";
We're grabbing the post info since we need the ID of the user by using the get_post() function. This function
accepts the ID of the post, which was provided with the hook. To get the author's email, we used the
get_the_author_email() function, which accepts the field to grab and the ID of the author.
Lastly, we prepared the subject and message of the email. We can send an email with the wp_mail() function,
which accepts the email, subject, and message.
Otherwise, we prepared two blocks. The starter files for the blocks can be found in the resource section. After
creating the blocks, we updated the register-blocks.php file for registering both blocks like so:
$blocks = [
['name' => 'fancy-header'],
['name' => 'search-form', 'options' => [
'render_callback' => 'up_search_form_render_cb'
]],
['name' => 'page-header', 'options' => [
'render_callback' => 'up_page_header_render_cb'
]],
['name' => 'header-tools', 'options' => [
'render_callback' => 'up_header_tools_render_cb'
]],
['name' => 'auth-modal', 'options' => [
'render_callback' => 'up_auth_modal_render_cb'
]],
['name' => 'recipe-summary', 'options' => [
'render_callback' => 'up_recipe_summary_render_cb'
]],
['name' => 'team-members-group'],
['name' => 'team-member'],
];
Resources
Team Members Block Starter Files
Single Team Member Block Starter Files
<InnerBlocks
orientation="horizontal"
allowedBlocks={[
'udemy-plus/team-member'
]}
/>
In the example, we are adding two properties. The orientation property will configure the direction blocks can
be stacked. Blocks can be stacked vertically or horizontally. The next property is called allowedBlocks , which is
an array of blocks that can be inserted into the current block.
In the save() function of a block, the inner content can be rendered with the InnerBlock.Content
component. This component doesn't require configuration since it doesn't render a UI for inserting blocks. It simply
adds the blocks inserted into the current block.
<InnerBlocks.Content />
Resources
Inner Blocks Component
<InnerBlocks
orientation="horizontal"
allowedBlocks={[
'udemy-plus/team-member'
]}
template={[
[
'udemy-plus/team-member',
{
name: 'John Doe',
title: 'CEO of Udemy',
bio: 'This is an example of a bio.'
}
],
['udemy-plus/team-member'],
['udemy-plus/team-member']
]}
// templateLock="all" // Cant add or rearrange blocks
// templateLock="insert" // Cant add but can rearrange blocks
/>
The template property is an array of blocks to insert into the component. A block is represented with an array
where the first item is the name of the block and the second item is an object of attributes.
In addition to modifying the template, we can lock the template with the templateLock property. By default, the
existing blocks can be removed, and new ones can be added. Setting this property to all will lock the template
completely. Whereas the insert value will prevent new blocks from being added, but the current set of blocks
can be rearranged.
<RangeControl
label={__('Columns', 'udemy-plus')}
onChange={columns => setAttributes({columns})}
value={columns}
min="2"
max="4"
/>
This component will be responsible for modifying the columns attribute for the Team Members Group block.
After modifying this component, we applied the class to the root element of our block through the
useBlockProps() function:
Inside our block, we inserted this component below the <img> tag.
<MediaPlaceholder
allowedTypes={['image']}
accept="image/*"
icon="admin-users"
onSelect={image => {
console.log(image)
}}
onError={err => console.error(err)}
/>
allowedTypes - An array of mime types. We can also just provide the category of the mime type. This
will limit the files that can be seen in the UI.
accept - The mime type of the file that will be uploaded to WordPress if the user decides to upload a file.
icon - An icon from the Dashicons icon set.
onSelect - An event that will run when an image has been selected or uploaded. The function will
provide the image as an argument.
onError - An event that will run when an error occurs, like an upload failure. The error is passed on as an
argument.
A mime type is another way to describe a file type. It's common for applications to check the mime type instead of
an extension. The format for a mime type is the category followed by the file type. The category and file type are
separated with a / character.
Resources
MediaPlaceholder
MIME Types
add_action('after_setup_theme', 'up_setup_theme');
Inside the includes directory, we created a file called setup.php with the up_setup_theme function
function up_setup_theme() {
add_image_size('teamMember', 56, 56, true);
}
In this function, we are running the add_image_size() function to register a new image size. This function has
four arguments:
You can check out the resource section of this lecture for more info on how the cropping mechanism works. Before
testing this code, you should regenerate the thumbnails with a plugin called Regenerate Thumbnails. The size will
only be applied to new images but not existing images. It's recommended to regenerate the thumbnails for existing
images to have the new image size.
However, our solution will not work just yet. Let's talk about why in the next lecture.
Resources
Image Cropping
A filter hook is a hook that accepts and returns a value. We can return a new value, the original value, or a modified
version of the original value. We can use a filter hook by using the add_filter() function like so:
add_filter('image_size_names_choose', 'up_custom_image_sizes');
We're using a filter hook called image_size_names_choose which will provide a list of valid sizes that can be
made available in a REST response. Next, inside the includes directory, create a file called image-sizes.php with the
function defined.
function up_custom_image_sizes($sizes) {
return array_merge($sizes, [
'teamMember' => __('Team Member', 'udemy-plus')
]);
}
Inside this function, we're given an array of sizes. We're adding onto this array by adding our custom size where the
key is the name of the size, and the value is a human-readable name. We're using the array_merge() php
function to merge the two arrays into one. Lastly, the merged array is returned.
After adding our size, the custom size will appear in the HTTP response for an image. We can use this information to
render the image in the block.
onSelect={image => {
setAttributes({
imgID: image.id,
imgAlt: image.alt,
imgURL: image.sizes.teamMember.url
})
}}
We are updating the attributes with the values from the image object. Next, we updated the <img> tag above
the <MediaPlaceholder /> component:
``jsx { imgURL && {imgAlt} }
If an image has been selected, the image will be rendered on the page. We should also
toggle the `<MediaPlaceholder />` component. However, we can take a different solution
by using the `disableMediaButtons`. By setting this property to `true`, the component
is hidden.
```javascript
disableMediaButtons={imgURL}
Even though this attribute is a string, strings are converted into booleans where an empty string is false , and a
non-empty string is true .
onSelectURL={url => {
setAttributes({
imgID: null,
imgAlt: null,
imgURL: url
})
}}
We're using this argument to update the attributes. We're resetting the other attributes just in case since the image is
not stored locally on the server.
WordPress leverages a browser feature called a blob. A blob is an object that represents a file. These files are
temporary and have URL that we can use to display an image in the browser. A blob is given to our event handler,
which doesn't contain the sizes or alt properties. This causes our function to fail.
We need to update our function to handle blobs and wait for the image upload to be completed. We will handle this
task in the next lecture.
Next, we updated the function for the onSelect event with the following code:
onSelect={image => {
let newImgURL = null;
if(isBlobURL(image.url)) {
newImgURL = image.url
} else {
newImgURL = image.sizes ?
image.sizes.teamMember.url :
image.media_details.sizes.teamMember.source_url
}
setAttributes({
imgID: image.id,
imgAlt: image.alt,
imgURL: newImgURL
})
}}
In the above example, we're checking if the image.url property contains a blob URL. If it does, we are storing the
URL in a variable. Otherwise, we're grabbing this information from the images.sizes.teamMember.url or
images.media_details.sizes.teamMember.source_url function.
WordPress stores the URL in different locations based on if the image is being uploaded or being selected from the
media library. You should always check where the URL is before storing it.
<svg class="components-spinner"></svg>
The spinner does not have absolute positioning. To move the spinner above the image, I used absolute positioning in
my CSS like so:
.wp-block-udemy-plus-team-member .components-spinner {
position: absolute;
top: 15px;
left: 9px;
}
In our file, we imported the useState function from the @wordpress/element package.
Next, we created state for storing the image URL in the edit function:
{
imgPreview &&
<img src={imgPreview} alt={imgAlt} />
}
{isBlobURL(imgPreview) && <Spinner />}
<MediaPlaceholder
onSelect={image => {
let newImgURL = null;
if(isBlobURL(image.url)) {
newImgURL = image.url
} else {
newImgURL = image.sizes ?
image.sizes.teamMember.url :
image.media_details.sizes.teamMember.source_url
setAttributes({
imgID: image.id,
imgAlt: image.alt,
imgURL: newImgURL
})
}
setImgPreview(newImgURL)
}}
disableMediaButtons={imgPreview}
/>
The setAttributes() function was moved into the else block since we don't want to update the attributes
unless we have a valid HTTP URL. At the end of the function, we're updating the state using providing the
newImgURL variable.
WordPress provides a function for releasing memory for a blob called revokeBlobURL from the
@wordpress/blob package.
Next, we called this function at the end of the else block in the onSelect event. This function has one
argument, which is a valid blob URL. If an invalid URL is provided, an error will not be thrown. We are providing this
function with the URL from the state.
revokeBlobURL(imgPreview)
Next, we imported a component called MediaReplaceFlow that will generate a button for opening the media
library from the same package.
<BlockControls>
<MediaReplaceFlow
name={__('Replace Image', 'udemy-plus')}
mediaId={imgID}
mediaURL={imgURL}
accept="image/*"
allowedTypes={[ 'image' ]}
onError={err => console.error(err)}
onSelect={selectImage}
onSelectURL={selectImageURL}
/>
</BlockControls>
The functions for the events were outsourced to variables since they're the same as the functions for the
MediaPlaceholder component's events. This component's events were updated to use these events too.
Next, we added this component inside the BlockControls component with the following properties.
<ToolbarButton
onClick={() => {
setAttributes({
imgID: 0,
imgURL: '',
imgAlt: ''
})
setImgPreview('')
}}
>
{__('Remove Image', 'udemy-plus')}
</ToolbarButton>
There's not much going on here besides resetting the attributes and image preview state to their original values. The
onClick event is emitted when the button is clicked.
In addition to adding the toolbar, we added the group property to the BlockControls component to add
separators to our buttons. This helps make them stand out from the other buttons on the toolbar.
``jsx ...
In this lecture, I gave you an exercise to toggle the Image Alt field when an image
has been added. If an image doesn't exist, there isn't a point in adding this field.
The solution to this exercise was to add the following conditions:
```jsx
{ imgPreview && !isBlobURL(imgPreview) && <TextareaControl /> }
{
"providesContext": {
"udemy-plus/image-shape": "imageShape"
}
}
It's recommended to give your values unique IDs. The most common format is the name of the plugin followed by
the name of the attribute.
Next, we update children blocks to accept this context in the block.json file like so:
{
"usesContext": ["udemy-plus/image-shape"]
}
Lastly, we could update edit() function's argument list to accept the context object to grab the data from the
parent block like so:
We defined a variable for the image's classes. It's recommended to add a class that starts with wp-image-
followed by the ID. This is for other plugins to select specific media items. Not required, but recommended. Lastly,
we added another class for the image shape.
You can refer to the CSS file for the list of classes for applying shapes to the image. We're using CSS clip paths to
generate different shapes. In the resource section of this lecture, you will find a link for generating your own custom
paths.
Resources
Clip Paths
Lecture 255: Preparing the Social Media Links
In this lecture, we began preparing the social media links by updating the attribute and looping through the
attribute. Firstly, we updated the socialHandles attribute by adding a default value:
{
"socialHandles": {
"type": "array",
"default": [
{ "url": "https://facebook.com/udemy", "icon": "facebook" },
{ "url": "https://instagram.com/udemy", "icon": "instagram" }
]
}
}
In the array, we are adding an object to represent each link. Each object will have a URL and icon. These icons are
taken from Bootstrap's icon set. Check out the resource section for a link to this icon set.
In <div> tag with the class social-links , we looped through the attribute's value with the map() method.
We're accepting the index argument to set the key attribute on the <a> element. React can have a difficult
time keeping track of multiple looped elements. To help it identify specific elements, we can associate a specific item
from the array by using the key attribute. The value for this attribute should be a unique value from the object. In
this case, we're using the index.
Resources
Bootstrap Icons
The Tooltip component will handle rendering the tooltip whenever a mouse hovers over an element. The Icon
component will render an icon from WordPress's Dashicon font set.
Next, we added the button below the other links like so:
setAttributes({
socialHandles: [
...socialHandles, {
icon: 'question',
url: ''
}
]
})
}}
>
<Icon icon="plus" />
</a>
</Tooltip>
The Tooltip component must surround the element it should be applied to. We can set the message by
configuring the text property. Inside this component, we added an <a> element with a click event to add a new
item to the socialHandles array.
Lastly, we're inserting the icon with the Icon component. An icon can be configured with the icon property. The
value for this property must be a valid icon from the Dashicon set.
Next, we used this information to toggle the button for adding a new link like so:
{
isSelected &&
<Tooltip></Tooltip>
}
By default, we're setting the state to null to not display the form initially. Afterward, we updated the template by
adding the onClick event to change the state when a link is clicked.
<a
href={handle.url}
key={index}
onClick={e => {
e.preventDefault()
setActiveSocialLink(
activeSocialLink === index ? null : index
)
}}
className={activeSocialLink === index && isSelected ? 'is-active' : ''}
>
<i class={`bi bi-${handle.icon}`}></i>
</a>
On the link, we're setting the state to the current index. However, if the same link is clicked twice, we'll toggle the
state back to null to hide the form. In addition, we're applying the isActive class to the link if the current link
and active link match to highlight the link. This lets the client know what link they're editing.
Next, we updated the link for adding a new social media link by changing the state to the last item in the array like
so:
<a
href="#"
onClick={e => {
e.preventDefault()
setAttributes({
socialHandles: [
...socialHandles, {
icon: 'question',
url: ''
}
]
})
setActiveSocialLink(socialHandles.length);
}}
>
<Icon icon="plus" />
</a>
Lastly, we created a container for the form that will be conditionally rendered:
{
isSelected && activeSocialLink !== null &&
<div className="team-member-social-edit-ctr">
</div>
}
Two TextControl components were added for modifying the icon and URL, respectively. In the onChange
event of either component, we're updating the attribute by storing a copy of the array and current item. WordPress
does not recommend directly updating an attribute. Therefore, we should create copies.
After doing so, we can save the copy as the attribute value with the setAttributes() function.
onChange={url => {
const tempLink = {...socialHandles[activeSocialLink]}
const tempSocial = [...socialHandles]
tempLink.url = url
tempSocial[activeSocialLink] = tempLink
Next, we added the Button component. On this component, we added the isDestructive property to
change the color of the button to red. Inside the onClick event, we're removing an item from the array by using
the splice() function, which accepts the index of the item to remove and the number of items to remove. Lastly,
we're resetting the activeSocialLink state to null since the item will be deleted from the array.
<Button
isDestructive
onClick={() => {
const tempSocial = [...socialHandles]
tempSocial.splice(activeSocialLink, 1)
setAttributes({
socialHandles: tempSocial
});
setActiveSocialLink(null)
}}
>
{__('Remove', 'udemy-plus')}
</Button>
{
"imageShape": {
"type": "string",
"default": "hexagon"
}
}
setAttributes({
imageShape: context["udemy-plus/image-shape"],
})
Lastly, we're able to use this attribute from within the save() function:
After grabbing everything we needed, the template was updated to render the image and social media links.
{
"name": {
"type": "string",
"source": "html",
"selector": ".author-meta strong"
},
"title": {
"type": "string",
"source": "html",
"selector": ".author-meta span"
},
"bio": {
"type": "string",
"source": "html",
"selector": ".member-bio p"
}
}
The source property can be set to html to select the inner contents of an HTML tag. After adding this property,
we can add the selector property to select a specific element. This property accepts any valid CSS selector.
Afterward, we updated the image attributes except for the ID since we're not storing it in the template:
{
"imgAlt": {
"type": "string",
"default": "",
"source": "attribute",
"selector": "img",
"attribute": "alt"
},
"imgURL": {
"type": "string",
"source": "attribute",
"selector": "img",
"attribute": "src"
}
}
Instead of using html as the source, we're using attribute , which allows us to grab an attribute's value from
an HTML attribute. If the source is an attribute, we must add the attribute property to specify an attribute from
the element selected by the selector property.
{
"socialHandles": {
"type": "array",
"default": [
{ "url": "https://facebook.com/udemy", "icon": "facebook" },
{ "url": "https://instagram.com/udemy", "icon": "instagram" }
],
"source": "query",
"selector": ".social-links a",
"query": {
"url": {
"source": "attribute",
"attribute": "href"
},
"icon": {
"source": "attribute",
"attribute": "data-icon"
}
}
}
}
We can set the source property to query to create an array of objects where each property in the object can
have a custom query for grabbing data from a template. The queries can be created within the query property like
we're doing above. Unlike before, we do not need to add the selector to each inner query as WordPress will use
the parent query.
{
transforms: {
from: [
]
}
}
As mentioned earlier, we can transform from and to a block. We can transform another block into this block by
adding the from array. Inside this array, we can provide objects for each block that should be transformed into this
block like so:
{
{
type: 'block',
blocks: ['core/gallery'],
transform({ images, columns }) {
const teamMemberBlocks = images.map(image => {
return createBlock('udemy-plus/team-member', {
imgID: image.id,
imgURL: image.url,
imgAlt: image.alt
})
})
if(!columns) {
columns = 3
} else if(columns < 2) {
columns = 2
} else if(columns > 4) {
columns = 4
}
return createBlock('udemy-plus/team-members-group', {
columns
}, teamMemberBlocks)
}
}
}
In this object, we can specify three properties.
Before beginning the process of transformation, you'll need the createBlock function from the
@wordpress/blocks package.
The transform method must return a valid block instance. This function will handle create an instance of any
block that we want. Inside our function, we created Team Member blocks for each image in the gallery by looping
through them. The createBlock function has three arguments, which are the name of the block, the block's
attributes, and inner contents.
After preparing each team member, we returned the Team Member Group block with the columns and children
blocks.
{
transforms: {
from: [
{
type: 'block',
blocks: ['core/image'],
isMultiBlock: true,
transform(attributes) {
// ...
}
}
]
}
}
In addition, the argument for the transform function will be different than before. It'll be an array of objects
where each object contains the attribute for the blocks selected by the client.
Inside this setting, we can configure the attributes with the attributes option and add inner blocks by adding
the innerBlocks option. In the array of the innerBlocks option, you can add an object to represent each
block. In the object, we can specify the block with the name option and change the attributes through the
attributes object.
After adding this example, Gutenberg will render a real-time preview of a block after hovering over it. This can be a
great way to give users a better idea of what a block will look like after adding it to a page.
Section 14: Querying Posts
In this section, we started creating blocks that will create custom queries for posts.
Resources
Popular Recipes Starter Files
<QueryControls
numberOfItems={count}
minItems={1}
maxItems={10}
onNumberOfItemsChange={count => setAttributes({count})}
/>
In this example, we are adding four properties for adding a slider for modifying the post count. They're the following:
Keep in mind, this component creates inputs for modifying a query but does not actually create the query itself.
Resources
QueryControls Block
After grabbing the terms, we formatted the data. The QueryControls component requires that the data be
formatted as an object instead of an array, which is what's returned by the useSelect() function. We began the
conversion with the following code:
const suggestions = {}
terms?.forEach(term => {
suggestions[term.name] = term;
})
In the above example, there are two note-worthy things going on. Firstly, we're using optional chaining. The terms
variable will initially be empty until the request is complete. If the value is empty, the forEach() method will not
be available, thus causing an error. We can use the ? after the variable to instruct the browser not to run the
function if the value is empty.
Secondly, we're adding a new property to the suggestion object with the square bracket notation. A dynamic
property name can be created with this syntax, where the name can be passed into the square brackets.
On the QueryControls component, we applied these suggestions with the categorySuggestions property.
<QueryControls
numberOfItems={count}
minItems={1}
maxItems={10}
onNumberOfItemsChange={count => setAttributes({count})}
categorySuggestions={suggestions}
onCategoryChange={newTerms => {
console.log(newTerms)
}}
/>
We also have to add the onCategoryChange event to handle updates. Otherwise, the input will not appear.
newTerms.forEach(cuisine => {
if(typeof cuisine === 'object') {
return newCuisines.push(cuisine)
}
if(cuisineTerm) newCuisines.push(cuisineTerm)
})
In the above example, we began looping through the newTerms variable to push the terms into the
newCuisines array. The newTerms variable will contain existing and new terms. We handled both scenarios by
checking if the item in the current iteration is an object. If it is, it's an existing term, which we just pushed into the
array.
If it's a new term, we searched for the term object from the terms variable to grab the whole object. New terms
are added as strings instead of whole objects. We need the object if we want to query the database for posts.
In the above example, we're using the find() function to search for the term object. This function accepts a
function for searching for a value within an array. If true is returned, the value is returned. Otherwise, JavaScript
will continue looping through the array until a value is found. In this example, we're searching for the value by its
name.
Lastly, we pushed the term into the object if a term was found. After the new array was ready, we updated the
cuisines attribute.
Unfortunately, the endpoint will not order the posts by metadata. We fixed this issue in the next lecture.
Resources
Posts Endpoint
The third and fourth arguments will set the priority and number of arguments accepted by the function. Multiple
functions can listen to the same hook. The priority can determine the order of the functions. A lower number means
a higher priority. By default, the priority is 10 .
The fourth argument is the number of arguments that our function can accept. By default, WordPress assumes that
our function will accept one argument, but this hook will send two arguments. Therefore, we must support multiple
arguments by specifying the number of arguments that'll be accepted by the function.
After adding the hook, we defined the up_rest_recipe_query function in a file called includes/rest-
api/recipe-query-mod.php.
if (isset($orderby)) {
$args["orderby"] = "meta_value_num";
$args["meta_key"] = "recipe_rating";
}
return $args;
}
In the above example, we're grabbing the orderByRating parameter. By default, we should not assume that
users want to order posts by a metadata key. Instead, we're going to check if this parameter is sent with the request.
If it is, we'll change the order of posts by adding the orderby and meta_key fields.
The orderby field will tell WordPress to order the fields by a numeric metadata field. The meta_key field will
provide the name of the metadata field to order by. These fields will be applied to a class called WP_Query , which
WordPress uses to query posts from the database. All fields documented can be applied to the $args variable.
Lastly, we returned this $args variable since this is a filter hook. In our Postman test, we added the following
parameters:
Resources
WP Query
Lecture 271: Querying Posts in a Block
In this lecture, we began querying the posts by using the useSelect() function. First, we had to update our array
to be an array of term IDs since that's what the REST API is expecting. We used this map() function to accomplish
this:
In this function, we're using the getEntityRecords() function to initiate the query. We're not limited to
querying taxonomy terms. We can grab posts by setting the first argument to postType and the second argument
to the name of the post type.
Afterward, we began modifying the parameters of the query by limiting the results, embedding the image/author,
filtering posts by the cuisine IDs, and, lastly, ordering the results by the metadata.
The useSelect() function has a third argument, which will rerun the query if specific variables are updated. We
can provide an array of variables to watch. In this example, we're watching the count and cuisines attributes.
If either variable updates, the request is sent again.
{posts?.map(post => {
})}
const featuredImage =
post._embedded &&
post._embedded['wp:featuredmedia'] &&
post._embedded['wp:featuredmedia'].length > 0 &&
post._embedded['wp:featuredmedia'][0]
Before grabbing the image, we're checking if the image exists since not all posts are required to have an image. The
image can be found within the post._embedded['wp:featuredmedia'] array.
Lastly, we began rendering the template from within the loop like so:
<div class="single-post">
{
featuredImage &&
<a class="single-post-image" href={post.link}>
<img
src={
featuredImage.media_details.sizes.thumbnail.source_url
}
alt={featuredImage.alt_text}
/>
</a>
}
<div class="single-post-detail">
<a href={post.link}>
<RawHTML>
{post.title.rendered}
</RawHTML>
</a>
<span>
by <a href={post.link}>{post._embedded.author[0].name}</a>
</span>
</div>
</div>
In the above example, we're rendering the image, title, and author. For the title, the title may store HTML. If that's the
case, the tags will be escaped. To prevent this behavior, we're using a React component called RawHTML , which will
allow users to render HTML. This component can be rendered from the @wordpress/element package.
function up_popular_recipes_cb($atts) {
$title = esc_html($atts['title']);
$cuisineIDs = array_map(function($term) {
return $term['id'];
}, $atts['cuisines']);
}
For the cuisine IDs, we're looping through the $atts['cuisine'] attribute with the array_map() function
since the attributes store objects. We don't need objects. We need an array of numeric IDs. So, we used this function
to help us generate an array of IDs. The first argument of this function is the function that will handle looping
through each item and grabbing a specific item on each iteration. The second argument is the array itself.
After preparing the data, we began building the query. Unlike before, we must manually create the query, whereas
previously, the query was built by WordPress. Custom queries are created with the WP_Query class. We prepared
the parameters in a variable called $args .
$args = [
'post_type' => 'recipe',
'posts_per_page' => $atts['count'],
'orderby' => 'meta_value_num',
'meta_key' => 'recipe_rating',
'order' => 'DESC'
];
In the example above, we're filtering the results by the post type, limiting the results, and lastly, ordering the results
by their rating. Up next, we checked if the $cuisineIDs variable is empty.
if (!empty($cuisineIDs)) {
$args['tax_query'] = [
[
'taxonomy' => 'cuisine',
'field' => 'term_id',
'terms' => $cuisineIDs,
]
];
}
If it isn't, we are modifying the query further by filtering the results by the IDs of the taxonomy terms. We can
perform this task by adding the tax_query parameter. The value for this parameter is an array of arrays where
each inner array can have configuration settings for a specific taxonomy.
In this example, we're filtering the posts by the cuisine taxonomy. If a post contains a specific taxonomy, the post
will be included in the results. The taxonomy terms are added to the query by setting the field option to
term_id , which tells WordPress to use the IDs of the terms and setting the terms parameter to a list of term
IDs.
Lastly, we initiated a query by creating a new instance of the WP_Query class and passing on the arguments.
?>
<?php
}
}
In this example, we're using the have_posts() function to check if the query has results. If it does, we began the
loop. Once again, we're using the have_posts() function. This function performs another job, which is to check if
all the posts have been looped through. If they have, the loop will stop running.
Inside the loop, we're running the the_post() function to check if we're on the first iteration. If so, WordPress will
grab the first post from the query. Otherwise, it'll move on to the next post until there are no more posts that have
been looped through. This function should always appear at the beginning of the loop. You may run into strange
behavior if you don't.
After running the loop, we used a series of functions to render info on the post.
We don't need to provide information on the post to these functions since they'll check if they're running in the loop.
WordPress does most of the heavy lifting for us.
After every post had been looped through, we ran the following function:
wp_reset_postdata();
WordPress will always query the database for posts. This is considered the main query. Custom queries are
considered secondary queries. If we create a secondary query, WordPress will lose focus of the main query. We must
run this function after we're finished with our secondary query. Otherwise, other developers may be grabbing post
data from query instead of the main query, which can lead to inconsistent behavior.
Resources
The Loop
Template Tags
Resources
Daily Recipe Starter Files
Lecture 276: Transients
In this lecture, we created a function for temporarily storing a random recipe in the database. WordPress has a
feature called transients, which allows us to store in the database with an expiration. In a file called generate-daily-
recipe.php, we defined a function called up_generate_daily_recipe() .
function up_generate_daily_recipe() {
global $wpdb;
$id = $wpdb->get_var(
"SELECT ID FROM {$wpdb->posts}
WHERE post_status='publish' AND post_type='recipe'
ORDER BY rand() LIMIT 1"
);
return $id;
}
In this function, we're querying the database with the $wpdb->get_var() function. The query will select the ID
from the posts table. WordPress has a property called $wpdb->posts for storing the name of the posts table.
Next, we're filtering posts that are published and are the recipe post type. Lastly, we're ordering the results with
the ORDER BY keywords with the rand() function, which will randomize the order. The results can be limited
with the LIMIT keyword.
After querying the database for a recipe, we're using the set_transient() function to store a value in the
database. This function has three arguments, which are the name of the transient, the value, and the expiration time.
WordPress has a few constants for creating the expiration time, which we used in this example. Check out the
resource section of this lecture for more info on transients.
Resources
Transients
register_rest_route('up/v1', '/daily-recipe', [
'methods' => WP_REST_Server::READABLE,
'callback' => 'up_rest_api_daily_recipe_handler',
'permission_callback' => '__return_true'
]);
This code snippet will register a route with the following endpoint: up/v1/daily-recipe . Next, we created a file
called daily-recipe.php with the function for handling the request.
function up_rest_api_daily_recipe_handler() {
$response = [
'url' => '',
'img' => '',
'title' => ''
];
$id = get_transient('up_daily_recipe_id');
if(!$id){
$id = up_generate_daily_recipe();
}
$response['url'] = get_permalink($id);
$response['img'] = get_the_post_thumbnail_url($id, 'full');
$response['title'] = get_the_title($id);
return $response;
}
In the function, we're using the get_transient() function to retrieve the transient from the database. This
function accepts the name of the transient. If the transient doesn't exist, that means it either expired or a daily recipe
hasn't been retrieved yet. If there is no recipe, we generate a new one with the up_generate_daily_recipe()
function.
Afterward, we updated the response with the URL, image, and title. We're using template tags while passing on the
ID of the post since we're not in a loop.
Both functions produce a URL to an image but have different parameters. The
the_post_post_thumbnail_url() does not accept the ID. This means you must resort to the other function
for outputting the URL. Not all functions are like this. Some pairs will accept IDs. It varies from function to function.
You should always refer to the documentation to catch these gotchas.
Resources
the_post_thumbnail_url()
get_the_post_thumbnail_url()
I showed you an example by measuring the time it took to run a raw SQL query and a query created by the
WP_Query class. Generating a random recipe is faster with raw SQL than with WordPress's query system. In most
cases, the WordPress query system is good enough, but if it's too slow, you may want to consider writing a raw SQL
query.
Section 15: Exploring More Block Features
In this section, we created an entirely new plugin for a single block and submitted it to the WordPress repository.
We're using a new option called namespace , which will allow us to configure the namespace of the plugin. In this
example, we're changing the namespace to u-plus to match the current namespace of our project.
After creating the plugin, we overrode the files with the files in the resource section of this lecture. Check it out for
the complete list of modified files.
Resources
Alert Box Starter Files
In the project, we are given a file called style.css for containing the block's styles. Inside this file, we have the
following code:
div.wp-block-u-plus-alert-box {
/* div.wp-block-u-plus-alert-box.is-style-accented */
&.is-style-accented {}
/* div.wp-block-u-plus-alert-box .dashicon */
.dashicon {}
}
In this example, we're not limited to adding properties. We can use additional selectors that will be converted into
valid CSS selectors. Children selectors will have the parent selector prefixed to them. We can reference the parent
selector with the & character.
This is just one feature, but nested selectors provide clarity and act as a shortcut for writing CSS. There are more
features worth checking. Check out the resource section for a link to the official site for SASS.
Resources
SASS
{
"supports": {
"html": false,
"align": true
}
}
In this example, we are adding the supports option to the block file to begin configuring the features to
enable/disable. First, we're disabling the html option to prevent users from modifying our block with HTML. Next,
we're setting the align option to true to enable alignment options on our block.
There are 5 alignment options, which are left, center, right, wide, and full. You may not want to support all options. In
this case, you can set the align option to an array of alignment options like so:
{
"supports": {
"align": ["full"]
}
}
WordPress will apply classes to the parent element of the block with the name of the alignment option. Here's an
example of our block using these styles:
.alignfull {
border-radius: 0;
}
Resources
Supports
In this example, we're changing the properties of the original block. In the attributes object, we can modify the
class name by adding the className property. Technically not an attribute, WordPress treats it as so.
In addition, we imported some icons. You can find these icons in the resource section of this lecture. In the same file,
we imported it with the following code:
Make sure to update the block.json file to remove the icon since we're setting the icon to an SVG image.
Resources
Icons
{
"styles": [
{
"name": "regular",
"label": "Regular",
"isDefault": true
},
{
"name": "accented",
"label": "Accented"
}
]
}
In this option, we can add an array of styles that are represented as object. In each object, we can add a name and
label. The name property will be the name of the class. All classes are prefixed with is-style- . The label
property will appear on the editor. In addition, we can add a property called isDefault to set the default style of
a block.
Resources
Block Styles
If the templates do not match, WordPress will fail the validation. If that's the case, we can help WordPress upgrade a
block, which is what we do in the next lecture.
registerBlockType('u-plus/alert-box', {
deprecated: [v1]
})
Inside this file, we exported an object with the following attributes: supports , attributes , save . We do not
need to provide every attribute. The values in each of these properties must contain the original values from the old
block.
Resources
Deprecation
For this case, you can use a function called omit from the Lodash library to help you merge the current set of
attributes and omit specific properties from an object like so:
{
attributes: {
...omit(blockData.attributes, ['backgroundColor']),
bgColor: {
type: "string",
default: "#4F46E5"
}
}
}
In addition, you need to help WordPress manually update the attributes for the current version of the block with the
older attributes by defining a migrate() function.
migrate(attributes) {
return {
...omit(attributes, ['bgColor']),
backgroundColor: attributes.bgColor
}
}
The migrate() function will be provided the attributes from the current config object (not the latest config object
of the block). We can use the values to set the values for the newer version of the block.
In the deprecations option of a block's config, you must provide the config objects from newest to oldest. It's
likely that users aren't on the oldest possible version of a block. To boost the performance of validation, you should
order them from newest to oldest like so:
deprecations: [v2,v1]
Lastly, the older versions of the block should contain the upgrade code from the newer versions. Otherwise, block
validation will fail. This does mean having repetitive code, but you must include the upgrade code in all versions.
Resources
Lodash
https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/#12-public-facing-pages-on-
wordpress-org-readmes-must-not-spam
Resources
Plugin Readme
Plugin Submission
Section 16: Custom Admin Pages
In this section, we created custom admin pages for editing our custom plugin settings.
Dashboard Widgets API - An API for adding widgets to the WordPress admin dashboard.
Database API - An API for interacting with the database of WordPress
File Header API - An API for processing files with file headers.
File System API - An API for securely connecting with a server for transferring files.
HTTP API - An API for sending HTTP requests with PHP.
Metadata API - An API for adding metadata to posts, users, comments, and terms.
Options API - An API for adding settings or data to the database.
Plugin API - An API for working with hooks in WordPress.
Quick tags API - An API for adding buttons to the classic editor.
Rewrite API - An API for modifying the URL structure of a WordPress site.
Settings API - An API for generating forms for modifying options.
Shortcodes API - An API for adding custom shortcodes to a site.
Theme Modification API - An API for adding theme options.
Theme Customization API - An API for adding fields and panels to the WordPress customizer.
Transients API - An API for storing data temporarily in the database.
Widgets API - An API for adding widgets to the sidebar of a theme.
XML-RPC WordPress API - An API for exposing our site to mobile and desktop apps.
Resources
Core APIs
Resources
Options API
$options = get_option('up_options');
if(!$options) {
}
We're retrieving an option from the database by using the get_option() function. This function has one
argument, which is the name of the option. If an option doesn't exist, a false value is returned. We are using a
conditional statement to check the existence of this value. If it doesn't, we can proceed to add our options to the
database.
add_option('up_options', [
'og_title' => get_bloginfo('name'),
'og_image' => '',
'og_description' => get_bloginfo('description'),
'enable_og' => 1
]);
We can insert new data by using the add_option() function. This function has two arguments, which are the
name of the option and the value. In this example, we are going to create options for modifying the open graph
settings of a site.
Open graph is a protocol for helping social media platforms generate a preview of site. We are going to allow users
to modify the title, image, and description. In addition, we'll allow users to disable this option if they have another
plugin for adding open graph tags.
Resources
Open Graph
Open Graph Preview
add_action('admin_menu', 'up_admin_menus');
In the includes/admin folder, we created a file called menus.php with the function for handling this hook.
function up_admin_menus() {
add_menu_page(
__('Udemy Plus', 'udemy-plus'),
__('Udemy Plus', 'udemy-plus'),
'edit_theme_options',
'up_plugin_options',
'up_plugin_options_page',
plugins_url( 'letter-u.svg', UP_PLUGIN_FILE )
);
}
In this function, we're running a function called add_menu_page() to add a new menu. It has six arguments.
Capabilities can be thought of as the permissions for a certain action. You can refer to the resource section for a
complete list of capabilities. As for the icon, we're using the plugins_url() function for generating a HTTP to
the icon. This function has two arguments, which are the name of the file and the plugin file.
We defined the constant in the second argument in the main plugin file as so:
define('UP_PLUGIN_FILE', __FILE__);
function up_plugin_options_page() {
?>
<div class="wrap">
Test
</div>
<?php
}
We're using a class called wrap , which will position the content perfectly on the page next to the menu. You should
always wrap your content with an element that has this class.
Resources
add_menu_page() Function
Roles and Capabilities
SVG Icon
After adding the template, we updated the <form> element with a few attributes:
On this element, we added the method attribute to set the HTTP method. The action attribute will tell the
browser where to send the form data. In this case, we're sending the data to a file called admin-post.php. This file
can be found in the wp-admin directory. It's the recommended file for sending form submissions.
Multiple forms can be sent to the same URL. To help WordPress identify our form, we can set the action to a
unique value. Without this input, we will not be able to intercept the request and handle the form submission.
Afterward, we invoked a function called wp_nonce_field() .
wp_nonce_field('up_options_verify');
Nonce stands for number used once. They're a feature for generating a unique value in the form that can be verified
on the backend. They're very difficult to spoof, so it prevents hackers from submitting our form without permission.
The last step we took was updating the inputs with their respective values from our custom option. For the final
input, we had a checkbox where we had to toggle the checked attribute. WordPress has a special function for
adding this attribute called checked() . We added it to the input like so:
This function accepts the two values to compare. If they match, the checked attribute is outputted onto the input.
Resources
WordPress Admin Styles
Admin Form
Nonces
add_action('admin_post_up_save_options', 'up_save_options');
print_r($_POST);
The $_POST variable will contain the form data submitted by a form. PHP handles this process for you. It'll be an
array of values, which we can output by using the print_r() function since the echo keyword cannot output
arrays or objects.
Resources
Admin_post_ Hook
if(!current_user_can('edit_theme_options')){
wp_die( __( 'You are not allowed to be on this page.', 'udemy-plus' ) );
}
In the above example, we're using the current_user_can() function to check a user's capabilities. We can pass
in a capability that the current user should have. If the function returns false , we will kill the script with the
wp_die() function. This function kills the script while outputting a message to the screen.
Afterward, we checked the nonce to verify that it was sent with the check_admin_referer() function.
check_admin_referer('up_options_verify');
This function accepts the nonce created by our form. If the nonce is invalid, the script will be killed.
wp_redirect( admin_url('admin.php?page=up_plugin_options&status=1') );
The wp_redirect() function handles redirecting the user. This function accepts a valid URL. In this example,
we're using the admin_url() function to help us generate a URL relative to admin dashboard. This function
accepts a path. In this instance, we're redirecting them to our custom admin page. This page exists under the
admin.php file, where the page query parameter should hold the slug of our page.
Lastly, we updated our page to output a message if the form was submitted successfully by checking if the status
query parameter exists in the URL. By default, PHP will store query parameters as an array in the $_GET variable.
add_action('admin_enqueue_scripts', 'up_admin_enqueue');
Lastly, we updated our function to accept an argument called $hook_suffix . This argument contains the name of
the current page.
function up_admin_enqueue($hook_suffix) {
if ($hook_suffix === "toplevel_page_up_plugin_options") {
wp_enqueue_media();
}
}
We used this argument to check if the user is viewing our custom admin page. If they are, we'll enqueue the files with
the wp_enqueue_media() function. This function will handle enqueueing all the files required for the media
library.
Lecture 299: Adding New Entry Files to Webpack
In this lecture, we created a new script file that's not related to blocks. Since that's the case, it will not be processed
to Webpack. However, we may want to run it through Webpack for optimization. In this case, we need to update the
Webpack configuration.
In the root directory of our plugin, we created a file called webpack.config.js. If this file exists, Webpack will
prioritize our configuration over WordPress's configuration file. However, we're not interested in overriding the
configuration by WordPress. We're simply interested in extending it.
We can extend the configuration by importing the configuration object by WordPress and spreading it into our
object.
export default {
...defaultConfig,
entry: {
...defaultConfig.entry(),
'admin/index': './src/admin',
}
}
In the above configuration, we're running the defaultCOnfig.entry() function to add WordPress's entry point
to our config object. Entry points refer to the files that Webpack should process. After adding WordPress's entry
point, we added our file to the list of files to process.
The property name represents where to store the file in the build. The extension can be excluded since Webpack will
add it for us. The property value is the path to the file.
1. Handle name
2. URL to the file
3. Dependencies
4. Version of the file
5. Whether to load the file in the header or footer
For the third and fourth arguments, we can use the file generated by Webpack. In the build of our files, Webpack will
create a PHP file. This file is an array of dependencies in our script and the version of the file. This array is returned by
the file.
Returning values are limited to functions. We can return values from files that will be returned by the include()
function.
Lastly, we called the wp_register_script() function like so:
wp_register_script(
'up_admin',
plugins_url('/build/admin/index.js', UP_PLUGIN_FILE),
$adminAsset['dependencies'],
$adminAsset['version'],
true
);
Resources
wp_register_script()
This function accepts an object of configuration settings. We can configure the title of the post, the text in the
button, and whether multiple images can be selected. The instance is returned by the function. We can interact with
the media library through the mediaFrame variable.
First, we opened the library whenever the user clicked on the button.
ogImgBtn.addEventListener('click', e => {
e.preventDefault()
mediaFrame.open()
})
Next, we added a custom event listener called select . This event is emitted when image is selected.
mediaFrame.on('select', () => {
const attachment = mediaFrame.state().get('selection').first().toJSON()
ogImgCtr.src = attachment.sizes.opengraph.url
ogImgInput.value = attachment.sizes.opengraph.url
})
In the event handler, we called the state() function to grab the data from the media library. Next, we chained the
get() function to select a specific piece of data from the collection of data called selection . This will return an
array of images selected by the user. Afterward, we chained the first() function to get the first item in the array.
Lastly, we called the toJSON() function to convert the data into an object.
After grabbing this information, we updated our elements with the URL from the image.
Resources
wp.media() Method
add_submenu_page(
'up_plugin_options',
__('Alt Udemy Plus', 'udemy-plus'),
__('Alt Udemy Plus', 'udemy-plus'),
'edit_theme_options',
'up_plugin_options_alt',
'up_plugin_options_alt_page'
);
Resources
Settings API
add_action('admin_init', 'up_settings_api');
This hook will run during the initialization of the admin dashboard. From this hook, we can register our option with a
group by using the register_setting() function. This function has two arguments, the name of our group and
the option to associate with the group.
register_setting('up_options_group', 'up_options');
The action attribute must be set to the options.php file. Unlike custom forms, forms created with the settings
API must send their data to this file. Lastly, we added our options group to the form with the
settings_fields() function.
settings_fields('up_opts_group');
This function has one argument, which is the name of the options group that the form will update. Any options
associated with this group will be updated.
add_settings_section(
'up_options_section',
esc_html__('Udemy Plus Settings', 'udemy-plus'),
'up_options_section_cb',
'up_options_page'
);
Afterward, we rendered this section of the page by using the do_settings_sections() function. This function
accepts the name of the section.
do_settings_sections('up_options_page');
add_settings_field(
'og_title_input',
esc_html__('Open Graph Title', 'udemy-plus'),
'og_title_input_cb',
'up_options_page',
'up_options_section'
);
function og_title_input_cb() {
$options = get_option('up_options');
?>
<input class="regular-text" name="up_options[og_title]"
value="<?php echo esc_attr($options['og_title']); ?>" />
<?php
}
As usual, we're grabbing the up_options option from the database. We're using this option to set the value of
the input. The most important thing to note down about this input is the name attribute. This attribute's value must
be the name of the option that will be updated in the database. If you're storing an array, you can add square
brackets to update a specific item from the array.
Resources
Settings API Input Fields
register_post_meta('', 'og_title', [
'single' => true,
'type' => 'string',
'show_in_rest' => true,
'sanitize_callback' => 'sanitize_text_field',
'auth_callback' => function() {
return current_user_can('edit_posts');
}
]);
Most of the options are familiar to us. I'm introducing two new options called sanitize_callback and
auth_callback . The sanitize_callback option allows us to sanitize the value during updates to the
metadata through the REST API. In this example, we're running the value through the sanitize_text_field()
function. As for the auth_callback option, this option allows us to limit who can update, add, or delete this
metadata. In this example, we're limiting permission to users who have the edit_posts capability.
In another metadata registration, we also looked at the default option for setting a default value:
register_post_meta('', 'og_override_image', [
'default' => false
]);
add_action('enqueue_block_editor_assets', 'up_enqueue_block_editor_assets');
In our function, we checked the current page since the Gutenberg editor will render for the post editing page or the
full-site editor. Our script should not be loaded for the full-site editor since we're trying to modify metadata for a
specific post.
$current_screen = get_current_screen();
if ($current_screen->base == 'appearance_page_gutenberg-edit-site') {
return;
}
We can use the get_current_screen() function to grab information related to the current page. This function
returns an object with a property called base with the name of the page.
We called the registePlugin() function to begin rendering a sidebar. This function has two arguments, the
name of the plugin and an object with a render() function for rendering content.
registerPlugin('up-sidebar', {
render() {
return (
<PluginSidebar
name="up-sidebar"
icon="share"
title={__("Udemy Plus Sidebar", "udemy-plus")}
>
Test
</PluginSidebar>
)
}
})
Within the function, we're returning the PluginSidebar component to render the sidebar. This component has
three properties for the name, icon, and title. This component will handle adding a button to the editor and toggling
the sidebar.
Resources
Sidebar Fields
This function works similarly to the useSelect() function. First, we must select a storage for accessing methods
to interact with the data from that store. In our case, functions for modifying metadata can be accessed from the
core/editor storage.
From this store, we're grabbing the editPost function for modifying the metadata. You can modify the metadata
like so:
editPost({
meta: {
meta: { og_title }
}
})
Resources
Data Reference
The MediaUploadCheck component will verify that the user has the proper capabilities for interacting with the
media library. We did not have to include this component before since the MediaPlaceholder component
performs that step for us. We added this component to the sidebar along with the MediaUpload component.
<MediaUploadCheck>
<MediaUpload
allowedTypes={['image']}
render={ ({ open }) => {
return (
<Button isPrimary onClick={ open }>
{__("Change Image", "udemy-plus")}
</Button>
)
}}
onSelect={ image => {
editPost({
meta: {
og_image: image.sizes.opengraph.url
}
})
}}
/>
</MediaUploadCheck>
$url = site_url('/');
Next, before updating the open graph data with information for a single post, we had to check if we were on a single
post with the is_single() function.
if(is_single()) {
}
If we're viewing a single post, the URL to a post can be grabbed with the get_permalink() function with the ID
of the post passed in.
$url = get_permalink($postID);
Lastly, the open graph preview can be configured with <meta> tags. These tags must have two attributes, the
property attribute for configuring the type of value and the content attribute for setting the value.
<meta property="og:title"
content="<?php echo esc_attr($title); ?>" />
<meta property="og:description"
content="<?php echo esc_attr($description); ?>" />
<meta property="og:image"
content="<?php echo esc_attr($image); ?>" />
<meta property="og:type" content="website" />
<meta property="og:url" content="<?php echo esc_attr($url); ?>" />
Resources
Neon CSS
Next, we called the registerFormatType() function, which will allow us to add a format to the toolbar.
registerFormatType('udemy-plus/neon', {
title: __("Neon", 'udemy-plus'),
tagName: 'span',
className: 'neon',
edit() {
}
})
This function has two arguments. The first argument is the name of the format. The second argument is a
configuration object. In this object, we can define the following properties:
return (
<RichTextToolbarButton
title={__('Neon', 'udemy-plus')}
icon='superhero'
isActive={isActive}
onClick={() => {
onChange(
toggleFormat(value, {
type: 'udemy-plus/neon'
})
)
}}
/>
)
We're using the RichTextToolbarButton component to help us render the button. WordPress will handle
rendering this button in the correct location. This component has four properties.
For the onClick event, we're using a function called onChange to verify that the format should be updated. If
that's the case, we passed in the toggleFormat() to handle toggling the selection. This function has two
arguments, which are the selection and the type of format.
Some of this information is available as props to the edit() function, which you can destructure like so:
{
selectedBlock?.name === 'core/paragraph' &&
}
In addition to these functions, WordPress has additional functions for escaping translation messages.
esc_attr__();
esc_attr_e();
esc_attr_x();
esc_html__();
esc_html_e();
esc_html_x();
Most of these functions are defined with PHP. These functions are also available in JavaScript from the
@wordpress/i18n package. The following functions are available.
__()
_n()
_x()
_nx()
wp --version
We can create a template with the CLI by running the following command in our plugin's directory:
This command accepts the location to find the translation messages and the name of the file. After running this
command, WordPress will scan the directory for all uses of WordPress's official translation functions. It'll extract the
strings into a file called udemy-plus.pot.
WordPress recommends storing the files in a directory called languages. The name of the file should be named after
the name of your plugin. Lastly, the file extension should be pot to indicate that this file should serve as a starting
point for all translators.
Resources
WP CLI Handbook
add_action('init', 'up_load_php_translations');
From our function, we called the load_plugin_textdomain() function. This function has three arguments,
which are the text domain of our plugin, a deprecated argument, and the directory where the translations are stored
relative to the plugins directory.
load_plugin_textdomain(
'udemy-plus',
false,
'udemy-plus/languages'
);
Afterward, from the function, we created an array of all the block handles and looped through all of them. The
handle name of a block is the name with the word editor-script appended to it.
$blocks = [
'udemy-plus-fancy-header-editor-script',
'udemy-plus-advanced-search-editor-script',
'udemy-plus-page-header-editor-script',
'udemy-plus-featured-video-editor-script',
'udemy-plus-header-tools-editor-script',
'udemy-plus-auth-modal-script',
'udemy-plus-auth-modal-editor-script',
'udemy-plus-recipe-summary-script',
'udemy-plus-recipe-summary-editor-script',
'udemy-plus-team-members-group-editor-script',
'udemy-plus-team-member-editor-script',
'udemy-plus-popular-recipes-editor-script',
'udemy-plus-daily-recipe-editor-script'
];
foreach($blocks as $block) {
wp_set_script_translations(
$block,
'udemy-plus',
UP_PLUGIN_DIR . 'languages'
);
}
On each loop, we're using the wp_set_script_translations() function. This function has three arguments,
which are the name of the block, the text domain, and the directory where the translations are stored.
Resources
List of Block Handles
if (!defined('WP_UNINSTALL_PLUGIN')) {
exit;
}
Afterward, we deleted the plugin's options by using the delete_option() function, which accepts the name of
the option.
delete_option('up_options');
Lastly, we used the $wpdb variable to run a function called query() for executing a custom query. This query
uses the DROP TABLE keywords to delete a table. The IF EXISTS keywords will check if the table exists before
deleting it. If it doesn't, the query will not throw an error. Errors can disrupt the uninstallation process, which we
shouldn't since our plugin may become unremovable.
global $wpdb;
$wpdb->query(
"DROP TABLE IF EXISTS {$wpdb->prefix}recipe_ratings"
);
Resources
Debugging in WordPress
Query Monitor
Debug Bar
Resources
Updated Template Parts
You can include this class from within the functions.php file. Next, you can use a hook for registering a set of plugins
called tgmpa_register .
add_action('tgmpa_register', 'u_register_plugins');
We defined a function for handling this process. In this function, we had an array for a set of plugins that should be
activated within a theme.
$plugins = array(
array(
'name' => 'Regenerate Thumbnails',
'slug' => 'regenerate-thumbnails',
'required' => false,
),
array(
'name' => 'Udemy Plus',
'slug' => 'udemy-plus',
'source' => get_template_directory() . '/plugins/udemy-plus.zip',
'required' => true,
),
);
$config = array(
'id' => 'udemy',
'menu' => 'tgmpa-install-plugins',
'parent_slug' => 'themes.php',
'capability' => 'edit_theme_options',
'has_notices' => true,
'dismissable' => true,
);
After creating these arrays, we passed them onto a function called tgmpa() , which accepts the array of plugins
and configuration settings.
Once the plugins have been registered, the class will handle generating a UI for installing/activating plugins. If you
have a locally sourced plugin, I highly recommend storing the zip file of a plugin in a separate directory called
plugins.
Resources
TGM Plugin Activation
Regenerate Thumbnails
Section 18: The End
In this section, I give one final farewell!