Proposal: A JSX layer for directives #53048
Replies: 0 comments 6 replies
-
Wow, thanks a lot, Peter. This is very interesting 🙂 From your comment, I understand that you'd like to:
So basically, write something like this: <div className={{"is-finished": isFinished}}>
<button onClick={reset}> Instead of something like this: <div data-wp-class--is-finished="context.isFinished">
<button data-wp-on--click="actions.reset"> Is that the case? Do you have in mind any other things apart from those? For reference, this is how this exact block would be written using the current directives+strings syntax: // save.js
import { formatTime } from "external-library";
export const save = ({ attributes: { initial } }) => (
<div
{...useBlockProps.save()}
data-wp-context={`{"initial": ${initial}`}
data-wp-init="actions.countdown.init"
>
<div data-wp-class--is-finished="context.isFinished" data-wp-text="context.time">
{formatTime(initial, "mm:ss")}
</div>
<button data-wp-on--click="actions.countdown.reset">
{__("Reset", "text-domain")}
</button>
</div>
);
// view.js (frontend)
import { formatTime } from "external-library";
wpx({
actions: {
countdown: {
reset: ({ context }) => {
context.count = context.initial;
},
init: ({ context }) => {
context.count = context.initial;
context.time = ({ context }) => formatTime(context.count, "mm:ss");
context.isFinished = ({ context }) => context.count === 0;
const timer = setInterval(() => {
context.count--;
if (context.count === 0) {
clearInterval(timer);
}
}, 1000);
},
},
},
}); |
Beta Was this translation helpful? Give feedback.
-
Yes, it was mainly the first point I was looking to resolve, and more generally, to bring the logic/context and the markup closer to each other. Having Another thing that's less explicit in the example, is a private context. (Or maybe context is not even a good name in that case.) I think most of the time a component would only want to deal with what it declared, and it could be worth optimizing for that case. Particularly painful was passing a merged context object to every action, and having to extract its own context from there. Instead accessing parent context (or other contexts defined on for the same node/component) could have an API, similar to |
Beta Was this translation helpful? Give feedback.
-
Great, thanks 🙂 I think this is worth investigating, so I have created a Stackblitz with your example but using the current syntax. It'd be great if you could fork it to add a basic version of the pattern you mentioned in the opening post. I'd love to see how it would play out in practice. Then, we can start shaping the DX to see if we can come up with a great solution 🙂 I've recorded a video to explain how the Stackblitz example is structured: https://www.loom.com/share/3880dbd776b348a6b122b16efe89568b Even though I'm asking Peter, if anyone else is reading this, feel free to play with these examples/ideas and add your feedback/comments as well. |
Beta Was this translation helpful? Give feedback.
-
I just only got a chance to take a deeper look at this and it's a fantastic idea, Peter! We should probably give it a fair shot at implementing once we are working on implementing the directives into GB. For static blocks, this should be totally doable. I think it would be somewhat similar to how context is implemented in Mitosis: BuilderIO/mitosis#113 because both in their framework and in ours, the context has be static i.e. serializable. For dynamic blocks, the story is a bit more complex so I would be very keen to see if we could improve it a little bit. It seems to me that the DX would be a little worse when doing this: <?php ?>
<div
class="wp-block-example-countdown aligncenter is-style-large"
wp-context="{'example/countdown': <?= wp_json_encode( $context['example/countdown']['attributes'] ); ?> }">
<div
class="<?= $context['example/countdown']['value']['isFinished']; ?>"
wp-class:is-finished="example/countdown.isFinished"
wp-bind="example/countdown.time"><?= $context['example/countdown']['time']; ?></div>
<button wp-on:click="example/countdown.reset">Reset</button>
</div> rather than using the Alpine-style syntax. But I'd be very keen to figure out if we can improve it. |
Beta Was this translation helpful? Give feedback.
-
Would you mind modifying the Stackblitz example I shared to show what the API would look like? 🙂 |
Beta Was this translation helpful? Give feedback.
-
<div
class="wp-block-example-countdown aligncenter is-style-large"
wp-context="{'example/countdown': <?= wp_json_encode( $context['example/countdown']['attributes'] ); ?> }"> Note that this PHP code would be generated (by the JS build tooling), as an option to author dynamic blocks in JSX. But if everything works,
|
Beta Was this translation helpful? Give feedback.
-
I did some thinking on what the best developer experience could be using the directives approach on hydration, and
started exploring a solution where the frontend component can still be authored like regular JSX.
The idea is to build upon the special
save
function of the blocks, which is not a live React component, but returns JSX that's serialized into a HTML string and saved in the post. This could be a good place to connect the markup with the context, adding the directives in the serialization step.Here is a rough draft of how it would look like:
There are two steps to achieve this:
useFrontendContext.save
wraps the value in a proxy that tracks how the context is used, returning a data object for each property:{ context: 'example/countdown', prop: 'isFinished', value: false }
onClick
towp:click
, and uses the data from the proxy.The serializer would see this:
And generate this HTML:
The context can be defined in a flexible way. I looked at doing it component-like, with a constructor function that can contain state, set up side effects or other integrations:
(The save function above doesn't have everything for this, the arguments this component takes still need to be saved and passed with something like the
wp-context
directive)PHP, dynamic blocks
For dynamic blocks, the same serializer could be run as a build-time step instead, and output PHP template strings for the initial values:
Generated for PHP, saved to
blocks/example-countdown.php
:Using it in PHP:
Custom directives
The above example maps JSX attributes to directives ( eg
onClick
→wp-on:click
) when there is already an existing practice that makes sense. For other cases, the directives can be written directly:Loops, conditionals
For dynamic fragments, helper components could collect the bindings and output the initial markup & template elements.
Benefits and limitations
For blocks, this would provide a way to author the frontend component very much like the rest of the block. It also lets the developer reference the context as a real JS object, not as strings.
For static blocks, a compiler is not needed, this can be part of the existing
renderToString
function in@wordpress/element
that turns a React element into a HTML string.As a drawback on the developer experience, it can be hard to explain that the code using the context proxy cannot contain expressions, and they are working with references, not the values of those props. (Though there might be a way with a compiler extension that takes expressions from the JSX and creates context props from them.)
It also probably requires chaining proxies to track deeper level references of the context props.
Beta Was this translation helpful? Give feedback.
All reactions