Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interactivity API: initial support for SSR #51229

Merged
merged 16 commits into from
Jun 6, 2023

Conversation

luisherranz
Copy link
Member

@luisherranz luisherranz commented Jun 5, 2023

What?

This PR adds server-side rendering for the following directives:

  • wp-context
  • wp-bind
  • wp-class
  • wp-style
  • wp-text

Tracking issue: #51056

Why?

Server-side rendering of the directives ensures that the HTML that the server sends will match the first HTML generated after the initial hydration improving the UX and SEO.

How?

Most of the code was created originally by @ockham in the block-interactivity-experiments repo with the help of @dmsnell and others, and ported by me to Gutenberg.

The current implementation uses either the HTML Tag Processor or a new class built on top of it (WP Directive Processor) that adds things like set_inner_html which are necessary for wp-text.

IMPORTANT DISCLAIMER: This code of the WP_Directive_Processor class is highly experimental and its only purpose is to provide a way to test the server-side rendering of the Interactivity API. Most of this code will be discarded once the HTML Processor is available. Please restrain from investing unnecessary time and effort trying to improve this code.

The technique to process only the root nodes is new (it was not in the BIE repo) but it doesn't yet work as it should because it processes the root blocks of each post loaded (the template, the template parts, the post content, the reusable blocks…), when it should only process the root blocks of the template to ensure that directives are only processed once. Therefore, there are still multiple processing of the same directives happening. Still, I would prefer to merge this PR and try to fix it in a subsequent one.

Testing Instructions

  • Open a post
  • Add an HTML block
  • Paste this code
    <div data-wp-context='{ "hide": false, "color": "red" }'>
      <div
        class="another"
        style="text-decoration: underline"
        data-wp-bind--hidden="context.hide"
        data-wp-class--show="!context.hide"
        data-wp-style--color="context.color"
        data-wp-text="context.color"
      >
        default value
      </div>
    </div>
  • Take a look at the frontend, there should be a red underlined text saying "red" with the class "show"
  • Change the "color" to "blue".
  • Now there should be a blue underlined text saying "blue"
  • Change "hide" to true.
  • The text now should have a hidden attribute and the "show" class should not be there anymore

@luisherranz
Copy link
Member Author

I've added support for functions during the SSR in c8d3c7f. It works pretty much like in JS:

<?php
$unique_id = substr( uniqid(), -5 );
$store = wp_store();
$previous_id = isset( $store['state']['selected'] ) && $store['state']['selected'];

wp_store( array(
  'state' => array(
    'selected' => $previous_id || $unique_id,
  ),
  'selectors' => array(
    'isOpen' => function( $store ) {
      return $store['state']['selected'] === $store['context']['id'];
    }
  )
) );
?>

<div
  <?php echo get_block_wrapper_attributes(); ?>
  data-wp-context='{ "id": "<?php echo $unique_id; ?>" }'
>
  ...
</div>
import { store } from "@wordpress/interactivity";

store({
  selectors: {
    isOpen: ({ state, context }) => state.selected === context.id,
  },
});

@luisherranz
Copy link
Member Author

I think this should be ready for review.

Performance tests are still failing, but they don't seem related. I'll try merging trunk with the interactivity branch to see if that solves the issue.

@luisherranz luisherranz marked this pull request as ready for review June 5, 2023 15:13
Copy link
Contributor

@DAreRodz DAreRodz left a comment

Choose a reason for hiding this comment

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

Code LGTM! Funny to see directives work without the runtime. 😄

@luisherranz
Copy link
Member Author

Thanks, David! Merging now 🙂

@ockham we can fix the markTestSkipped thing in the parent PR.

@luisherranz luisherranz merged commit e740fff into interactivity Jun 6, 2023
@luisherranz luisherranz deleted the interactivity-api-initial-support-for-ssr branch June 6, 2023 16:31
// Helper that removes the part after the double hyphen before looking for
// the directive processor inside `$attribute_directives`.
$get_directive_type = function ( $attr ) {
return explode( '--', $attr )[0];
Copy link
Member

Choose a reason for hiding this comment

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

it would be great if we added explanatory comments in places like this where we're performing arbitrary transforms on data. I don't think it's at all obvious to anyone who hasn't worked recently in this code what the purpose is here, but a single example in a comment could clarify that instantly.

also, here and elsewhere, WordPress coding style will require all multi-line comments use the /* .. */ format.

 * Example
 *
 *     'wp-context' === $get_directive_type( 'wp-context' );
 *     'wp-bind'    === $get_directive_type( 'wp-bind--src' );
 */


$attributes = $tags->get_attribute_names_with_prefix( $prefix );
$attributes = array_map( $get_directive_type, $attributes );
$attributes = array_intersect( $attributes, array_keys( $directives ) );
Copy link
Member

Choose a reason for hiding this comment

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

for cases like this in performance sensitive code (of which this very much is) it would be good to adopt a default practice of avoiding multiple iterations and array functions. we can avoid a function call and avoid the costly array iterations and intersection with a direct foreach loop

$attributes = array();
foreach ( $tags->get_attribute_names_with_prefix( $prefix ) as $name ) {
	// Get "wp-bind" from "wp-bind--src" and "wp-context" from "wp-context" etc...
	list( $type ) = explode( '--', $name );
	if ( array_key_exists( $type, $directives ) ) {
		$attributes[] = $name;
	}
}

array( 'context' => $context )
);

