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

Implement speculative loading of the search form #1297

Draft
wants to merge 5 commits into
base: trunk
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"devDependencies": {
"@octokit/rest": "^19.0.5",
"@wordpress/env": "^9.6.0",
"@wordpress/interactivity": "6.0.0",
"@wordpress/scripts": "^26.19.0",
"commander": "^9.4.1",
"copy-webpack-plugin": "^12.0.2",
Expand Down
25 changes: 12 additions & 13 deletions plugins/speculation-rules/helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,8 @@
* @return array<string, array<int, array<string, mixed>>> Associative array of speculation rules by type.
*/
function plsr_get_speculation_rules(): array {
$option = get_option( 'plsr_speculation_rules' );

/*
* This logic is only relevant for edge-cases where the setting may not be registered,
* a.k.a. defensive coding.
*/
if ( ! $option || ! is_array( $option ) ) {
$option = plsr_get_setting_default();
} else {
$option = array_merge( plsr_get_setting_default(), $option );
}

$mode = (string) $option['mode'];
$option = plsr_get_setting();
$mode = $option['mode'];
$eagerness = $option['eagerness'];

$prefixer = new PLSR_URL_Pattern_Prefixer();
Expand Down Expand Up @@ -123,3 +112,13 @@ static function ( string $href_exclude_path ) use ( $prefixer ): string {

return array( $mode => $rules );
}

/**
* Adds config for the speculative-loading search form.
*
* @since n.e.x.t
*/
function plsr_add_search_form_config(): void {
$setting = plsr_get_setting();
wp_interactivity_config( 'speculationRules', array( 'mode' => $setting['mode'] ) );
}
72 changes: 72 additions & 0 deletions plugins/speculation-rules/hooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,75 @@ function plsr_render_generator_meta_tag(): void {
echo '<meta name="generator" content="speculation-rules ' . esc_attr( SPECULATION_RULES_VERSION ) . '">' . "\n";
}
add_action( 'wp_head', 'plsr_render_generator_meta_tag' );

/**
* Filters the HTML output of the search form to inject speculative loading interactivity.
*
* @since n.e.x.t
*
* @param string|mixed $form The search form HTML output.
* @return string Filtered HTML.
*/
function plsr_filter_searchform( $form ): string {
if ( ! is_string( $form ) ) {
return '';
}

$namespace = 'speculationRules';
$directive_prefix = 'data-wp-on'; // TODO: Use data-wp-on-async when available.

$p = new WP_HTML_Tag_Processor( $form );
while ( $p->next_tag() ) {
if ( 'FORM' === $p->get_tag() ) {
if ( ! $p->get_attribute( 'data-wp-interactive' ) ) {
$p->set_attribute( 'data-wp-interactive', $namespace );
}
// Create context if not already present.
// TODO: Should there be namespaced context?
if ( ! $p->get_attribute( 'data-wp-context' ) ) {
$p->set_attribute( 'data-wp-context', '{}' );
}
Comment on lines +85 to +88
Copy link
Member Author

Choose a reason for hiding this comment

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

See wp_interactivity_data_wp_context() which has a $store_namepace argument. The logic in that function could be replicated here. However, it seems this would conflict when there is already a data-wp-context attribute present.

Copy link
Member

Choose a reason for hiding this comment

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

I have created a discussion to see how we can solve that problem in a general way for all the directives:

// Note: This directive only subscribes to the properties that are accessed during its execution.
$p->set_attribute( "data-wp-watch--{$namespace}", "{$namespace}::callbacks.doSpeculativeLoad" );
$p->set_attribute( "data-wp-on--submit--{$namespace}", "{$namespace}::actions.handleFormSubmit" );

$p->set_attribute( "{$directive_prefix}--change--{$namespace}", "{$namespace}::actions.updateSpeculativeLoadUrl" );

wp_enqueue_script_module( 'speculation-rules-search-form' );
plsr_add_search_form_config();
} elseif (
( 'INPUT' === $p->get_tag() || 'BUTTON' === $p->get_tag() )
&&
'submit' === $p->get_attribute( 'type' )
) {
$p->set_attribute( "{$directive_prefix}--focus--{$namespace}", "{$namespace}::actions.updateSpeculativeLoadUrl" );
$p->set_attribute( "{$directive_prefix}--pointerover--{$namespace}", "{$namespace}::actions.updateSpeculativeLoadUrl" );
} elseif (
'INPUT' === $p->get_tag()
&&
's' === $p->get_attribute( 'name' )
) {
$p->set_attribute( "{$directive_prefix}--keydown--{$namespace}", "{$namespace}::actions.handleInputKeydown" );
}
}

return $p->get_updated_html();
}
add_filter( 'get_search_form', 'plsr_filter_searchform' );
add_filter( 'render_block_core/search', 'plsr_filter_searchform' );

/**
* Registers script module for the speculatively loading search form.
*
* @since n.e.x.t
*/
function plsr_register_script_module(): void {
wp_register_script_module(
'speculation-rules-search-form',
plugin_dir_url( __FILE__ ) . 'search-form.js',
array(
array( 'id' => '@wordpress/interactivity' ),
)
);
}
add_action( 'wp_enqueue_scripts', 'plsr_register_script_module' );
74 changes: 74 additions & 0 deletions plugins/speculation-rules/search-form.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import {
store,
getConfig,
getContext,
getElement,
} from '@wordpress/interactivity';

const { actions } = store( 'speculationRules', {
callbacks: {
doSpeculativeLoad: () => {
/**
* @type {Object}
* @property {string} [speculativeLoadUrl] Speculative load URL.
*/
const context = getContext();
const scriptId = 'speculation-rules-search-form';
const existingScript = document.getElementById( scriptId );
if ( ! context.speculativeLoadUrl ) {
if ( existingScript ) {
existingScript.remove();
}
} else {
const script = document.createElement( 'script' );
script.type = 'speculationrules';
script.id = scriptId;
const rules = {
[ getConfig().mode ]: [
{
source: 'list',
urls: [ context.speculativeLoadUrl ],
},
],
};
script.textContent = JSON.stringify( rules );

if ( existingScript ) {
existingScript.replaceWith( script );
} else {
document.body.appendChild( script );
}
}
},
},
actions: {
// TODO: Is this really actually callback?
updateSpeculativeLoadUrl: () => {
const context = getContext();
const { ref } = getElement();
const form = ref.closest( 'form' );
const formData = new FormData( form );
if ( ! formData.get( 's' ) ) {
context.speculativeLoadUrl = null;
} else {
const url = new URL( form.action );
url.search = new URLSearchParams( formData ).toString();
context.speculativeLoadUrl = url.href;
}
},
handleInputKeydown: ( event ) => {
// Eke out a few milliseconds when hitting enter on the input to submit.
if ( event.key === 'Enter' ) {
actions.updateSpeculativeLoadUrl();
Copy link
Member Author

Choose a reason for hiding this comment

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

An action calling another action. Is this bad?

Copy link
Member

Choose a reason for hiding this comment

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

That's fine. Even expected 🙂

}
},
handleFormSubmit: ( event ) => {
event.preventDefault();
const form = event.target;
const formData = new FormData( form );
const url = new URL( form.action );
url.search = new URLSearchParams( formData ).toString();
location.href = url.href;
},
},
} );
48 changes: 48 additions & 0 deletions plugins/speculation-rules/settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ function plsr_get_eagerness_labels(): array {
*
* @since 1.0.0
*
* @phpstan-return array{ mode: 'prerender', eagerness: 'moderate' }
* @return array<string, string> {
* Default setting value.
*
Expand All @@ -59,12 +60,59 @@ function plsr_get_setting_default(): array {
);
}

/**
* Gets setting.
*
* @since n.e.x.t
*
* @phpstan-return array{
* mode: 'prerender'|'prefetch',
* eagerness: 'conservative'|'moderate'|'eager'
* }
* @return array<string, string> {
* Setting value.
*
* @type string $mode Mode.
* @type string $eagerness Eagerness.
* }
*/
function plsr_get_setting(): array {
$setting = get_option( 'plsr_speculation_rules' );

/*
* This logic is only relevant for edge-cases where the setting may not be registered,
* a.k.a. defensive coding.
*/
if ( ! $setting || ! is_array( $setting ) ) {
$setting = plsr_get_setting_default();
} else {
$setting = array_merge( plsr_get_setting_default(), $setting );
}

/**
* Validated setting.
*
* The value is validated by {@see plsr_sanitize_setting()}.
*
* @var array{
* mode: 'prerender'|'prefetch',
* eagerness: 'conservative'|'moderate'|'eager'
* } $setting
*/
return $setting;
}

/**
* Sanitizes the setting for Speculative Loading configuration.
*
* @since 1.0.0
*
* @param mixed $input Setting to sanitize.

* @phpstan-return array{
* mode: 'prerender'|'prefetch',
* eagerness: 'conservative'|'moderate'|'eager'
* }
* @return array<string, string> {
* Sanitized setting.
*
Expand Down
Loading
Loading