Categories
Block Editor Gutenberg WordPress

PHP Web Components for WordPress

The state of web components

Modern front-end web development encourages breaking up of designs into reusable, repeatable and unique components. A component is a piece of markup capable of holding – but is separate from – content within it.

The Web Components specification provides a great way to work with components using the Shadow DOM and other things that are native to the browser. This would (should?) be the go-to solution for web components – since it is an actual W3C standard. However, there are several reasons I personally do not like to use them – the main reason being the fact that components are rendered in the browser using JavaScript. For websites where SEO metrics like CLS are important, it seems almost inconceivable to use them.

Other front-end technologies like React reach for Server Side Rendering or similar methods to overcome CLS and other issues with rankings, where the user is served a static HTML page.

Components using PHP

The problems stated above do not exist in the world of WordPress where we use PHP for templates, since users are always served dynamically generated static HTML pages. Static in the sense that they are not generated by the browser.

So creating components using PHP seems to hit the sweet spot:

  1. Markup is already generated by the server
  2. It works similar to how Web Components do – in that JavaScript functionality is achieved by manipulating the DOM – rather than the Virtual DOM like what React and others do, which hydrate the front-end once a Server Side Rendered page is served

PHP Components in WordPress

Creating PHP components in the WordPress context basically boils down to structuring the theme in a specific way, and making use of template parts. In order to get all the benefits that Web Components (and React and other components) provide, we need to first define what we want our components to do:

  1. Components need to exist in a single place (file). Updating the component in this file must update it everywhere it is used on the website
  2. Components need to be void of context: A component’s only purpose is to be a container of content
  3. Components need to be flexible enough to work with any front-end methodology or utility-class-driven framework like Tailwind CSS
  4. Components need to be flexible, depending on the context or content passed into it
  5. Bonus: Components need to work in the Gutenberg block editor, and can be changed or updated without affecting any posts that have already used it
  6. Bonus: Components must handle their own CSS and JavaScript

Solution Source Code

Here’s a Git repository I put together to illustrate my solution:
https://github.com/junaidbhura/wp-php-web-components-demo

The Component File

File: https://github.com/junaidbhura/wp-php-web-components-demo/blob/master/wp-content/themes/php-components/components/hello-world/index.php

<?php /** * Hello World. */ $attributes = wp_parse_args( $attributes ?? [], [ 'title' => '', 'subtitle' => '', ] ); if ( empty( $attributes['title'] ) ) { return; } wp_enqueue_style( 'hello-world', get_template_directory_uri() . '/components/hello-world/style.css' ); ?> <div class="hello-world"> <h1><?php echo esc_html( $attributes['title'] ); ?></h1> <?php if ( ! empty( $attributes['subtitle'] ) ): ?> <h2><?php echo esc_html( $attributes['subtitle'] ); ?></h2> <?php endif ?> </div>
Code language: PHP (php)

The component is essentially a template part. This is the easiest implementation of a component and is similar to WordPress core’s get_template_part implementation.

What this component does:

  • Sets default values
  • Does not render at all if a required field is missing
  • Enqueues a stylesheet – so the stylesheet is added to the page only if this component is called at least once – reducing CSS bloat on the front-end
  • Shows the subtitle only if it was passed into the component
  • Handles all escaping

We would call this component like this:

<?php jb_component( 'hello-world', [ 'title' => 'Title goes here', 'subtitle' => 'Subtitle goes here', ] ); ?>
Code language: PHP (php)

Example: Click here

This is in contrast to Laravel’s implementation of components, which tries to replicate the standard Web Components specification in PHP – slots and all. If we were to use its implementation, we would do something like:

<x-hello-world :title="Title goes here" :subtitle="Subtitle goes here"/>
Code language: HTML, XML (xml)

Although this is admittedly better looking, and much closer to the official Web Components spec and follows an XML-like syntax which React developers would love – it adds overhead to the project – both in terms of build tools and complexity.

What we have done instead achieves the exact same goal, is more flexible and allows us to easily enqueue assets (more on that later) – in a super simple way with no overhead whatsoever.

