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

Interactivity API refactor to TypeScript (utils & kebabToCamelCase) #60149

Merged
merged 11 commits into from
Apr 10, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
useCallback as _useCallback,
useEffect as _useEffect,
useLayoutEffect as _useLayoutEffect,
type EffectCallback,
type Inputs,
} from 'preact/hooks';
import { effect } from '@preact/signals';

Expand All @@ -21,8 +23,19 @@ import {
resetNamespace,
} from './hooks';

const afterNextFrame = ( callback ) => {
return new Promise( ( resolve ) => {
interface Flusher {
readonly flush: () => void;
readonly dispose: () => void;
}

/**
* Executes a callback function after the next frame is rendered.
*
* @param callback The callback function to be executed.
* @return A promise that resolves after the callback function is executed.
*/
const afterNextFrame = ( callback: () => void ) => {
return new Promise< void >( ( resolve ) => {
const done = () => {
clearTimeout( timeout );
window.cancelAnimationFrame( raf );
Expand All @@ -36,35 +49,50 @@ const afterNextFrame = ( callback ) => {
} );
};

// Using the mangled properties:
// this.c: this._callback
// this.x: this._compute
// https://github.com/preactjs/signals/blob/main/mangle.json
function createFlusher( compute, notify ) {
let flush;
/**
* Creates a Flusher object that can be used to flush computed values and notify listeners.
*
* Using the mangled properties:
* this.c: this._callback
* this.x: this._compute
* https://github.com/preactjs/signals/blob/main/mangle.json
*
* @param compute The function that computes the value to be flushed.
* @param notify The function that notifies listeners when the value is flushed.
* @return The Flusher object with `flush` and `dispose` properties.
*/
function createFlusher( compute: () => unknown, notify: () => void ): Flusher {
let flush: () => void;
const dispose = effect( function () {
flush = this.c.bind( this );
this.x = compute;
this.c = notify;
return compute();
} );
return { flush, dispose };
return { flush, dispose } as const;
}

// Version of `useSignalEffect` with a `useEffect`-like execution. This hook
// implementation comes from this PR, but we added short-cirtuiting to avoid
// infinite loops: https://github.com/preactjs/signals/pull/290
export function useSignalEffect( callback ) {
/**
* Custom hook that executes a callback function whenever a signal is triggered.
* Version of `useSignalEffect` with a `useEffect`-like execution. This hook
* implementation comes from this PR, but we added short-cirtuiting to avoid
* infinite loops: https://github.com/preactjs/signals/pull/290
*
* @param callback The callback function to be executed.
*/
export function useSignalEffect( callback: () => unknown ) {
_useEffect( () => {
let eff = null;
let isExecuting = false;

const notify = async () => {
if ( eff && ! isExecuting ) {
isExecuting = true;
await afterNextFrame( eff.flush );
isExecuting = false;
}
};

eff = createFlusher( callback, notify );
return eff.dispose;
}, [] );
Expand All @@ -75,17 +103,30 @@ export function useSignalEffect( callback ) {
* accessible whenever the function runs. This is primarily to make the scope
* available inside hook callbacks.
*
* @param {Function} func The passed function.
* @return {Function} The wrapped function.
* Asyncronous functions should use generators that yield promises instead of awaiting them.
* See the documentation for details: https://developer.wordpress.org/block-editor/reference-guides/packages/packages-interactivity/packages-interactivity-api-reference/#the-store
*
* @param func The passed function.
* @return The wrapped function.
*/
export const withScope = ( func ) => {
export function withScope<
Func extends ( ...args: any[] ) => Generator< any, any >,
>(
func: Func
): (
...args: Parameters< Func >
) => ReturnType< Func > extends Generator< any, infer Return >
? Promise< Return >
: never;
export function withScope< Func extends Function >( func: Func ): Func;
export function withScope( func ) {
Copy link
Member

Choose a reason for hiding this comment

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

I changed this to be a function so I could use function overloading for the typing here.

See types demo in practice

const scope = getScope();
const ns = getNamespace();
if ( func?.constructor?.name === 'GeneratorFunction' ) {
return async ( ...args ) => {
const gen = func( ...args );
let value;
let it;
let value: any;
let it: any;
while ( true ) {
setNamespace( ns );
setScope( scope );
Expand Down Expand Up @@ -115,7 +156,7 @@ export const withScope = ( func ) => {
resetScope();
}
};
};
}

/**
* Accepts a function that contains imperative code which runs whenever any of
Expand All @@ -125,9 +166,9 @@ export const withScope = ( func ) => {
* This hook makes the element's scope available so functions like
* `getElement()` and `getContext()` can be used inside the passed callback.
*
* @param {Function} callback The hook callback.
* @param callback The hook callback.
*/
export function useWatch( callback ) {
export function useWatch( callback: () => unknown ) {
useSignalEffect( withScope( callback ) );
}

Expand All @@ -138,9 +179,9 @@ export function useWatch( callback ) {
* This hook makes the element's scope available so functions like
* `getElement()` and `getContext()` can be used inside the passed callback.
*
* @param {Function} callback The hook callback.
* @param callback The hook callback.
*/
export function useInit( callback ) {
export function useInit( callback: EffectCallback ) {
_useEffect( withScope( callback ), [] );
}

Expand All @@ -152,12 +193,12 @@ export function useInit( callback ) {
* available so functions like `getElement()` and `getContext()` can be used
* inside the passed callback.
*
* @param {Function} callback Imperative function that can return a cleanup
* function.
* @param {any[]} inputs If present, effect will only activate if the
* values in the list change (using `===`).
* @param callback Imperative function that can return a cleanup
* function.
* @param inputs If present, effect will only activate if the
* values in the list change (using `===`).
*/
export function useEffect( callback, inputs ) {
export function useEffect( callback: EffectCallback, inputs: Inputs ) {
_useEffect( withScope( callback ), inputs );
}

Expand All @@ -169,12 +210,12 @@ export function useEffect( callback, inputs ) {
* scope available so functions like `getElement()` and `getContext()` can be
* used inside the passed callback.
*
* @param {Function} callback Imperative function that can return a cleanup
* function.
* @param {any[]} inputs If present, effect will only activate if the
* values in the list change (using `===`).
* @param callback Imperative function that can return a cleanup
* function.
* @param inputs If present, effect will only activate if the
* values in the list change (using `===`).
*/
export function useLayoutEffect( callback, inputs ) {
export function useLayoutEffect( callback: EffectCallback, inputs: Inputs ) {
_useLayoutEffect( withScope( callback ), inputs );
}

Expand All @@ -186,16 +227,17 @@ export function useLayoutEffect( callback, inputs ) {
* scope available so functions like `getElement()` and `getContext()` can be
* used inside the passed callback.
*
* @template {Function} T The callback function type.
* @param callback Callback function.
* @param inputs If present, the callback will only be updated if the
* values in the list change (using `===`).
*
* @param {T} callback Callback function.
* @param {ReadonlyArray<unknown>} inputs If present, the callback will only be updated if the
* values in the list change (using `===`).
*
* @return {T} The callback function.
* @return The callback function.
*/
export function useCallback( callback, inputs ) {
return _useCallback( withScope( callback ), inputs );
export function useCallback< T extends Function >(
callback: T,
inputs: Inputs
): T {
return _useCallback< T >( withScope( callback ), inputs );
}

/**
Expand All @@ -206,34 +248,42 @@ export function useCallback( callback, inputs ) {
* available so functions like `getElement()` and `getContext()` can be used
* inside the passed factory function.
*
* @template {unknown} T The memoized value.
*
* @param {() => T} factory Factory function that returns that value for memoization.
* @param {ReadonlyArray<unknown>} inputs If present, the factory will only be run to recompute if
* the values in the list change (using `===`).
* @param factory Factory function that returns that value for memoization.
* @param inputs If present, the factory will only be run to recompute if
* the values in the list change (using `===`).
*
* @return {T} The memoized value.
* @return The memoized value.
*/
export function useMemo( factory, inputs ) {
export function useMemo< T >( factory: () => T, inputs: Inputs ): T {
return _useMemo( withScope( factory ), inputs );
}

// For wrapperless hydration.
// See https://gist.github.com/developit/f4c67a2ede71dc2fab7f357f39cff28c
export const createRootFragment = ( parent, replaceNode ) => {
/**
* Creates a root fragment by replacing a node or an array of nodes in a parent element.
* For wrapperless hydration.
* See https://gist.github.com/developit/f4c67a2ede71dc2fab7f357f39cff28c
*
* @param parent The parent element where the nodes will be replaced.
* @param replaceNode The node or array of nodes to replace in the parent element.
* @return The created root fragment.
*/
export const createRootFragment = (
parent: Element,
replaceNode: Node | Node[]
) => {
replaceNode = [].concat( replaceNode );
const s = replaceNode[ replaceNode.length - 1 ].nextSibling;
function insert( c, r ) {
parent.insertBefore( c, r || s );
const sibling = replaceNode[ replaceNode.length - 1 ].nextSibling;
function insert( child: any, root: any ) {
parent.insertBefore( child, root || sibling );
}
return ( parent.__k = {
return ( ( parent as any ).__k = {
nodeType: 1,
parentNode: parent,
firstChild: replaceNode[ 0 ],
childNodes: replaceNode,
insertBefore: insert,
appendChild: insert,
removeChild( c ) {
removeChild( c: Node ) {
parent.removeChild( c );
},
} );
Expand Down
14 changes: 0 additions & 14 deletions packages/interactivity/src/utils/kebab-to-camelcase.js

This file was deleted.

14 changes: 14 additions & 0 deletions packages/interactivity/src/utils/kebab-to-camelcase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Transforms a kebab-case string to camelCase.
*
* @param str The kebab-case string to transform to camelCase.
* @return The transformed camelCase string.
*/
export function kebabToCamelCase( str: string ): string {
return str
.replace( /^-+|-+$/g, '' )
.toLowerCase()
.replace( /-([a-z])/g, function ( _match, group1: string ) {
return group1.toUpperCase();
} );
}
Loading