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

Commit 72ed66a

Browse files
authored
Serialize the Interactivity API's store from PHP and hydrate it on the client (#8447)
* Update Interactivity API * Change `wp` prefixes to `woo` * Use `woo` prefix for the directives runtime bundle * Update Interactivity API runtime * Hardcode php from interactivity API * Temporarily add gutenberg plugin as dependency * Exclude Interactivity API files from phpcs checks * Update Interactivity API js files * Update Interactivity API php files * Remove gutenberg from wp-env plugins * Fix registered runtime paths * Fix prefixes when getting attributes in directives * Fix directive prefix in constants * Avoid a Fatal error when importing `wp-html` * Remove TODO comments from Interactivity API files * Add missing prefix to some global functions * Use true as value for boolean attributes * Add `wp-html` file * Change requires in `wp-html` with includes
1 parent b73fbca commit 72ed66a

20 files changed

+556
-73
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
export const cstMetaTagItemprop = 'woo-client-side-transitions';
1+
export const csnMetaTagItemprop = 'woo-client-side-navigation';
22
export const componentPrefix = 'woo-';
33
export const directivePrefix = 'data-woo-';

assets/js/interactivity/directives.js

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ import { useContext, useMemo, useEffect } from 'preact/hooks';
22
import { useSignalEffect } from '@preact/signals';
33
import { deepSignal, peek } from 'deepsignal';
44
import { directive } from './hooks';
5-
import { prefetch, navigate, hasClientSideTransitions } from './router';
5+
import { prefetch, navigate, canDoClientSideNavigation } from './router';
66

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

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

1515
const isObject = ( item ) =>
1616
item && typeof item === 'object' && ! Array.isArray( item );
@@ -91,19 +91,19 @@ export default () => {
9191
className: name,
9292
context: contextValue,
9393
} );
94+
const currentClass = element.props.class || '';
95+
const classFinder = new RegExp(
96+
`(^|\\s)${ name }(\\s|$)`,
97+
'g'
98+
);
9499
if ( ! result )
95-
element.props.class = element.props.class
96-
.replace(
97-
new RegExp( `(^|\\s)${ name }(\\s|$)`, 'g' ),
98-
' '
99-
)
100+
element.props.class = currentClass
101+
.replace( classFinder, ' ' )
100102
.trim();
101-
else if (
102-
! new RegExp( `(^|\\s)${ name }(\\s|$)` ).test(
103-
element.props.class
104-
)
105-
)
106-
element.props.class += ` ${ name }`;
103+
else if ( ! classFinder.test( currentClass ) )
104+
element.props.class = currentClass
105+
? `${ currentClass } ${ name }`
106+
: name;
107107

