Skip to content
This repository has been archived by the owner on Feb 23, 2024. It is now read-only.

Serialize the Interactivity API's store from PHP and hydrate it on the client #8447

Merged
merged 27 commits into from
Feb 28, 2023
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5067e7a
Update Interactivity API
DAreRodz Jan 27, 2023
92d0d9d
Change `wp` prefixes to `woo`
DAreRodz Jan 27, 2023
0199ade
Use `woo` prefix for the directives runtime bundle
DAreRodz Jan 27, 2023
ceedc72
Merge branch 'trunk' into update/interactivity-api-prefixes
DAreRodz Feb 10, 2023
6277a3c
Update Interactivity API runtime
DAreRodz Feb 10, 2023
423c858
Hardcode php from interactivity API
DAreRodz Feb 14, 2023
9af6478
Temporarily add gutenberg plugin as dependency
DAreRodz Feb 14, 2023
36bd2c5
Merge branch 'trunk' into update/interactivity-api-store-ssr
DAreRodz Feb 20, 2023
78b7db2
Exclude Interactivity API files from phpcs checks
DAreRodz Feb 20, 2023
92f5a85
Update Interactivity API js files
DAreRodz Feb 20, 2023
0c11440
Update Interactivity API php files
DAreRodz Feb 21, 2023
97f39c1
Remove gutenberg from wp-env plugins
DAreRodz Feb 21, 2023
28dd2ad
Fix registered runtime paths
DAreRodz Feb 14, 2023
5453d6b
Fix prefixes when getting attributes in directives
DAreRodz Feb 14, 2023
1874a4c
Fix directive prefix in constants
DAreRodz Feb 21, 2023
e3df31b
Merge branch 'trunk' into update/interactivity-api-store-ssr
DAreRodz Feb 21, 2023
0cd9a23
Avoid a Fatal error when importing `wp-html`
DAreRodz Feb 21, 2023
68b0a66
Remove TODO comments from Interactivity API files
DAreRodz Feb 21, 2023
17fe079
Merge branch 'trunk' into update/interactivity-api-store-ssr
DAreRodz Feb 21, 2023
fa0ef8a
Merge branch 'trunk' into update/interactivity-api-store-ssr
DAreRodz Feb 22, 2023
92c93af
Merge branch 'trunk' into update/interactivity-api-store-ssr
DAreRodz Feb 23, 2023
a824616
Merge branch 'trunk' into update/interactivity-api-store-ssr
DAreRodz Feb 28, 2023
f7ee7d1
Add missing prefix to some global functions
DAreRodz Feb 28, 2023
ebd1119
Use true as value for boolean attributes
DAreRodz Feb 28, 2023
2f36582
Add `wp-html` file
DAreRodz Feb 28, 2023
248de47
Merge branch 'trunk' into update/interactivity-api-store-ssr
DAreRodz Feb 28, 2023
e842182
Change requires in `wp-html` with includes
DAreRodz Feb 28, 2023
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
2 changes: 1 addition & 1 deletion assets/js/interactivity/constants.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export const cstMetaTagItemprop = 'woo-client-side-transitions';
export const csnMetaTagItemprop = 'woo-client-side-navigation';
export const componentPrefix = 'woo-';
export const directivePrefix = 'data-woo-';
32 changes: 16 additions & 16 deletions assets/js/interactivity/directives.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import { useContext, useMemo, useEffect } from 'preact/hooks';
import { useSignalEffect } from '@preact/signals';
import { deepSignal, peek } from 'deepsignal';
import { directive } from './hooks';
import { prefetch, navigate, hasClientSideTransitions } from './router';
import { prefetch, navigate, canDoClientSideNavigation } from './router';

// Until useSignalEffects is fixed:
// https://github.com/preactjs/signals/issues/228
const raf = window.requestAnimationFrame;
const tick = () => new Promise( ( r ) => raf( () => raf( r ) ) );

// Check if current page has client-side transitions enabled.
const clientSideTransitions = hasClientSideTransitions( document.head );
// Check if current page can do client-side navigation.
const clientSideNavigation = canDoClientSideNavigation( document.head );

const isObject = ( item ) =>
item && typeof item === 'object' && ! Array.isArray( item );
Expand Down Expand Up @@ -91,19 +91,19 @@ export default () => {
className: name,
context: contextValue,
} );
const currentClass = element.props.class || '';
const classFinder = new RegExp(
`(^|\\s)${ name }(\\s|$)`,
'g'
);
if ( ! result )
element.props.class = element.props.class
.replace(
new RegExp( `(^|\\s)${ name }(\\s|$)`, 'g' ),
' '
)
element.props.class = currentClass
.replace( classFinder, ' ' )
.trim();
else if (
! new RegExp( `(^|\\s)${ name }(\\s|$)` ).test(
element.props.class
)
)
element.props.class += ` ${ name }`;
else if ( ! classFinder.test( currentClass ) )
element.props.class = currentClass
? `${ currentClass } ${ name }`
: name;

