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

Introduce withSyncEvent action wrapper utility and proxy event object whenever it is not used #68097

Open
wants to merge 26 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
60448fe
Implement withSyncEvent action wrapper utility.
felixarntz Dec 18, 2024
dba93ec
Prepare Interactivity API infrastructure for awareness of action prio…
felixarntz Dec 18, 2024
5201460
Merge branch 'trunk' into add/64944-with-sync-event
felixarntz Jan 2, 2025
dad7083
Proxy event object when withSyncEvent() is not used.
felixarntz Jan 2, 2025
524bf72
Merge branch 'trunk' into add/64944-with-sync-event
felixarntz Jan 21, 2025
21e51be
Ensure generator functions using withSyncEvent() are wrapped correctl…
felixarntz Jan 21, 2025
a582f01
Update Interactivity API documentation to reference withSyncEvent().
felixarntz Jan 21, 2025
5d68fcb
Use withSyncEvent() in all built-in actions that require it.
felixarntz Jan 21, 2025
d5de596
Merge branch 'trunk' into add/64944-with-sync-event
felixarntz Jan 22, 2025
621a9c9
Minor fixes for withSyncEvent docs.
felixarntz Jan 22, 2025
8a15886
Merge branch 'trunk' into add/64944-with-sync-event
felixarntz Jan 29, 2025
ec3d662
Clarify documentation.
felixarntz Jan 29, 2025
b183fcd
Merge branch 'add/64944-with-sync-event' of github.com:WordPress/gute…
felixarntz Jan 29, 2025
bf3dbcc
Enhance withSyncEvent implementation and ensure the sync flag is main…
felixarntz Jan 29, 2025
2dd6eab
Add doc block for wrapEventAsync().
felixarntz Jan 29, 2025
682ee6f
Use more specific types for event proxy handler.
felixarntz Jan 29, 2025
dff811c
Merge branch 'trunk' into add/64944-with-sync-event
felixarntz Feb 10, 2025
f0fa92a
Amend callback in withSyncEvent instead of wrapping it.
felixarntz Feb 10, 2025
21f96c4
Merge branch 'trunk' into add/64944-with-sync-event
felixarntz Feb 12, 2025
524b30d
Merge branch 'trunk' into add/64944-with-sync-event
felixarntz Feb 13, 2025
2fbfd0e
Revert "Prepare Interactivity API infrastructure for awareness of act…
felixarntz Feb 13, 2025
0dbd15f
Update evaluate() to no longer invoke functions (except where needed …
felixarntz Feb 13, 2025
5a6eca3
Export withSyncEvent
luisherranz Feb 18, 2025
dc5fc98
Merge branch 'trunk' into add/64944-with-sync-event
felixarntz Feb 18, 2025
d2a990a
Fix evaluate to return scoped function and always reset scope.
felixarntz Feb 18, 2025
a02b873
Update custom directives for e2e tests to account for evaluate behavi…
felixarntz Feb 18, 2025
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
99 changes: 56 additions & 43 deletions packages/interactivity/src/directives.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,19 @@ const cssStringToObject = (
const getGlobalEventDirective = (
type: 'window' | 'document'
): DirectiveCallback => {
return ( { directives, evaluate } ) => {
return ( { directives, resolveEntry, evaluateResolved } ) => {
directives[ `on-${ type }` ]
.filter( isNonDefaultDirectiveSuffix )
.forEach( ( entry ) => {
const eventName = entry.suffix.split( '--', 1 )[ 0 ];
useInit( () => {
const cb = ( event: Event ) => evaluate( entry, event );
const cb = ( event: Event ) => {
const resolved = resolveEntry( entry );
if ( ! resolved.value?.sync ) {
// TODO: Wrap event in proxy.
}
evaluateResolved( resolved, event );
};
const globalVar = type === 'window' ? window : document;
globalVar.addEventListener( eventName, cb );
return () => globalVar.removeEventListener( eventName, cb );
Expand Down Expand Up @@ -263,51 +269,58 @@ export default () => {
} );

// data-wp-on--[event]
directive( 'on', ( { directives: { on }, element, evaluate } ) => {
const events = new Map< string, Set< DirectiveEntry > >();
on.filter( isNonDefaultDirectiveSuffix ).forEach( ( entry ) => {
const event = entry.suffix.split( '--' )[ 0 ];
if ( ! events.has( event ) ) {
events.set( event, new Set< DirectiveEntry >() );
}
events.get( event )!.add( entry );
} );
directive(
'on',
( { directives: { on }, element, resolveEntry, evaluateResolved } ) => {
const events = new Map< string, Set< DirectiveEntry > >();
on.filter( isNonDefaultDirectiveSuffix ).forEach( ( entry ) => {
const event = entry.suffix.split( '--' )[ 0 ];
if ( ! events.has( event ) ) {
events.set( event, new Set< DirectiveEntry >() );
}
events.get( event )!.add( entry );
} );

events.forEach( ( entries, eventType ) => {
const existingHandler = element.props[ `on${ eventType }` ];
element.props[ `on${ eventType }` ] = ( event: Event ) => {
entries.forEach( ( entry ) => {
if ( existingHandler ) {
existingHandler( event );
}
let start;
if ( globalThis.IS_GUTENBERG_PLUGIN ) {
if ( globalThis.SCRIPT_DEBUG ) {
start = performance.now();
events.forEach( ( entries, eventType ) => {
const existingHandler = element.props[ `on${ eventType }` ];
element.props[ `on${ eventType }` ] = ( event: Event ) => {
entries.forEach( ( entry ) => {
if ( existingHandler ) {
existingHandler( event );
}
}
evaluate( entry, event );
if ( globalThis.IS_GUTENBERG_PLUGIN ) {
if ( globalThis.SCRIPT_DEBUG ) {
performance.measure(
`interactivity api on ${ entry.namespace }`,
{
// eslint-disable-next-line no-undef
start,
end: performance.now(),
detail: {
devtools: {
track: `IA: on ${ entry.namespace }`,
let start;
if ( globalThis.IS_GUTENBERG_PLUGIN ) {
if ( globalThis.SCRIPT_DEBUG ) {
start = performance.now();
}
}
const resolved = resolveEntry( entry );
if ( ! resolved.value?.sync ) {
// TODO: Wrap event in proxy.
}
evaluateResolved( resolved, event );
if ( globalThis.IS_GUTENBERG_PLUGIN ) {
if ( globalThis.SCRIPT_DEBUG ) {
performance.measure(
`interactivity api on ${ entry.namespace }`,
{
// eslint-disable-next-line no-undef
start,
end: performance.now(),
detail: {
devtools: {
track: `IA: on ${ entry.namespace }`,
},
},
},
}
);
}
);
}
}
}
} );
};
} );
} );
} );
};
} );
}
);

// data-wp-on-async--[event]
directive(
Expand Down
71 changes: 69 additions & 2 deletions packages/interactivity/src/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@ interface DirectiveArgs {
* context.
*/
evaluate: Evaluate;
/**
* Function that resolves a given path to value data in the store or the
* context.
*/
resolveEntry: ResolveEntry;
/**
* Function that evaluates resolved value data to a value either in the store
* or the context.
*/
evaluateResolved: EvaluateResolved;
}

export interface DirectiveCallback {
Expand All @@ -90,6 +100,17 @@ interface DirectiveOptions {
priority?: number;
}

interface ResolvedEntry {
/**
* Resolved value, either a result or a function to get the result.
*/
value: any;
/**
* Whether a negation operator should be used on the result.
*/
hasNegationOperator: boolean;
}

export interface Evaluate {
( entry: DirectiveEntry, ...args: any[] ): any;
}
Expand All @@ -98,6 +119,22 @@ interface GetEvaluate {
( args: { scope: Scope } ): Evaluate;
}

export interface ResolveEntry {
( entry: DirectiveEntry ): ResolvedEntry;
}

interface GetResolveEntry {
( args: { scope: Scope } ): ResolveEntry;
}

export interface EvaluateResolved {
( resolved: ResolvedEntry, ...args: any[] ): any;
}

interface GetEvaluateResolved {
( args: { scope: Scope } ): EvaluateResolved;
}

type PriorityLevel = string[];

interface GetPriorityLevels {
Expand Down Expand Up @@ -229,9 +266,19 @@ const resolve = ( path: string, namespace: string ) => {
};

// Generate the evaluate function.
export const getEvaluate: GetEvaluate =
export const getEvaluate: GetEvaluate = ( scopeData ) => {
const resolveEntry = getResolveEntry( scopeData );
const evaluateResolved = getEvaluateResolved( scopeData );
return ( entry, ...args ) => {
const resolved = resolveEntry( entry );
return evaluateResolved( resolved, ...args );
};
};

// Generate the resolveEntry function.
export const getResolveEntry: GetResolveEntry =
( { scope } ) =>
( entry, ...args ) => {
( entry ) => {
let { value: path, namespace } = entry;
if ( typeof path !== 'string' ) {
throw new Error( 'The `value` prop should be a string path' );
Expand All @@ -241,6 +288,19 @@ export const getEvaluate: GetEvaluate =
path[ 0 ] === '!' && !! ( path = path.slice( 1 ) );
setScope( scope );
const value = resolve( path, namespace );
resetScope();
return {
value,
hasNegationOperator,
};
};

// Generate the evaluateResolved function.
export const getEvaluateResolved: GetEvaluateResolved =
( { scope } ) =>
( resolved, ...args ) => {
const { value, hasNegationOperator } = resolved;
setScope( scope );
const result = typeof value === 'function' ? value( ...args ) : value;
resetScope();
return hasNegationOperator ? ! result : result;
Expand Down Expand Up @@ -277,6 +337,11 @@ const Directives = ( {
// element ref, state and props.
const scope = useRef< Scope >( {} as Scope ).current;
scope.evaluate = useCallback( getEvaluate( { scope } ), [] );
scope.resolveEntry = useCallback( getResolveEntry( { scope } ), [] );
scope.evaluateResolved = useCallback(
getEvaluateResolved( { scope } ),
[]
);
const { client, server } = useContext( context );
scope.context = client;
scope.serverContext = server;
Expand Down Expand Up @@ -308,6 +373,8 @@ const Directives = ( {
element,
context,
evaluate: scope.evaluate,
resolveEntry: scope.resolveEntry,
evaluateResolved: scope.evaluateResolved,
};

setScope( scope );
Expand Down
1 change: 1 addition & 0 deletions packages/interactivity/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export {
useCallback,
useMemo,
splitTask,
withSyncEvent,
} from './utils';

export { useState, useRef } from 'preact/hooks';
Expand Down
4 changes: 3 additions & 1 deletion packages/interactivity/src/scopes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import type { h as createElement, RefObject } from 'preact';
* Internal dependencies
*/
import { getNamespace } from './namespaces';
import type { Evaluate } from './hooks';
import type { Evaluate, ResolveEntry, EvaluateResolved } from './hooks';

export interface Scope {
evaluate: Evaluate;
resolveEntry: ResolveEntry;
evaluateResolved: EvaluateResolved;
context: object;
serverContext: object;
ref: RefObject< HTMLElement >;
Expand Down
12 changes: 12 additions & 0 deletions packages/interactivity/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,3 +374,15 @@ export const isPlainObject = (
typeof candidate === 'object' &&
candidate.constructor === Object
);

/**
* Indicates that the passed `callback` requires synchronous access to the event object.
*
* @param callback The event callback.
* @return Wrapped event callback.
*/
export const withSyncEvent = ( callback: Function ): Function => {
felixarntz marked this conversation as resolved.
Show resolved Hide resolved
const wrapped = ( ...args: any[] ) => callback( ...args );
felixarntz marked this conversation as resolved.
Show resolved Hide resolved
wrapped.sync = true;
return wrapped;
};
Loading