TL;DR
I would use AlpineJS to create a custom theme (the old way), or maybe a tiny blocks plugin. But I would not consider it a good standard to create highly interactive blocks, both core, and custom.
I have taken a look at other PHP communities to see their approach to building interactive components. A portion of the Magento Community and the Laravel one uses AlpineJS as the main library for the frontend interactions.
I did a small proof of concept about how would the block developer experience be using this technology, and here is what I found:
What is AlpineJS?
According to their creators, Alpine is a rugged, minimal tool for composing behavior directly in your markup. Think of it like jQuery for the modern web. But, we don’t want jQuery, right?
That’s it. Alpine is lighter weight than jQuery. It directly mutates the DOM while exposing the same declarative API as Vue. It is HTML centric, so most of the code will be written on both index.php or save.js files, depending on if we are on static or dynamic blocks.
What did I achieve?
- I did a simple show/hide toggle.
- I experimented with data post, dom update, and frontend hydration.
A video will show better the user experience we can offer to the WordPress ecosystem.
How did I do it?
The experiment is available on this Github PR.
Let’s decipher it:
- I render the comments data, and add it to the PHP callback, inside an HTML directive called
x-data
. This way it will be rendered in afor
loop, handled by JavaScript. File view here.
$content = sprintf(
'<div x-data=\'{ comments: %1$s }\'><template x-for="comment in comments" :key="comment.id"></div>%2$s</template></div>',
json_encode( $comments_array ), // Data parsed in order to be an array of objects
$content // The block comment-template of each comment
);
return $content;
By this approach, when the user replies to a comment, just by adding the comment data to the comments array, Alpine will automatically add it with the same HTML structure as the other comments.
- We print the author’s name, date, and content of each comment by using
x-text
orx-html
directives by AlpineJS. We do this inside each comment’s inner block. This way, AlpineJS will bind the comments array data attribute to each correspondent HTML tag. Author name file view here. Comment content file view here.
return sprintf(
'<div x-html="comment.content" %1$s></div>',
$wrapper_attributes
);
- We render a reply form, hidden, inside every comment reply link (not the best performance approach to be honest).
$comment_field = '<p class="comment-form-comment"><label for="comment">' . _x( 'Comment', 'noun' ) . '</label><br /><textarea x-model="commentContent" id="comment" name="comment" aria-required="true"></textarea></p>';
ob_start();
comment_form(
array(
'comment_field' => $comment_field,
)
);
$form = str_replace(
'<form',
'<form x-on:submit.prevent="createComment({ parent:' . $block->context['commentId'] . ', content: commentContent })" ',
ob_get_clean()
);
$comment_toggled_form = sprintf(
'<div x-data="commentObject" %1$s>
<div @click.prevent="open = ! open">%2$s</div>
<div x-show="open" x-transition>
%3$s
</div>
</div>',
$wrapper_attributes,
$comment_reply_link,
$form
);
return $comment_toggled_form;
The x-data attribute commentObject
is defined in the view.js JavaScript file. It gives the state open or closes per each comment, the variable for the reply we write on the form, and finally the function that send the reply via POST to the database via REST API and adds the comment created to the comments array.
/**
* External dependencies
*/
import Alpine from 'alpinejs';
window.Alpine = Alpine;
// Most of the body variables could be read from x-data attributes on the html.
const commentObject = () => ( {
commentContent: '',
open: false,
async postComment( { content, parent } ) {
const commentPosted = await window.fetch(
window.apiSettings.root + 'wp/v2/comments/',
{
method: 'POST',
body: JSON.stringify( {
post: 1,
author_name: 'admin',
author_email: 'wordpress@example.com',
parent,
content,
} ),
headers: {
'Content-type': 'application/json; charset=UTF-8',
'X-WP-Nonce': window.apiSettings.nonce,
},
}
);
const comment = await commentPosted.json();
if ( comment ) {
this.open = false;
this.comments.push( {
id: comment.id,
author: comment.author_name,
date: comment.date,
content: comment.content?.rendered,
} );
}
},
} );
Alpine.data( 'commentObject', commentObject );
Alpine.start();
The this.comments
is accessible, due to it having been defined by a parent div
, the one that loops to create the comments list.
When we update the comments array, AlpineJS takes care of adding the comment automatically to the list (not in the right place, but I did not want to invest too much more time on the POC). There is some recursion with replies to deal with.
Conclusions
What would happen if the community chooses AlpineJS as a new standard to create the frontend render of the blocks?
Benefits:
- Lightweight. 37,1kb. 13.1gzipped. This does not mean that is performant, in fact, in some performance tests, is getting some red flags compared to other frameworks.
- Easy to learn, declarative, and PHP friendly. For small interactions like modals or hiding content is quite fast and easy.
<div x-data="{ open: false }">
<button @click="open = true">Expand</button>
<span x-show="open">
Content...
</span>
</div>
- Widely used in PHP communities like Laravel or Magento.
- We can add interactivity to both static and dynamic blocks.
- As is DOM-centric, it should not crash with PHP hooks.
Cons:
- It would live with PHP and React. Being more similar to Vue in terms of how you code it. This means a wider learning curve for the developers. As it would be adding a third syntax to the system.
- It doesn’t solve Server Side Rendering issues for SEO for cases that require data fetching. In our case, using only AlpineJS to render a loop of comments means you only have the data in a JSON, not in structured HTML. In case you want it, you need to do a pre-render with PHP, remove it from the DOM and then do it the Alpine way, to add the interactivity. This means duplicating work.
- It would be needed to rewrite some WordPress core functions in order to add AlpineJS attributes to different parts of the HTML. This also would couple a lot of the Core to the frontend library.
- Other plugins like Sensei or WooCommerce are working on React-based approaches.
My personal opinion
After working a couple of weeks in this POC, I’ve experienced the developer journey to create a block that is rendering a form, submitting the info async, and adding the response to a loop render.
As a React and PHP developer, I’ve found it really easy to add small interactions but, when I’ve started coding a more advanced experienced, I’ve seen some caveats.
- It’s easy to get spaghetti (here is an example of a Magento theme header). You can end up adding a lot of content into an HTML attribute. This ends up being really difficult for the developer to read and understand what is happening.
- The blocks end up being tightly coupled and are hard to read and follow their interactions. The moment a child block needs to read a parent attribute, you need an overall overview of all the blocks together to really understand what is happening. It’s not a really good developer experience to dig file after file and merge them in your mind to understand what is happening.
I would use AlpineJS to create a custom theme (the old way), or maybe a small blocks plugin. But I would not consider it a good standard to create highly interactive blocks, both core, and custom.
Feel free to share feedback and opinion in the comments!