useEffect( () => {
// This seems necessary because Preact doesn't change the class names
Expand Down Expand Up @@ -146,13 +146,13 @@ export default () => {
} ) => {
useEffect( () => {
// Prefetch the page if it is in the directive options.
if ( clientSideTransitions && link?.prefetch ) {
if ( clientSideNavigation && link?.prefetch ) {
prefetch( href );
}
} );

// Don't do anything if it's falsy.
if ( clientSideTransitions && link !== false ) {
if ( clientSideNavigation && link !== false ) {
element.props.onclick = async ( event ) => {
event.preventDefault();

Expand Down
4 changes: 2 additions & 2 deletions assets/js/interactivity/hooks.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { h, options, createContext } from 'preact';
import { useRef } from 'preact/hooks';
import { store } from './wpx';
import { rawStore as store } from './store';
import { componentPrefix } from './constants';

// Main context.
Expand All @@ -18,7 +18,7 @@ export const component = ( name, Comp ) => {
componentMap[ name ] = Comp;
};

// Resolve the path to some property of the wpx object.
// Resolve the path to some property of the store object.
const resolve = ( path, context ) => {
let current = { ...store, context };
path.split( '.' ).forEach( ( p ) => ( current = current[ p ] ) );
Expand Down
1 change: 1 addition & 0 deletions assets/js/interactivity/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import registerDirectives from './directives';
import registerComponents from './components';
import { init } from './router';
export { store } from './store';

/**
* Initialize the initial vDOM.
Expand Down
12 changes: 6 additions & 6 deletions assets/js/interactivity/router.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { hydrate, render } from 'preact';
import { toVdom, hydratedIslands } from './vdom';
import { createRootFragment } from './utils';
import { cstMetaTagItemprop, directivePrefix } from './constants';
import { csnMetaTagItemprop, directivePrefix } from './constants';

// The root to render the vdom (document.body).
let rootFragment;
Expand All @@ -17,10 +17,10 @@ const cleanUrl = ( url ) => {
return u.pathname + u.search;
};

// Helper to check if a page has client-side transitions activated.
export const hasClientSideTransitions = ( dom ) =>
// Helper to check if a page can do client-side navigation.
export const canDoClientSideNavigation = ( dom ) =>
dom
.querySelector( `meta[itemprop='${ cstMetaTagItemprop }']` )
.querySelector( `meta[itemprop='${ csnMetaTagItemprop }']` )
?.getAttribute( 'content' ) === 'active';

// Fetch styles of a new page.
Expand Down Expand Up @@ -55,7 +55,7 @@ const fetchHead = async ( head ) => {
const fetchPage = async ( url ) => {
const html = await window.fetch( url ).then( ( r ) => r.text() );
const dom = new window.DOMParser().parseFromString( html, 'text/html' );
if ( ! hasClientSideTransitions( dom.head ) ) return false;
if ( ! canDoClientSideNavigation( dom.head ) ) return false;
const head = await fetchHead( dom.head );
return { head, body: toVdom( dom.body ) };
};
Expand Down Expand Up @@ -98,7 +98,7 @@ window.addEventListener( 'popstate', async () => {

// Initialize the router with the initial DOM.
export const init = async () => {
if ( hasClientSideTransitions( document.head ) ) {
if ( canDoClientSideNavigation( document.head ) ) {
// Create the root fragment to hydrate everything.
rootFragment = createRootFragment(
document.documentElement,
Expand Down
42 changes: 42 additions & 0 deletions assets/js/interactivity/store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { deepSignal } from 'deepsignal';

const isObject = ( item ) =>
item && typeof item === 'object' && ! Array.isArray( item );

export const deepMerge = ( target, source ) => {
if ( isObject( target ) && isObject( source ) ) {
for ( const key in source ) {
if ( isObject( source[ key ] ) ) {
if ( ! target[ key ] ) Object.assign( target, { [ key ]: {} } );
deepMerge( target[ key ], source[ key ] );
} else {
Object.assign( target, { [ key ]: source[ key ] } );
}
}
}
};

const getSerializedState = () => {
const storeTag = document.querySelector(
`script[type="application/json"]#store`
);
if ( ! storeTag ) return {};
try {
const { state } = JSON.parse( storeTag.textContent );
if ( isObject( state ) ) return state;
throw Error( 'Parsed state is not an object' );
} catch ( e ) {
console.log( e );
}
return {};
};

const rawState = getSerializedState();
export const rawStore = { state: deepSignal( rawState ) };

if ( typeof window !== 'undefined' ) window.store = rawStore;

export const store = ( { state, ...block } ) => {
deepMerge( rawStore, block );
deepMerge( rawState, state );
};
27 changes: 0 additions & 27 deletions assets/js/interactivity/wpx.js

This file was deleted.

3 changes: 3 additions & 0 deletions phpcs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,7 @@
<rule ref="Generic.Arrays.DisallowShortArraySyntax.Found">
<exclude-pattern>src/*</exclude-pattern>
</rule>

<!-- Exclude Interactivity API-->
<exclude-pattern>./src/Interactivity/*</exclude-pattern>
</ruleset>
22 changes: 22 additions & 0 deletions src/Interactivity/directives/attributes/woo-bind.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

require_once __DIR__ . '/../utils.php';

function process_woo_bind( $tags, $context ) {
if ( $tags->is_tag_closer() ) {
return;
}

$prefixed_attributes = $tags->get_attribute_names_with_prefix( 'data-woo-bind:' );

foreach ( $prefixed_attributes as $attr ) {
list( , $bound_attr ) = explode( ':', $attr );
if ( empty( $bound_attr ) ) {
continue;
}

$expr = $tags->get_attribute( $attr );
$value = evaluate( $expr, $context->get_context() );
$tags->set_attribute( $bound_attr, $value );
}
}
26 changes: 26 additions & 0 deletions src/Interactivity/directives/attributes/woo-class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

require_once __DIR__ . '/../utils.php';

function process_woo_class( $tags, $context ) {
if ( $tags->is_tag_closer() ) {
return;
}

$prefixed_attributes = $tags->get_attribute_names_with_prefix( 'data-woo-class:' );

foreach ( $prefixed_attributes as $attr ) {
list( , $class_name ) = explode( ':', $attr );
if ( empty( $class_name ) ) {
continue;
}

$expr = $tags->get_attribute( $attr );
$add_class = evaluate( $expr, $context->get_context() );
if ( $add_class ) {
$tags->add_class( $class_name );
} else {
$tags->remove_class( $class_name );
}
}
}
18 changes: 18 additions & 0 deletions src/Interactivity/directives/attributes/woo-context.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

function process_woo_context_attribute( $tags, $context ) {
if ( $tags->is_tag_closer() ) {
$context->rewind_context();
return;
}

$value = $tags->get_attribute( 'data-woo-context' );
if ( null === $value ) {
// No woo-context directive.
return;
}

$new_context = json_decode( $value, true );

$context->set_context( $new_context );
}
29 changes: 29 additions & 0 deletions src/Interactivity/directives/attributes/woo-style.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

require_once __DIR__ . '/../utils.php';

function process_woo_style( $tags, $context ) {
if ( $tags->is_tag_closer() ) {
return;
}

$prefixed_attributes = $tags->get_attribute_names_with_prefix( 'data-woo-style:' );

foreach ( $prefixed_attributes as $attr ) {
list( , $style_name ) = explode( ':', $attr );
if ( empty( $style_name ) ) {
continue;
}

$expr = $tags->get_attribute( $attr );
$style_value = evaluate( $expr, $context->get_context() );
if ( $style_value ) {
$style_attr = $tags->get_attribute( 'style' );
$style_attr = set_style( $style_attr, $style_name, $style_value );
$tags->set_attribute( 'style', $style_attr );
} else {
// $tags->remove_class( $style_name );
}
}
}

73 changes: 73 additions & 0 deletions src/Interactivity/directives/class-woo-directive-context.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php
/**
* Context data implementation.
*
* @package block-hydration-experiments
*/

/**
* This is a data structure to hold the current context.
*
* Whenever encountering a `woo-context` directive, we need to update
* the context with the data found in that directive. Conversely,
* when "leaving" that context (by encountering a closing tag), we
* need to reset the context to its previous state. This means that
* we actually need sort of a stack to keep track of all nested contexts.
*
* Example:
*
* <woo-context data='{ "foo": 123 }'>
* <!-- foo should be 123 here. -->
* <woo-context data='{ "foo": 456 }'>
* <!-- foo should be 456 here. -->
* </woo-context>
* <!-- foo should be reset to 123 here. -->
* </woo-context>
*/
class Woo_Directive_Context {
/**
* The stack used to store contexts internally.
*
* @var array An array of contexts.
*/
protected $stack = array( array() );

/**
* Constructor.
*
* Accepts a context as an argument to initialize this with.
*
* @param array $context A context.
*/
function __construct( $context = array() ) {
$this->set_context( $context );
}

/**
* Return the current context.
*
* @return array The current context.
*/
public function get_context() {
return end( $this->stack );
}

/**
* Set the current context.
*
* @param array $context The context to be set.
* @return void
*/
public function set_context( $context ) {
array_push( $this->stack, array_replace_recursive( $this->get_context(), $context ) );
}

/**
* Reset the context to its previous state.
*
* @return void
*/
public function rewind_context() {
array_pop( $this->stack );
}
}
Loading