Table of Contents
The create-block package is a fantastic time-saving tool that allows you to quickly and consistently create custom blocks for your projects with a single command. At its core, it really only does one thing. It receives some values, processes those values, and then generates files with those values inserted into them.
In fact, the create-block tool relies on a template to tell it where to find the files it needs, where inside them it should insert the processed values, and to define any default values needed.
A default template provides a standard configuration for creating simple blocks. The real power of create-block becomes evident when you start creating external project templates.
External project templates provide a level of control that is not otherwise possible via interactive mode or the available command line options by allowing fine grain control over all aspects of the scaffolded files.
Templates can be passed to create-block via the --template
flag and the templates can be hosted on npmjs.com as is the case with the Getting Started guide for the new Interactivity API:
npx @wordpress/create-block@latest my-first-interactive-block --template @wordpress/create-block-interactive-template
Templates can also be referenced locally by passing the path to the directory where the template is located:
npx @wordpress/create-block@latest custom-template-block –template ./path/to/files
This is a great way of not only creating and testing templates, but also leveraging them in projects where the code cannot be shared with the overall community.
Getting set up
To work with the create-block tool, you’ll need to have Node.js installed. For a primer on that, refer to the official documentation on setting up the Node.js development tools.
Once you have Node installed, clone the accompanying repository for this article:
git clone https://github.com/wptrainingteam/external-project-template.git
Then move into the directory:
cd external-project-template
The contents of the project should like like this:
All the template files and configurations will be stored in the my-template
directory. It is inside the root directory, so you can test the template easily in the same location.
The configuration will be stored in the index.js
file, and the actual template files (more on these in a bit) will be stored in the files
directory and its sub-directories. The directory names are not important, you can call them whatever you’d like. The important part is that we’re separating the files that are used for the plugin and block portions of the resulting plugin.
Defining a template
Templates are made up of two parts: the index.js
file that contains the configuration object and the mustache templates that will be used as patterns for each new plugin or block created by the tool.
Mustache is a logic-less template syntax. It can be used for HTML, config files, source code – anything. It works by expanding tags in a template using values provided in a hash or object.
Configuration
The index.js
file contains a single object that is exported and used as the configuration for the template. There are six available properties available for use here, and you can read about them in the documentation.
As this is Node.js, you will be using CommonJS modules instead of ES Modules that you would see in most modern WordPress JavaScript projects.
Start by exporting an empty object and importing a helper that you’ll use a bit later.
Add the following to the index.js
file you created earlier:
/**
* Dependencies
*/
const { join } = require("path");
module.exports = {};
Defining file locations
Next, you will need to tell the template where it can find the files it needs. The repository contains a files
directory with two inner directories for plugin
files and block
files. You can define those locations using the pluginTemplatesPath
and blockTemplatesPath
respectively.
Do that now:
/**
* Dependencies
*/
const { join } = require("path");
module.exports = {
pluginTemplatesPath: join(__dirname, "files/plugin"),
blockTemplatesPath: join(__dirname, "files/block"),
};
At this point, you’re pointing to empty directories, but that’s okay for now. You’ll define the contents of these directories later.
There is another option called assetsPath
that is used for static assets such as images or fonts that shouldn’t be processed. You’re not using this property, but it’s worth mentioning and you can read more about it in the documentation.
defaultValues
Most of the configuration settings will be set using this object. It allows the template author access to set defaults for a considerable number of properties.
While all the properties are added to the defaultValues
object, they are conceptually split across three groups: Project, Plugin header, and block metadata. Read more about all the properties in the documentation.
Personally, I like to use Prettier in my local setup. I have VSCode configured to format my JavaScript when I save the file and I typically add it to the package.json
file in my project. When the create-block tool is run, a package.json
file is automatically created for the scaffolded plugin. There are several defaults available to customize the package, but they have very specific uses such as defining dependencies, scripts, and wp-env/scripts integrations. The defaultValues.customPackageJSON
is used as a catch-all to allow template authors to extend the package.json
as needed.
Add the following to the index.js
file:
/**
* Dependencies
*/
const { join } = require("path");
module.exports = {
defaultValues: {
customPackageJSON: {
prettier: "@wordpress/prettier-config",
},
},
pluginTemplatesPath: join(__dirname, "files/plugin"),
blockTemplatesPath: join(__dirname, "files/block"),
};
When run, this will add "prettier": "@wordpress/prettier-config",
to the created package.json.
Next, you’re going to add default values for namespace
, version
, and description
. Keep in mind that these are the default values that can still be overridden using the appropriate flag.
/**
* Dependencies
*/
const { join } = require("path");
module.exports = {
defaultValues: {
version: "1.0.0",
namespace: "developer-blog",
description:
"A plugin created by the create-block tool using a custom external project template.",
customPackageJSON: {
prettier: "@wordpress/prettier-config",
},
},
pluginTemplatesPath: join(__dirname, "files/plugin"),
blockTemplatesPath: join(__dirname, "files/block"),
};
Variants
Variants are a very powerful method for creating variations of your template. These are associated with a specific template and are accessed via the --variant
flag. For example, the default theme has both static
and dynamic
variants. Each variant produces a different set of files.
Variants contain their own set of defaultValues
that combine with and override the main ones. This allows template authors to change parts of the configuration to suit the needs for a specific variant.
Your template is going to contain two variants: dynamic
, and interactive
. The former generates a simple dynamic block while the latter produces a dynamic block that leverages the new Interactivity API included as of WordPress 6.5.
Each variant is an object under the variants
property that contains the specific values to be combined with defaultValues
.
/**
* Dependencies
*/
const { join } = require("path");
module.exports = {
defaultValues: {
version: "1.0.0",
namespace: "developer-blog",
description:
"A plugin created by the create-block tool using a custom external project template.",
render: "file:./render.php",
customPackageJSON: {
prettier: "@wordpress/prettier-config",
},
},
variants: {
dynamic: {},
interactive: {},
},
pluginTemplatesPath: join(__dirname, "files/plugin"),
blockTemplatesPath: join(__dirname, "files/block"),
};
Typically, the first variant is left empty (in your case, dynamic
) as it is used as the default, should no --variant
be passed to the command. From a philosophical standpoint, this makes sense as the defaultValues
should be all the values needed to create a plugin.
Next, you add the customizations to the interactive
variant:
/**
* Dependencies
*/
const { join } = require("path");
module.exports = {
defaultValues: {
version: "1.0.0",
namespace: "developer-blog",
description:
"A plugin created by the create-block tool using a custom external project template.",
render: "file:./render.php",
customPackageJSON: {
prettier: "@wordpress/prettier-config",
},
},
variants: {
dynamic: {},
interactive: {
viewScriptModule: "file:./view.js",
customScripts: {
build: "wp-scripts build --experimental-modules",
start: "wp-scripts start --experimental-modules",
},
supports: {
interactive: true,
},
},
},
pluginTemplatesPath: join(__dirname, "files/plugin"),
blockTemplatesPath: join(__dirname, "files/block"),
};
This variant adds the viewScriptModule
property and sets supports.interactive
to true
in block.json
. Additionally, it changes the build
and `start
scripts in package.json
. These changes are all needed to enable the Interactivity API, which is beyond the scope of this article.
Learn more about the Interactivity API in the official documentation.
Mustache templates
Mustache templates are used as patterns to create the various files that are needed to generate the plugin. They are split into two types: plugin and block.
Plugin files
The plugin files are output into the root of the generated plugin, such as the main plugin PHP file, readme.txt
, and composer.json
.
Block files
These are the files that will be output inside the plugin’s src
directory. This is where the plugin files are output, such as block.json
, index.js
, edit.js
, and render.php
.
Regardless of the type, each template must have the .mustache
extension. If it doesn’t, the create-block tool will ignore the file.
Template variables
Looking in the files/plugin
directory, you will see five files that all have the .mustache
extension:
- .editorconfig.mustache
- .eslintrc.mustache
- .gitignore.mustache
- readme.txt.mustache
- $slug.php.mustache
Each of these files will be added to the root directory of the plugin without the .mustache
extension. You’re probably wondering why that last file has $slug
in its name. And if you looked inside of it or readme.txt.mustache
, you may also be wondering what this odd looking notation is:
<?php
/**
* Plugin Name: {{title}}
*/
These items are mustache template variables that are replaced with values defined by the template, passed by the user, or derived by the create-block tool based on other values.
For example, if you run the following command:
npx @wordpress/create-block my-example-template-block --template ./my-template
$slug.php.mustache
would become my-example-template-block.php
, and the {{title}}
variable would be replaced with My Example Template Block
, which was generated from the slug that was passed to the tool.
These are the variables that are available for use in any of the templates that are replaced with values:
- {{namespace}},
- {{namespaceSnakeCase}}
- {{namespacePascalCase}}
- {{slug}}
- {{slugPascalCase}}
- {{slugSnakeCase}}
- {{title}}
- {{description}}
- {{author}}
- {{pluginURI}}
- {{license}}
- {{licenseURI}}
- {{domainPath}}
- {{updateURI}}
- {{version}}
- {{textdomain}}
There are other variables that are created related to the variants you defined earlier. Each one is turned into a boolean variable that indicates a variant’s selected status. In your template, two will be defined: {{isDynamicVariant}
} and {{isInteractiveVariant}}
. If --variant interactive
is passed, then the {{isInteractiveVariant}}
will be true
and {{isDynamicVariant}}
will be false. If no --variant
is passed or the value is set to dynamic
, then the opposite will take place.
This is a powerful feature enabling template authors to conditionally output content in their files based on these variables. Mustache supports conditional template variables by using the following format:
{{#VariableVar}}
If VariableVar is true, then this content is output
{{/VariableVar}}
There are two cases where this is very helpful:
The first is when you only need a file for a given variant. In your template, there is a view.js.mustache
file that is only needed when using the interactive
variant. Open that file now and you will see the following code:
/**
* WordPress dependencies
*/
import { store } from '@wordpress/interactivity';
store( '{{slug}}', {
state: {},
actions: {},
callbacks: {},
} );
Run the following command to generate a dynamic block:
npx @wordpress/create-block my-example-template-block --template ./my-template
Notice that the file is generated, which is not the desired outcome. Add the conditional tags to the template to check if {{isInteractiveVariant}
} is active:
{{#isInteractiveVariant}}
/**
* WordPress dependencies
*/
import { store } from '@wordpress/interactivity';
store( '{{slug}}', {
state: {},
actions: {},
callbacks: {},
} );
{{/isInteractiveVariant}}
Run the command again and you’ll see that the file is not output because the conditional did not validate as true and an empty file was created. The create-block tool is smart enough to check to make sure any file has content, and if not, it won’t write the file to the file system.
The other case occurs when the contents of a file are different based on the variant. In our case, the render.php file will have slightly different content based on the variant but some content is always output.
<?php
/**
* PHP file to use when rendering the block type on the server to show on the front end.
*
* The following variables are exposed to the file:
* $attributes (array): The block attributes.
* $content (string): The block default content.
* $block (WP_Block): The block instance.
*
* @see https://github.com/WordPress/gutenberg/blob/trunk/docs/reference-guides/block-api/block-metadata.md#render
*/
{{#isInteractiveVariant}}
// Define some global state
wp_interactivity_state(
'{{slug}}',
array()
);
// Define some context.
$context = array();
?>
<p <?php echo wp_kses_data( get_block_wrapper_attributes() ); ?>
data-wp-interactive="{{slug}}"
<?php echo wp_interactivity_data_wp_context( $context ); ?>
>
<?php esc_html_e( '{{title}} – hello from an interactive block!', '{{textdomain}}' ); ?>
</p>
{{/isInteractiveVariant}}
{{#isDynamicVariant}}
?>
<p <?php echo wp_kses_data( get_block_wrapper_attributes() ); ?>>
<?php esc_html_e( '{{title}} – hello from a dynamic block!', '{{textdomain}}' ); ?>
</p>
{{/isDynamicVariant}}
The template above will always have the contents that appear at the top of the file, but based on which variant is active, it will output different content.
Transformer
The transformer
property is part of defaultValues
but has a special use. It accepts a function that allows a template author to access all the variables that are created by the create-block tool and then returns the modified values.
Update the index.js
file with the following:
/**
* Dependencies
*/
const { join } = require("path");
module.exports = {
defaultValues: {
transformer: (view) => {
console.log(view);
return view;
},
version: "1.0.0",
namespace: "developer-blog",
description:
"A plugin created by the create-block tool using a custom external project template.",
render: "file:./render.php",
customPackageJSON: {
prettier: "@wordpress/prettier-config",
},
},
variants: {
dynamic: {},
interactive: {
viewScriptModule: "file:./view.js",
customScripts: {
build: "wp-scripts build --experimental-modules",
start: "wp-scripts start --experimental-modules",
},
supports: {
interactive: true,
},
},
},
pluginTemplatesPath: join(__dirname, "files/plugin"),
blockTemplatesPath: join(__dirname, "files/block"),
};
Run this command again:
npx @wordpress/create-block my-example-template-block --template ./my-template
In your terminal, you will see a large object containing all the variables that can be accessed.
{
'$schema': 'https://schemas.wp.org/trunk/block.json',
apiVersion: 3,
plugin: true,
namespace: 'developer-blog',
slug: 'my-example-template-block',
title: 'My Example Template Block',
description: 'A plugin created by the create-block tool using a custom external project template.',
dashicon: undefined,
category: 'widgets',
attributes: undefined,
supports: undefined,
author: 'The WordPress Contributors',
pluginURI: undefined,
license: 'GPL-2.0-or-later',
licenseURI: 'https://www.gnu.org/licenses/gpl-2.0.html',
domainPath: undefined,
updateURI: undefined,
version: '1.0.0',
wpScripts: true,
wpEnv: false,
npmDependencies: [],
npmDevDependencies: undefined,
customScripts: {},
folderName: './src',
editorScript: 'file:./index.js',
editorStyle: 'file:./index.css',
style: 'file:./style-index.css',
viewStyle: undefined,
render: 'file:./render.php',
viewScriptModule: undefined,
viewScript: undefined,
variantVars: { isDynamicVariant: true, isInteractiveVariant: false },
customPackageJSON: { prettier: '@wordpress/prettier-config' },
customBlockJSON: undefined,
example: undefined,
textdomain: 'my-example-template-block'
}
There are any number of reasons you may want to access and modify these. For example, the Block Development Examples repo uses it to add a generated value to the slug, title, and some other items.
Another use case is to create template variables that are not supported natively by create-block. In your template, you are providing a variant that uses the Interactivity API released in WordPress 6.5. In this case, it makes sense for you to set the Requires at least plugin header to require WordPress 6.5. There is no readily available defaultValue
that you can override, so you’re going to add your own and use it in the template.
First, update the Requires at least header
in $slug.php.mustache
to use the following:
* Requires at least: {{requiresAtLeast}}
This is a new variable you introduced via the transformer function.
Next, update the transformer function in your configuration to conditionally check the variant and return the appropriate version number to set the header value.
transformer: (view) => {
const {
variantVars: { isInteractiveVariant },
} = view;
return {
...view,
requiresAtLeast: isInteractiveVariant ? "6.5" : "6.1",
};
},
This code returns the view object and adds the new template variable to it.
Run the following command:
npx @wordpress/create-block my-example-template-block --template ./my-template --variant interactive
The my-example-template-block.php will now read as follows:
* Requires at least: 6.5
Congratulations! You now have a working custom external project template. Try adding a new variant of your own.
If you have any templates you’d like to share, add them to the comments below.
Props @greenshady, @bph and @dansoschin for providing feedback and reviewing this post.
Leave a Reply