The Template

You’re probably wondering how the enqueue works within the component, since the component would technically be rendered after the header – and therefore wp_head hook is called. This is achieved with two template tag functions:

/** * Start building template content. */ function jb_template_header() { ob_start(); } /** * Finish building template content. */ function jb_template_footer() { // Get built content. $content = ob_get_clean(); // Render built content after header. // This is to trigger all hooks before the header is called. get_header(); echo $content; // phpcs:ignore -- No need to escape it, since all escaping happens in the template / components. get_footer(); }
Code language: PHP (php)

So you would call jb_template_header instead of get_header – and jb_template_footer instead of get_footer in the template. This builds all the content before wp_head has had a chance to execute.

Note: This is similar to how blocks work – they are rendered before the header is called, so enqueues will still work in blocks.

The Block

Gutenberg blocks in WordPress have two functions:

  • Edit: This function executes within the block editor and contains editor components as well as markup from the component
  • Save: This function is responsible for what we save into the database which contains HTML markup

So we essentially have two copies of the markup we need to maintain when working with blocks. Saving markup in the database poses a big problem: If the markup ever changes, all the blocks need to be updated within the database. So one needs to be very careful when designing a block to decide what markup needs to be saved to the database. This is especially a problem if you use something like Tailwind CSS, which essentially works by saving a whole bunch of utility CSS classes in the markup. You could just use @apply in Tailwind to overcome this – but it still doesn’t solve our problem.

After a lot of consideration and actually having worked on projects where we save the markup in the database, I now recommend against doing this. I would instead recommend saving all blocks as dynamic blocks to future-proof them. Simple blocks / inner content can still be saved without a problem.

Dynamic blocks work by saving all data to the block’s attributes. We still need to maintain two copies of the markup – but one copy would be in the block’s Edit function – and the other would be in the component partial – which is called within the block like this:

<?php /** * Block: Hello World. */ namespace JB\Theme\Blocks\HelloWorld; const BLOCK_NAME = 'jb/hello-world'; const COMPONENT = 'hello-world'; add_action( 'template_redirect', __NAMESPACE__ . '\register' ); /** * Register block on the front-end. */ function register() { add_filter( 'pre_render_block', __NAMESPACE__ . '\render', 10, 2 ); } /** * Render this block. * * @param mixed $content Original content. * @param array $block Parsed block. * * @return mixed */ function render( $content = null, $block = [] ) { // Check for block. if ( BLOCK_NAME !== $block['blockName'] ) { return $content; } // Build component attributes. $attributes = [ 'title' => $block['attrs']['title'] ?? '', 'subtitle' => $block['attrs']['subtitle'] ?? '', ]; // Return rendered component. return jb_component( COMPONENT, $attributes, false ); }
Code language: PHP (php)

A few things to note:

  1. We use the template_redirect hook to register this block – because we don’t want to unnecessarily execute this code in the back-end for performance reasons
  2. We render the block using the pre_render_block hook rather than registering the block via register_block_type. This is again for performance reasons – we short-circuit the block by adding our own render function
  3. Notice how the render function simply takes the attributes from Gutenberg and passes it to our component

So we have a clear separation of concerns here:

  1. The block is only responsible to maintain data, not presentation
  2. The component is only responsible for presentation, not data

Conclusion

To summarise, this is what we achieved:

  1. We created a single component called “Hello World”
  2. We used this component directly in our template by calling it via jb_component
  3. We created a block, also called “Hello World” which called the exact same component

Looking back at our goals, lets see how we did:

✅ Single component file. If the design were to ever change, we change it in this one place (and related blocks for the editor)
✅ Component has no context – it simply accepts data – whether it comes from a template or a block
✅ It can work with any CSS methodology or framework
✅ Works with Gutenberg – blocks can be safely updated since no markup is ever saved in the database
✅ Our component handles its own assets

Looks like we have a winner! 💪

Leave a Reply

Your email address will not be published. Required fields are marked *