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

Commit

Permalink
Update the Interactivity API files to include latest changes (#9924)
Browse files Browse the repository at this point in the history
* Update Interactivity API JS files

* Disable TS checks in the Interactivity API for now

* Add new SSR files

* Replace wp_ prefixes with wc_ ones

* Replace wp- prefix with wc-

* Replace guternberg_ prefix with woocommerce_

* Remove file comments from Gutenberg

* Rename files with `wp` prefix

* Fix code to load Interactivity API php files

* Remove TODO comments

* Replace @WordPress with @woocommerce

* Update Webpack configuration

* Fix directive prefix

* Remove interactivity folder from tsconfig exclude

* Add client-side navigation meta tag code

* Remove unneeded blocks.php file

* Fix store tag id

* Register Interactivity API runtime script

* Fix Interactivity API runtime registering

* Remove all files related to directive processing in PHP

* Move json_encode to Store's render method
  • Loading branch information
DAreRodz authored Jun 27, 2023
1 parent 6872626 commit 1d2c7dc
Show file tree
Hide file tree
Showing 27 changed files with 560 additions and 661 deletions.
26 changes: 0 additions & 26 deletions assets/js/interactivity/components.js

This file was deleted.

5 changes: 2 additions & 3 deletions assets/js/interactivity/constants.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export const csnMetaTagItemprop = 'woo-client-side-navigation';
export const componentPrefix = 'woo-';
export const directivePrefix = 'data-woo-';
export const csnMetaTagItemprop = 'wc-client-side-navigation';
export const directivePrefix = 'wc';
105 changes: 90 additions & 15 deletions assets/js/interactivity/directives.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import { useContext, useMemo, useEffect } from 'preact/hooks';
import { useSignalEffect } from '@preact/signals';
import { deepSignal, peek } from 'deepsignal';
import { useSignalEffect } from './utils';
import { directive } from './hooks';
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 can do client-side navigation.
const clientSideNavigation = canDoClientSideNavigation( document.head );

Expand All @@ -32,7 +27,7 @@ const mergeDeepSignals = ( target, source ) => {
};

export default () => {
// wp-context
// data-wc-context
directive(
'context',
( {
Expand All @@ -51,20 +46,21 @@ export default () => {
}, [ context, inheritedValue ] );

return <Provider value={ value }>{ children }</Provider>;
}
},
{ priority: 5 }
);

// wp-effect:[name]
// data-wc-effect--[name]
directive( 'effect', ( { directives: { effect }, context, evaluate } ) => {
const contextValue = useContext( context );
Object.values( effect ).forEach( ( path ) => {
useSignalEffect( () => {
evaluate( path, { context: contextValue } );
return evaluate( path, { context: contextValue } );
} );
} );
} );

// wp-on:[event]
// data-wc-on--[event]
directive( 'on', ( { directives: { on }, element, evaluate, context } ) => {
const contextValue = useContext( context );
Object.entries( on ).forEach( ( [ name, path ] ) => {
Expand All @@ -74,7 +70,7 @@ export default () => {
} );
} );

// wp-class:[classname]
// data-wc-class--[classname]
directive(
'class',
( {
Expand Down Expand Up @@ -119,22 +115,43 @@ export default () => {
}
);

// wp-bind:[attribute]
// data-wc-bind--[attribute]
directive(
'bind',
( { directives: { bind }, element, context, evaluate } ) => {
const contextValue = useContext( context );
Object.entries( bind )
.filter( ( n ) => n !== 'default' )
.forEach( ( [ attribute, path ] ) => {
element.props[ attribute ] = evaluate( path, {
const result = evaluate( path, {
context: contextValue,
} );
element.props[ attribute ] = result;

// This seems necessary because Preact doesn't change the attributes
// on the hydration, so we have to do it manually. It doesn't need
// deps because it only needs to do it the first time.
useEffect( () => {
// aria- and data- attributes have no boolean representation.
// A `false` value is different from the attribute not being
// present, so we can't remove it.
// We follow Preact's logic: https://github.com/preactjs/preact/blob/ea49f7a0f9d1ff2c98c0bdd66aa0cbc583055246/src/diff/props.js#L131C24-L136
if ( result === false && attribute[ 4 ] !== '-' ) {
element.ref.current.removeAttribute( attribute );
} else {
element.ref.current.setAttribute(
attribute,
result === true && attribute[ 4 ] !== '-'
? ''
: result
);
}
}, [] );
} );
}
);

// wp-link
// data-wc-link
directive(
'link',
( {
Expand Down Expand Up @@ -173,4 +190,62 @@ export default () => {
}
}
);

// data-wc-show
directive(
'show',
( {
directives: {
show: { default: show },
},
element,
evaluate,
context,
} ) => {
const contextValue = useContext( context );

if ( ! evaluate( show, { context: contextValue } ) )
element.props.children = (
<template>{ element.props.children }</template>
);
}
);

// data-wc-ignore
directive(
'ignore',
( {
element: {
type: Type,
props: { innerHTML, ...rest },
},
} ) => {
// Preserve the initial inner HTML.
const cached = useMemo( () => innerHTML, [] );
return (
<Type
dangerouslySetInnerHTML={ { __html: cached } }
{ ...rest }
/>
);
}
);

// data-wc-text
directive(
'text',
( {
directives: {
text: { default: text },
},
element,
evaluate,
context,
} ) => {
const contextValue = useContext( context );
element.props.children = evaluate( text, {
context: contextValue,
} );
}
);
};
138 changes: 96 additions & 42 deletions assets/js/interactivity/hooks.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,21 @@
import { h, options, createContext } from 'preact';
import { useRef } from 'preact/hooks';
import { h, options, createContext, cloneElement } from 'preact';
import { useRef, useMemo } from 'preact/hooks';
import { rawStore as store } from './store';
import { componentPrefix } from './constants';

// Main context.
const context = createContext( {} );

// WordPress Directives.
const directiveMap = {};
export const directive = ( name, cb ) => {
const directivePriorities = {};
export const directive = ( name, cb, { priority = 10 } = {} ) => {
directiveMap[ name ] = cb;
};

// WordPress Components.
const componentMap = {};
export const component = ( name, Comp ) => {
componentMap[ name ] = Comp;
directivePriorities[ name ] = priority;
};

// Resolve the path to some property of the store object.
const resolve = ( path, context ) => {
let current = { ...store, context };
const resolve = ( path, ctx ) => {
let current = { ...store, context: ctx };
path.split( '.' ).forEach( ( p ) => ( current = current[ p ] ) );
return current;
};
Expand All @@ -29,22 +24,92 @@ const resolve = ( path, context ) => {
const getEvaluate =
( { ref } = {} ) =>
( path, extraArgs = {} ) => {
// If path starts with !, remove it and save a flag.
const hasNegationOperator =
path[ 0 ] === '!' && !! ( path = path.slice( 1 ) );
const value = resolve( path, extraArgs.context );
return typeof value === 'function'
? value( {
state: store.state,
...( ref !== undefined ? { ref } : {} ),
...extraArgs,
} )
: value;
const returnValue =
typeof value === 'function'
? value( {
ref: ref.current,
...store,
...extraArgs,
} )
: value;
return hasNegationOperator ? ! returnValue : returnValue;
};

// Separate directives by priority. The resulting array contains objects
// of directives grouped by same priority, and sorted in ascending order.
const usePriorityLevels = ( directives ) =>
useMemo( () => {
const byPriority = Object.entries( directives ).reduce(
( acc, [ name, values ] ) => {
const priority = directivePriorities[ name ];
if ( ! acc[ priority ] ) acc[ priority ] = {};
acc[ priority ][ name ] = values;

return acc;
},
{}
);

return Object.entries( byPriority )
.sort( ( [ p1 ], [ p2 ] ) => p1 - p2 )
.map( ( [ , obj ] ) => obj );
}, [ directives ] );

// Directive wrapper.
const Directive = ( { type, directives, props: originalProps } ) => {
const ref = useRef( null );
const element = h( type, { ...originalProps, ref, _wrapped: true } );
const props = { ...originalProps, children: element };
const evaluate = getEvaluate( { ref: ref.current } );
const element = h( type, { ...originalProps, ref } );
const evaluate = useMemo( () => getEvaluate( { ref } ), [] );

// Add wrappers recursively for each priority level.
const byPriorityLevel = usePriorityLevels( directives );
return (
<RecursivePriorityLevel
directives={ byPriorityLevel }
element={ element }
evaluate={ evaluate }
originalProps={ originalProps }
/>
);
};

// Priority level wrapper.
const RecursivePriorityLevel = ( {
directives: [ directives, ...rest ],
element,
evaluate,
originalProps,
} ) => {
// This element needs to be a fresh copy so we are not modifying an already
// rendered element with Preact's internal properties initialized. This
// prevents an error with changes in `element.props.children` not being
// reflected in `element.__k`.
element = cloneElement( element );

// Recursively render the wrapper for the next priority level.
//
// Note that, even though we're instantiating a vnode with a
// `RecursivePriorityLevel` here, its render function will not be executed
// just yet. Actually, it will be delayed until the current render function
// has finished. That ensures directives in the current priorty level have
// run (and thus modified the passed `element`) before the next level.
const children =
rest.length > 0 ? (
<RecursivePriorityLevel
directives={ rest }
element={ element }
evaluate={ evaluate }
originalProps={ originalProps }
/>
) : (
element
);

const props = { ...originalProps, children };
const directiveArgs = { directives, props, element, context, evaluate };

for ( const d in directives ) {
Expand All @@ -58,27 +123,16 @@ const Directive = ( { type, directives, props: originalProps } ) => {
// Preact Options Hook called each time a vnode is created.
const old = options.vnode;
options.vnode = ( vnode ) => {
const type = vnode.type;
const { directives } = vnode.props;

if (
typeof type === 'string' &&
type.slice( 0, componentPrefix.length ) === componentPrefix
) {
vnode.props.children = h(
componentMap[ type.slice( componentPrefix.length ) ],
{ ...vnode.props, context, evaluate: getEvaluate() },
vnode.props.children
);
} else if ( directives ) {
if ( vnode.props.__directives ) {
const props = vnode.props;
delete props.directives;
if ( ! props._wrapped ) {
vnode.props = { type: vnode.type, directives, props };
vnode.type = Directive;
} else {
delete props._wrapped;
}
const directives = props.__directives;
delete props.__directives;
vnode.props = {
type: vnode.type,
directives,
props,
};
vnode.type = Directive;
}

if ( old ) old( vnode );
Expand Down
8 changes: 4 additions & 4 deletions assets/js/interactivity/index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import registerDirectives from './directives';
import registerComponents from './components';
import { init } from './router';
export { store } from './store';
export { navigate } from './router';

/**
* Initialize the initial vDOM.
* Initialize the Interactivity API.
*/
document.addEventListener( 'DOMContentLoaded', async () => {
registerDirectives();
registerComponents();
await init();
console.log( 'hydrated!' );
// eslint-disable-next-line no-console
console.log( 'Interactivity API started' );
} );
Loading

0 comments on commit 1d2c7dc

Please sign in to comment.