if ( strpos( $path, '!' ) === 0 ) {
Copy link
Member

Choose a reason for hiding this comment

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

similar note here on an explanatory comment - it's extremely confusing to try and figure out what's happening here and why. so few words can save so much hassle. also, we are welcome to use str_starts_with

/*
 * I actually have no idea what this is doing.
 * At first I assumed this negated the value of a context value,
 * but "resolve the reference using the store and the context
 * from the provided path" is a jumble of very generic words and
 * I am lost. If I understood what's happening though this
 * comment would be quite terse.
 */
if ( str_starts_with( $path, '!' ) ) {

or, given that all we need to do is check the first character we can avoid a function call and do the much faster character check

// Meaningful explanatory comment.
if ( '!' === $path[0] ) {

$has_negation_operator = true;
}

$array = explode( '.', $path );
Copy link
Member

Choose a reason for hiding this comment

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

is there a better name than $array? how about $data_path or $data_path_segments or $value_path etc…?

}

// Check if $current is a function and if so, call it passing the store.
if ( is_callable( $current ) ) {
Copy link
Member

Choose a reason for hiding this comment

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

note that the comment and code contradict each other here. almost anything can be a callable, including things that we don't want to treat as callable here. "file" is callable, because there's a PHP function named file. this means that it's not possible to use a context path like users.avatar.file because we will try and run file and this will likely crash in surprising and unexpected ways.

given that the default behavior here is the dangerous and unsurprising one, it would be valuable to restrict this either to actual function data types or require more strict means for indicating that something ought to be called.

Copy link
Contributor

Choose a reason for hiding this comment

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

There are other callable types, like an array of two strings, e.g., array( 'MyClass', 'some_static_method' ). Those are harder to be evaluated as real callable, but it is another possibility. 🤔

We could restrict $current to be an instance of Closure, which is the class that anonymous and arrow functions use. I don't think developers should be allowed to use a different callable type inside the store.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sounds good to me.

Copy link
Contributor

Choose a reason for hiding this comment

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

And, in any case, if you want to use another callable, you can do so inside an anonymous function. 🤷

$tag_stack = array();

while ( $tags->next_tag( array( 'tag_closers' => 'visit' ) ) ) {
$tag_name = strtolower( $tags->get_tag() );
Copy link
Member

Choose a reason for hiding this comment

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

why are we lower-casing the tag name? I think someone explained this once, but is there any reason to do so?

}

// Return the opposite if it has a negator operator (!).
return isset( $has_negation_operator ) ? ! $current : $current;
Copy link
Member

Choose a reason for hiding this comment

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

personal styling note, but I find a boolean value here might be clearer than a true-or-absent value

$should_negate_context_value = '!' === $path[0];
$path = $should_negate_context_value ? substr( $path, 1 ) : $path;

…

return $should_negate_context_value ? ! $current : $current;

list( , $class_name ) = explode( '--', $attr );
if ( empty( $class_name ) ) {
continue;
}
Copy link
Member

Choose a reason for hiding this comment

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

how about a single function somewhere, maybe in class-wp-directive-processor.php, that parses the directive name. it's not much code, but given that we're already doing it in different ways and in each of these files, seems like it could be clearer to move it inward

// class-wp-directive-processor {
	public static function parse_attribute_name( $name ) {
		/*
		 * Blah blah blargy bloop.
		 *
		 * Example
		 *     'wp-island'            => array( 'wp-island', null )
		 *     'wp-bind--src'         => array( 'wp-bind', 'src' )
		 *     'wp-thing--and--thang' => array( 'wp-thing', 'and--thang' )
		 */
		return explode( '--', $name, 2 );
	}
}
list( $type ) = WP_Directive_Processor::parse_attribute_name( $name );

list( $type, $class_name ) = WP_Directive_Processor::parse_attribute_name( $name );

}

