Skip to content
This repository was archived by the owner on Jul 28, 2023. It is now read-only.

SSR: Fix directive processor invocation#215

Closed
ockham wants to merge 2 commits intomain-wp-directives-pluginfrom
fix/directive-processor-invocation
Closed

SSR: Fix directive processor invocation#215
ockham wants to merge 2 commits intomain-wp-directives-pluginfrom
fix/directive-processor-invocation

Conversation

@ockham
Copy link
Copy Markdown
Collaborator

@ockham ockham commented Apr 17, 2023

There are currently two problems with the way we invoke our directive SSR processors:

  1. Since we are hooking wp_process_directives into render_block, it gets run repeatedly on markup inside nested inner blocks (once for each level of block nesting around a given piece of markup).
  2. After invoking all relevant directive processors on a given tag, we just move on via next_tag(), without flushing the changes made to the current tag.

Any issues arising from these remained undiscovered until #141, which introduces the data-wp-show directive. The latter is a bit special in that it wraps a tag with said directive in a <template> tag (if the directive attribute value is falsy), and moves the data-wp-show directive to that newly prepended <template>).

Instructions to reproduce the issue(s)

The following issues were observed when running the wp-movies-demo repo locally in connection with a local copy of the block-interactivity-experiments repo, with the add/wp-show-ssr branch (from #141) checked out.

To do the latter, you can create a file named `.wp-env.override.json` in the `wp-movies-demo` root with these contents
{
    "plugins": [
        "https://downloads.wordpress.org/plugin/gutenberg.latest-stable.zip",
        "https://downloads.wordpress.org/plugin/wordpress-importer.latest-stable.zip",
        "../block-interactivity-experiments",
        "."
    ]
}

Because of the aforementioned issues, we would then encounter markup such as #141 (comment):

<template>
  <template data-wp-show="falseValue">
    <template>
      <div>
        <span>WP Show</span>
      </div>
    </template>
  </template>
</template>

Note the three nested <template>s -- two of them are due to the fact that the block that contains the data-wp-show directive is nested in another block. Finally, the outermost <template> is due to the fact that another directive (data-wp-class is present and processed before data-wp-show, without flushing (i.e. calling get_updated_html()) prior to invoking the data-wp-show directive processor.

This PR aims to fix both issues. To verify, follow the above instructions to reproduce the issue.
Then, on top of the add/wp-show-ssr branch, apply the changes from this branch (e.g. by merging this branch into it, or by cherry-picking the commits), and test again. The markup should now be fixed:

<template data-wp-show="falseValue">
  <div>
    <span>WP Show</span>
  </div>
</template>

Automated test coverage, or the lack thereof

It's unfortunately a bit hard to add proper unit test coverage for this -- arguable as it's more of an integration problem (that would require integration tests).

In the process of investigating this issue, I added some tests to #141 to reproduce issue number 2 (e.g.), but they can't really count as unit tests: While they helped detecting the issue, we cannot make them pass by applying the code changes from this PR; instead, we'd need to add get_updated_html() calls right inside test (after add_class, before next_tag). Similarly, verifying that a function called from the render_block hook runs only once (rather than repeatedly) for nested blocks isn't really unit test material.


Arguably, this PR fixes #148.

Comment on lines +237 to +249
block_has_support(
$parent_block->block_type,
array(
'interactivity',
'isolated',
)
)
isset( $parent_block )
) {
$parsed_block['isolated'] = true;
if (
block_has_support(
$parent_block->block_type,
array(
'interactivity',
'isolated',
)
)
) {
$parsed_block['isolated'] = true;
}
$parsed_block['inner_block'] = true; // TODO: Limit to interactive blocks.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm kind of piggy-backing on an existing, similar mechanism here (set a block attribute during render_block_data; inspect presence and truthiness of that attribute during render_block to act accordingly). h/t @luisherranz and @dmsnell for the prior art and recommendation.

I wish we could only use one attribute and move some logic into the functions that hook into render_block, but since the isolated attribute is set depending on the presence of block support for the current block's parent, that might be impossible, since we don't have access to the parent inside of render_block AFAIK.

@ockham ockham requested a review from a team April 17, 2023 14:33
@ockham ockham self-assigned this Apr 17, 2023
@ockham ockham marked this pull request as ready for review April 17, 2023 14:39
@ockham ockham force-pushed the fix/directive-processor-invocation branch from f2f94d8 to 9ccf375 Compare April 18, 2023 09:49
@SantosGuillamot
Copy link
Copy Markdown
Contributor

I was just testing this by merging it to the add/wp-show-ssr branch. However, I’m finding a weird behavior 😅

It seems that the HTML from the server is correct, but it gets malformed after hydrating. I made a quick video trying to explain it better:

https://www.loom.com/share/b6060fcc6d5c4ac0b4be34a2fd41f746

Basically, for this HTML:

<div data-wp-context=‘{ “isOpen”: false }’>
    <div data-wp-show=‘context.isOpen’><span>WP Show</span></div>
</div>

The HTML from the server is correct:

<div data-wp-context=“{ “isOpen”: false }“>
    <template data-wp-show=“context.isOpen”>
        <div><span>WP Show</span></div>
    </template>
</div>

In the client, an extra empty <template> is being added inside the <template>:

<div data-wp-context=“{ “isOpen”: false }“>
    <template data-wp-show=“context.isOpen”>
        <div><span>WP Show</span></div>
        <template></template>
    </template>
</div>

In the video, I'm not using the latest version of the branch, but I tested it again with the latest changes and the problem is still there for me.

@ockham
Copy link
Copy Markdown
Collaborator Author

ockham commented Apr 18, 2023

Thank you Mario for testing! 🙌

I’m a bit relieved that the issue is on the client side only, and that it doesn’t occur when you disable JS, as that confirms that it must be an issue with our client-side code (and not e.g. malformed HTML that the browser "fixes" when building the DOM).

@ockham
Copy link
Copy Markdown
Collaborator Author

ockham commented Apr 18, 2023

I made a Loom to explain the issue:

https://www.loom.com/share/b05feef81060465983c313ef40733c4f

@ockham
Copy link
Copy Markdown
Collaborator Author

ockham commented Apr 18, 2023

Hmm, I think I've found another instance of wrapped <template> tags that this PR hasn't fixed yet 😕

<template data-wp-show="selectors.wpmovies.isPlaying"><template ><div  class="wpmovies-video-player wp-block-wpmovies-video-player">
	<div class="wpmovies-video-wrapper">
		<div class="wpmovies-video-close">
			<button class="close-button" data-wp-on.click="actions.wpmovies.closeVideo">
				Close			</button>
		</div>
		<iframe src=""
			width="420"
			height="315"
			allow="autoplay"
			allowfullscreen
			data-wp-bind.src="state.wpmovies.currentVideo"
		></iframe>
	</div>
</div></template></template>

(This is again the Movies Demo when used locally with the block-interactivitiy-experiments repo, with the branch from #141 checked out, and fixes from this branch applied.)

@SantosGuillamot
Copy link
Copy Markdown
Contributor

SantosGuillamot commented Apr 18, 2023

Oh, I just remembered that the wp-show directive client logic was different than the one we are using in the server because there is a bug (no issue yet) of props passing to next element. If we take a look at this discussion, the SSR is returning the Option 1: "Wrapped everything inside the <template> tag", and the CSR is returning the Option 2: "Wrapped children inside the <template> tag"

Maybe that's the reason why it is not working as expected in the video I shared previously.


I think I've found another instance of wrapped <template> tags that this PR hasn't fixed yet

I confirm that I can also replicate the issue you reported in the movies demo 😢

Apart from that, I have one question: is it correct to move the data-wp-show="selectors.wpmovies.isPlaying" attribute to the <template> tag, or should it remain in the div? I don't know about the implications (if any).

@ockham
Copy link
Copy Markdown
Collaborator Author

ockham commented Apr 18, 2023

Oh, I just remembered that the wp-show directive client logic was different than the one we are using in the server because there is a bug (no issue yet) of props passing to next element. If we take a look at this discussion, the SSR is returning the Option 1: "Wrapped everything inside the <template> tag", and the CSR is returning the Option 2: "Wrapped children inside the <template> tag"

Maybe that's the reason why it is not working as expected in the video I shared previously.

Ah yeah, that seems likely!

Apart from that, I have one question: is it correct to move the data-wp-show="selectors.wpmovies.isPlaying" attribute to the <template> tag, or should it remain in the div? I don't know about the implications (if any).

TBH I was really just trying to follow what seemed to be the outcome of the discussion you mentioned at the time:

Actually, I would even use a slightly simplified variation of 1. When wp-show is false:

  • Wrap it with <template> in the server (maybe also move the wp-show to the template).
  • Remove it from the DOM in the client.

Should I leave the data-wp-show attribute on the wrapped tag (instead of moving it to the wrapping <template>) after all? cc/ @luisherranz

@luisherranz
Copy link
Copy Markdown
Member

Should I leave the data-wp-show attribute on the wrapped tag (instead of moving it to the wrapping template) after all?

So there's a small problem with templates and vdoms that I haven't figured it out completely yet.

https://www.loom.com/share/a8fe41eeb30343cc8fc41f08d0045b41
https://stackblitz.com/edit/vitejs-vite-razb5u?file=src%2Fmain.jsx,index.html

At first glance, it looked like moving the data-wp-show to the template would help. But again, I've barely investigated the issue, so maybe there's a better solution.

@luisherranz
Copy link
Copy Markdown
Member

luisherranz commented Apr 20, 2023

Oh, wait. I just remembered that I actually did some coding for this.

This is wp-show with the directive moved to the <template> in the server: https://stackblitz.com/edit/demo-filters-multiple-queries-rsrp5l?file=src%2Fruntime%2Fdirectives.js,index.html

<!-- This was SSRed as false in the server -->
<template data-wp-show="context.showImage">
  <img src="..." />
</template>

<!-- This was SSRed as true in the server -->
<img data-wp-show="context.showImage" src="..." />

In this implementation, the HTML is removed from the client when wp-show is false (instead of keeping the <template> in the DOM).

What I'm doing here is saving the HTML of the template in a separate prop:

const isTemplate = localName === "template";
const childNodes = isTemplate ? node.content.childNodes : node.childNodes;

// ...

if (isTemplate) return h(localName, { ...props, templateChildren: children });

That way, the hydration doesn't add the inner contents of the template to the DOM.

Then, in the wp-show directive, I get either the children in that prop (when wp-show was false in the server), or the element (when wp-show was true in the server).

const children = useMemo(
  () =>
    element.type === "template" ? element.props.templateChildren : element,
  []
);

And finally, I remove the HTML from the DOM in the client when wp-show is false:

if (!evaluate(show, { context: contextValue })) return null;

return children;

@ockham
Copy link
Copy Markdown
Collaborator Author

ockham commented Apr 24, 2023

Since this PR isn't quite fixing all the issues we were seeing, and we're looking to fix what seems to be an underlying issue with the Tag Processor, I'm considering closing this PR.

There's still the separate issue of processing directives repeatedly (once for each layer of nested inner block) which is fixed by one of the commits in this PR (a8cadac), but I'm thinking to file a new PR for that to keep that conversation separate.

@ockham
Copy link
Copy Markdown
Collaborator Author

ockham commented Apr 24, 2023

There's still the separate issue of processing directives repeatedly (once for each layer of nested inner block) which is fixed by one of the commits in this PR (a8cadac), but I'm thinking to file a new PR for that to keep that conversation separate.

#227

@ockham ockham closed this Apr 24, 2023
@ockham ockham deleted the fix/directive-processor-invocation branch April 24, 2023 13:16
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SSR: Audit usage of wp_process_directives

3 participants