108108
useEffect( () => {
109109
// This seems necessary because Preact doesn't change the class names
@@ -146,13 +146,13 @@ export default () => {
146146
} ) => {
147147
useEffect( () => {
148148
// Prefetch the page if it is in the directive options.
149-
if ( clientSideTransitions && link?.prefetch ) {
149+
if ( clientSideNavigation && link?.prefetch ) {
150150
prefetch( href );
151151
}
152152
} );
153153

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

assets/js/interactivity/hooks.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { h, options, createContext } from 'preact';
22
import { useRef } from 'preact/hooks';
3-
import { store } from './wpx';
3+
import { rawStore as store } from './store';
44
import { componentPrefix } from './constants';
55

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

21-
// Resolve the path to some property of the wpx object.
21+
// Resolve the path to some property of the store object.
2222
const resolve = ( path, context ) => {
2323
let current = { ...store, context };
2424
path.split( '.' ).forEach( ( p ) => ( current = current[ p ] ) );

assets/js/interactivity/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import registerDirectives from './directives';
22
import registerComponents from './components';
33
import { init } from './router';
4+
export { store } from './store';
45

56
/**
67
* Initialize the initial vDOM.

assets/js/interactivity/router.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { hydrate, render } from 'preact';
22
import { toVdom, hydratedIslands } from './vdom';
33
import { createRootFragment } from './utils';
4-
import { cstMetaTagItemprop, directivePrefix } from './constants';
4+
import { csnMetaTagItemprop, directivePrefix } from './constants';
55

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

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

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

9999
// Initialize the router with the initial DOM.
100100
export const init = async () => {
101-
if ( hasClientSideTransitions( document.head ) ) {
101+
if ( canDoClientSideNavigation( document.head ) ) {
102102
// Create the root fragment to hydrate everything.
103103
rootFragment = createRootFragment(
104104
document.documentElement,

assets/js/interactivity/store.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { deepSignal } from 'deepsignal';
2+
3+
const isObject = ( item ) =>
4+
item && typeof item === 'object' && ! Array.isArray( item );
5+
6+
export const deepMerge = ( target, source ) => {
7+
if ( isObject( target ) && isObject( source ) ) {
8+
for ( const key in source ) {
9+
if ( isObject( source[ key ] ) ) {
10+
if ( ! target[ key ] ) Object.assign( target, { [ key ]: {} } );
11+
deepMerge( target[ key ], source[ key ] );
12+
} else {
13+
Object.assign( target, { [ key ]: source[ key ] } );
14+
}
15+
}
16+
}
17+
};
18+
19+
const getSerializedState = () => {
20+
const storeTag = document.querySelector(
21+
`script[type="application/json"]#store`
22+
);
23+
if ( ! storeTag ) return {};
24+
try {
25+
const { state } = JSON.parse( storeTag.textContent );
26+
if ( isObject( state ) ) return state;
27+
throw Error( 'Parsed state is not an object' );
28+
} catch ( e ) {
29+
console.log( e );
30+
}
31+
return {};
32+
};
33+
34+
const rawState = getSerializedState();
35+
export const rawStore = { state: deepSignal( rawState ) };
36+
37+
if ( typeof window !== 'undefined' ) window.store = rawStore;
38+
39+
export const store = ( { state, ...block } ) => {
40+
deepMerge( rawStore, block );
41+
deepMerge( rawState, state );
42+
};

assets/js/interactivity/wpx.js

Lines changed: 0 additions & 27 deletions
This file was deleted.

phpcs.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,7 @@
6666
<rule ref="Generic.Arrays.DisallowShortArraySyntax.Found">
6767
<exclude-pattern>src/*</exclude-pattern>
6868
</rule>
69+
70+
<!-- Exclude Interactivity API-->
71+
<exclude-pattern>./src/Interactivity/*</exclude-pattern>
6972
</ruleset>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
require_once __DIR__ . '/../utils.php';
4+
5+
function process_woo_bind( $tags, $context ) {
6+
if ( $tags->is_tag_closer() ) {
7+
return;
8+
}
9+
10+
$prefixed_attributes = $tags->get_attribute_names_with_prefix( 'data-woo-bind:' );
11+
12+
foreach ( $prefixed_attributes as $attr ) {
13+
list( , $bound_attr ) = explode( ':', $attr );
14+
if ( empty( $bound_attr ) ) {
15+
continue;
16+
}
17+
18+
$expr = $tags->get_attribute( $attr );
19+
$value = woo_directives_evaluate( $expr, $context->get_context() );
20+
$tags->set_attribute( $bound_attr, $value );
21+
}
22+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
require_once __DIR__ . '/../utils.php';
4+
5+
function process_woo_class( $tags, $context ) {
6+
if ( $tags->is_tag_closer() ) {
7+
return;
8+
}
9+
10+
$prefixed_attributes = $tags->get_attribute_names_with_prefix( 'data-woo-class:' );
11+
12+
foreach ( $prefixed_attributes as $attr ) {
13+
list( , $class_name ) = explode( ':', $attr );
14+
if ( empty( $class_name ) ) {
15+
continue;
16+
}
17+
18+
$expr = $tags->get_attribute( $attr );
19+
$add_class = woo_directives_evaluate( $expr, $context->get_context() );
20+
if ( $add_class ) {
21+
$tags->add_class( $class_name );
22+
} else {
23+
$tags->remove_class( $class_name );
24+
}
25+
}
26+
}

0 commit comments

Comments
 (0)