$new_context = json_decode( $value, true );
// TODO: Error handling.
Copy link
Member

Choose a reason for hiding this comment

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

it's not much work to do basic error-handling here. right now we're setting context to null on any JSON decode failures. maybe we should abort in this case and circle back swiftly on it

$new_context = json_decode( $value, true );
if ( null === $new_context ) {
	return;
}

$context->set_context( $new_context );

}

$new_context = json_decode( $value, true );
// TODO: Error handling.
Copy link
Member

Choose a reason for hiding this comment

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

it's not much work to do basic error-handling here. right now we're setting context to null on any JSON decode failures. maybe we should abort in this case and circle back swiftly on it

$new_context = json_decode( $value, true );
if ( null === $new_context ) {
	return;
}

$context->set_context( $new_context );

luisherranz added a commit that referenced this pull request Jun 28, 2023
* Start with package.json and README

* Add new package to docs/manifest.json

* Copy-paste runtime files from block-library

* Add .npmrc file

* Add interactivity package to dependencies

* Create a custom webpack config for interactivity

* Expose interactivity runtime in `wp.interactivity`

* Update package-lock

* Add `@wordpress/interactivity` to block-library deps

* Rename entry point to index

* Remove vendors chunk

* Add oddly required aliases

* Add view prefix to interactivity.js files

* Use view-interactivity files when enabled

* Stop adding defer to Interactivity scripts

* Remove webpack config for interactivity.js files

* Remove interactivity runtime from block-library

* Remove interactivity runtime from sideEffects

* Undo temporary fix for Interactivity API in dependency-extraction-webpack-plugin

* Remove script loader for Interactivity API runtime

* Remove block-librar/interactivity from build_files

* Add src/index.js to Interactivity API entry file

* Remove unnecessary aliases

* Restore data-wp-body directive

* Interactivity API: add `wp_store` (#51191)

* Add `wp_store` to the Interactivity API

* Rename WP_Interactivity_Store and move filter to scripts file

* Remove todos to change the store id

* Rename syntax to -- and data-wp-interactive (#51241)

* Interactivity API: initial support for SSR (#51229)

* Initial version working with basic support for wp-bind

* Add wp-context

* Add wp-class

* Add wp-style

* Add wp-text

* Add directive processing tests

* Add WP_Directive_Processor class tests

* Add wp-bind tests

* Add wp-context tests

* Add wp-class tests

* Add wp-style tests

* Add wp-text tests

* Add evaluate tests

* Fix PHP lint

* Prevent errors with incorrect JSON objects

* Add support for functions in the server

* Remove require for missing script-loader.php

* Remove missing PHP file

* Rename view file and fix block.json

* Add "interactivity" to supports and fix renaming

* Code improvements for the SSR part of the Interactivity API (#51640)

* Fix multi-line comments and add examples

* Add parse_attribute_name static method to WP_Directive_Processor

* Replace array functions with a foreach loop

* Add explanatory comment for the negation operator check

* Replace $array with $path_segments

* Minor fix for the negation operator comment

* Call only instances of Closure

* Improve negation operator code style

* Do not lower-case tags

* Use static parse_attribute_name inside directive processors

* Add basic error handling in wp-context

* Fix hidden identation errors

* Use the correct variable name

* Fix test for evaluating functions

* Remove references to "attribute" directives

* Remove emtpy lines in multi-line function calls

* Fix typo

---------

Co-authored-by: Luis Herranz <luisherranz@gmail.com>

* Add the full Interactivity API runtime (but removing the client-side navigation). (#51194)

* Add show and text directives

* Move directive bind tests

* Move the rest of e2e tests (except csn-related)

* Add interactive-blocks plugin for e2e tests

* Move test plugins one folder up

* Add plugin to .wp-env.json

* Change directive-bind spec file to use new plugin

* Move plugin to e2e-tests package

* Move HTML for directive-bind to plugin

* Update exposed properties from preact

* Refactor directive-bind spec file

* Create directive-effect block for e2e testing

* Update directive-effect spec file

* Remove unnecessary files

* Fix e2e tests for bind and effect directives

* Refactor fixtures and use them for bind and effect

* Remove unnecessary editorScript

* Fix e2e test for directive priorities

* Remove unnecessary files

* Fix negation operator

* Refactor store-tag e2e tests

* Refactor directive-class e2e tests

* Remove extra spaces

* Add util for removing all created posts

* Add block for context directive

* Add block for directive show testing

* Remove unintentionally added artifact

* Ignore artifacts generated inside /test/e2e

* Remove unused html

* Add block for directive text testing

* Add blocks for tovdom testing

* Update directives syntax in e2e tests

* Add getLink to InteractivityUtils

* Fix php lint errors

* Add disable_directives_ssr param

* Fix phpcs errors

* Fix missing phpcs error and warnings

* Remove `wp-interactivity` from `viewScript`

---------

Co-authored-by: Luis Herranz <luisherranz@gmail.com>

* Remove custom watchOptions in interactivity webpack config

* Update version and description of interactivity package

---------

Co-authored-by: Luis Herranz <luisherranz@gmail.com>
luisherranz added a commit that referenced this pull request Jun 30, 2023
…#51962)

* Start with package.json and README

* Add new package to docs/manifest.json

* Copy-paste runtime files from block-library

* Add .npmrc file

* Add interactivity package to dependencies

* Create a custom webpack config for interactivity

* Expose interactivity runtime in `wp.interactivity`

* Update package-lock

* Add `@wordpress/interactivity` to block-library deps

* Rename entry point to index

* Remove vendors chunk

* Add oddly required aliases

* Add view prefix to interactivity.js files

* Use view-interactivity files when enabled

* Stop adding defer to Interactivity scripts

* Remove webpack config for interactivity.js files

* Remove interactivity runtime from block-library

* Remove interactivity runtime from sideEffects

* Undo temporary fix for Interactivity API in dependency-extraction-webpack-plugin

* Remove script loader for Interactivity API runtime

* Remove block-librar/interactivity from build_files

* Add src/index.js to Interactivity API entry file

* Remove unnecessary aliases

* Restore data-wp-body directive

* Interactivity API: add `wp_store` (#51191)

* Add `wp_store` to the Interactivity API

* Rename WP_Interactivity_Store and move filter to scripts file

* Remove todos to change the store id

* Rename syntax to -- and data-wp-interactive (#51241)

* Interactivity API: initial support for SSR (#51229)

* Initial version working with basic support for wp-bind

* Add wp-context

* Add wp-class

* Add wp-style

* Add wp-text

* Add directive processing tests

* Add WP_Directive_Processor class tests

* Add wp-bind tests

* Add wp-context tests

* Add wp-class tests

* Add wp-style tests

* Add wp-text tests

* Add evaluate tests

* Fix PHP lint

* Prevent errors with incorrect JSON objects

* Add support for functions in the server

* Remove require for missing script-loader.php

* Remove missing PHP file

* Rename view file and fix block.json

* Add "interactivity" to supports and fix renaming

* Code improvements for the SSR part of the Interactivity API (#51640)

* Fix multi-line comments and add examples

* Add parse_attribute_name static method to WP_Directive_Processor

* Replace array functions with a foreach loop

* Add explanatory comment for the negation operator check

* Replace $array with $path_segments

* Minor fix for the negation operator comment

* Call only instances of Closure

* Improve negation operator code style

* Do not lower-case tags

* Use static parse_attribute_name inside directive processors

* Add basic error handling in wp-context

* Fix hidden identation errors

* Use the correct variable name

* Fix test for evaluating functions

* Remove references to "attribute" directives

* Remove emtpy lines in multi-line function calls

* Fix typo

---------

Co-authored-by: Luis Herranz <luisherranz@gmail.com>

* Add the full Interactivity API runtime (but removing the client-side navigation). (#51194)

* Add show and text directives

* Move directive bind tests

* Move the rest of e2e tests (except csn-related)

* Add interactive-blocks plugin for e2e tests

* Move test plugins one folder up

* Add plugin to .wp-env.json

* Change directive-bind spec file to use new plugin

* Move plugin to e2e-tests package

* Move HTML for directive-bind to plugin

* Update exposed properties from preact

* Refactor directive-bind spec file

* Create directive-effect block for e2e testing

* Update directive-effect spec file

* Remove unnecessary files

* Fix e2e tests for bind and effect directives

* Refactor fixtures and use them for bind and effect

* Remove unnecessary editorScript

* Fix e2e test for directive priorities

* Remove unnecessary files

* Fix negation operator

* Refactor store-tag e2e tests

* Refactor directive-class e2e tests

* Remove extra spaces

* Add util for removing all created posts

* Add block for context directive

* Add block for directive show testing

* Remove unintentionally added artifact

* Ignore artifacts generated inside /test/e2e

* Remove unused html

* Add block for directive text testing

* Add blocks for tovdom testing

* Update directives syntax in e2e tests

* Add getLink to InteractivityUtils

* Fix php lint errors

* Add disable_directives_ssr param

* Fix phpcs errors

* Fix missing phpcs error and warnings

* Remove `wp-interactivity` from `viewScript`

---------

Co-authored-by: Luis Herranz <luisherranz@gmail.com>

* Check that modal exists before using `contains`

---------

Co-authored-by: David Arenas <david.arenas@automattic.com>
sethrubenstein pushed a commit to pewresearch/gutenberg that referenced this pull request Jul 13, 2023
…#50906)

* Start with package.json and README

* Add new package to docs/manifest.json

* Copy-paste runtime files from block-library

* Add .npmrc file

* Add interactivity package to dependencies

* Create a custom webpack config for interactivity

* Expose interactivity runtime in `wp.interactivity`

* Update package-lock

* Add `@wordpress/interactivity` to block-library deps

* Rename entry point to index

* Remove vendors chunk

* Add oddly required aliases

* Add view prefix to interactivity.js files

* Use view-interactivity files when enabled

* Stop adding defer to Interactivity scripts

* Remove webpack config for interactivity.js files

* Remove interactivity runtime from block-library

* Remove interactivity runtime from sideEffects

* Undo temporary fix for Interactivity API in dependency-extraction-webpack-plugin

* Remove script loader for Interactivity API runtime

* Remove block-librar/interactivity from build_files

* Add src/index.js to Interactivity API entry file

* Remove unnecessary aliases

* Restore data-wp-body directive

* Interactivity API: add `wp_store` (WordPress#51191)

* Add `wp_store` to the Interactivity API

* Rename WP_Interactivity_Store and move filter to scripts file

* Remove todos to change the store id

* Rename syntax to -- and data-wp-interactive (WordPress#51241)

* Interactivity API: initial support for SSR (WordPress#51229)

* Initial version working with basic support for wp-bind

* Add wp-context

* Add wp-class

* Add wp-style

* Add wp-text

* Add directive processing tests

* Add WP_Directive_Processor class tests

* Add wp-bind tests

* Add wp-context tests

* Add wp-class tests

* Add wp-style tests

* Add wp-text tests

* Add evaluate tests

* Fix PHP lint

* Prevent errors with incorrect JSON objects

* Add support for functions in the server

* Remove require for missing script-loader.php

* Remove missing PHP file

* Rename view file and fix block.json

* Add "interactivity" to supports and fix renaming

* Code improvements for the SSR part of the Interactivity API (WordPress#51640)

* Fix multi-line comments and add examples

* Add parse_attribute_name static method to WP_Directive_Processor

* Replace array functions with a foreach loop

* Add explanatory comment for the negation operator check

* Replace $array with $path_segments

* Minor fix for the negation operator comment

* Call only instances of Closure

* Improve negation operator code style

* Do not lower-case tags

* Use static parse_attribute_name inside directive processors

* Add basic error handling in wp-context

* Fix hidden identation errors

* Use the correct variable name

* Fix test for evaluating functions

* Remove references to "attribute" directives

* Remove emtpy lines in multi-line function calls

* Fix typo

---------

Co-authored-by: Luis Herranz <luisherranz@gmail.com>

* Add the full Interactivity API runtime (but removing the client-side navigation). (WordPress#51194)

* Add show and text directives

* Move directive bind tests

* Move the rest of e2e tests (except csn-related)

* Add interactive-blocks plugin for e2e tests

* Move test plugins one folder up

* Add plugin to .wp-env.json

* Change directive-bind spec file to use new plugin

* Move plugin to e2e-tests package

* Move HTML for directive-bind to plugin

* Update exposed properties from preact

* Refactor directive-bind spec file

* Create directive-effect block for e2e testing

* Update directive-effect spec file

* Remove unnecessary files

* Fix e2e tests for bind and effect directives

* Refactor fixtures and use them for bind and effect

* Remove unnecessary editorScript

* Fix e2e test for directive priorities

* Remove unnecessary files

* Fix negation operator

* Refactor store-tag e2e tests

* Refactor directive-class e2e tests

* Remove extra spaces

* Add util for removing all created posts

* Add block for context directive

* Add block for directive show testing

* Remove unintentionally added artifact

* Ignore artifacts generated inside /test/e2e

* Remove unused html

* Add block for directive text testing

* Add blocks for tovdom testing

* Update directives syntax in e2e tests

* Add getLink to InteractivityUtils

* Fix php lint errors

* Add disable_directives_ssr param

* Fix phpcs errors

* Fix missing phpcs error and warnings

* Remove `wp-interactivity` from `viewScript`

---------

Co-authored-by: Luis Herranz <luisherranz@gmail.com>

* Remove custom watchOptions in interactivity webpack config

* Update version and description of interactivity package

---------

Co-authored-by: Luis Herranz <luisherranz@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Feature] Interactivity API API to add frontend interactivity to blocks. [Type] Experimental Experimental feature or API.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants