Skip to content

Commit

Permalink
Interactivity API: Refactor internal proxy and signals system (#62734)
Browse files Browse the repository at this point in the history
* WIP

* More WIP

* Restore previous packages

* Fix current tests

* Add computation tests

* Fix getter call

* Fix for..in on new properties

* Refactor code a little

* Add tests for getters with scope

* Move namespaces to properties

* Fix a problem with object references

* Add store handlers

* Add signals-core dependency

* Rename Property to PropSignal

* Move withScope inside PropSignal logic

* Create proxies folder

* Attempt to make the context work

* Reorganize code

* Make peek() return only the value inside signalValue

* A bit of refactoring

* Return the computed value with `peek()`

* Rename DEFAULT_SCOPE to NO_SCOPE

* Remove deepsignal

* Handle functions inside state

* Fix withScope in this PR

* Fix context proxification

* Move store root logic to store proxy handlers

* Move store initialization inside init

* Add TODO comment inside `peek()`

* Fix store root assignments

* Fix lint error

* Enable skipped test for non-initialized getters

* Rename get(State|Store)Proxy to proxify(State|Store)

* Make `getContext` throw when there is no scope

* Fix `proxifyState` types

* Rename some variables in tests

* Add test for object reference keeping

* Rename test suite for state proxy

* Add tests for getters and functions with scope

* Add tests for prop subscription inside functions

* Allow functions to use `this`

* Minor fixes

* Add tests to store proxies

* Throw an error when an object cannot be proxified

* Change peek implementation

* Add tests for peek and unsupported structures

* Test peeking getters that access scope or other namespaces

* Ignore well-known symbols

* Minor comment format fix

* Move namespace arg to first position in proxify functions

* chore: Update jest.config.js to stop ignoring deepsignal because we removed it as dependency

* Add comments to proxy registry

* Remove namespace from PropSignal

* Remove unused `peekValueSignal` method

* Simplify PropSignal methods

* Add TSDocs to PropSignal

* Disable unused vars lint rule in state-proxy tests

* Remove descriptor alias

* Expand `getProxyNs` docs

* Add more comments in `state`

* Refactor state functions and add tsdocs

* Fix some grammar issues

* Fix default namespace for setters

* Replace `isNotRoot` with `isRoot`

* Update comments in store.ts

* Move `isPlainObject` to utils

* Remove remaining deepSignal references

* Delete unnecessary @ts-ignore-next-line

* Use `isPlainObject` from utils inside store

* Move scopes and namespaces to separate files

* Call `init` outside `DOMContentLoaded`

* Replace `signals-core` imports with `signals`

* Rename `proxiedStore` to `proxifiedStore`

Co-authored-by: Luis Herranz <[email protected]>

* Remove unnecessary `peek()` call

* Throw more descriptive errors from getContext and getElement

* Use more descriptive name for proxy functions

* Use destructured `get` inside `setGetter` call

* Add comment to explain `objToIterable` signals subscription

Co-authored-by: Luis Herranz <[email protected]>

* Change line comment to block comment

* Replace `?` with `!` operator

* Wrap Interactivity API tests with appropriate `describe`

* Remove unnecessary `setNamespace` calls in tests

* Remove duplicated test

* Fix typo

* Add extra tests for getter modification

* Test the right namespace is used inside getters

* Fix test name

* Check if length's PropSignal exists before updating its value

* Add deepMerge tests and prevent server overwritting

* Make sure context inheritance is shallow and server props don't overwrite

* Refactor wp-each to use new proxifyContext structure

---------

Co-authored-by: DAreRodz <[email protected]>
Co-authored-by: michalczaplinski <[email protected]>
Co-authored-by: luisherranz <[email protected]>
Co-authored-by: gziolo <[email protected]>
Co-authored-by: otakupahp <[email protected]>
Co-authored-by: sirreal <[email protected]>
  • Loading branch information
7 people committed Aug 7, 2024
1 parent c384bb6 commit bd9ba41
Show file tree
Hide file tree
Showing 26 changed files with 2,571 additions and 434 deletions.
32 changes: 0 additions & 32 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -220,14 +220,14 @@
<div
data-wp-interactive="directive-each"
data-wp-router-region="navigation-updated list"
data-wp-context='{ "list": [ "beta", "gamma", "delta" ] }'
data-wp-context='{ "b": 2, "c": 3, "d": 4 }'
data-testid="navigation-updated list"
>
<button
data-testid="navigate"
data-wp-on--click="actions.navigate"
>Navigate</button>
<template data-wp-each="context.list">
<template data-wp-each="state.list">
<p data-wp-text="context.item" data-testid="item"></p>
</template>
<p data-testid="item" data-wp-each-child>beta</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,14 +169,14 @@ const html = `
<div
data-wp-interactive="directive-each"
data-wp-router-region="navigation-updated list"
data-wp-context='{ "list": [ "alpha", "beta", "gamma", "delta" ] }'
data-wp-context='{ "a": 1, "b": 2, "c": 3, "d": 4 }'
data-testid="navigation-updated list"
>
<button
data-testid="navigate"
data-wp-on--click="actions.navigate"
>Navigate</button>
<template data-wp-each="context.list">
<template data-wp-each="state.list">
<p data-wp-text="context.item" data-testid="item"></p>
</template>
<p data-testid="item" data-wp-each-child>alpha</p>
Expand All @@ -187,6 +187,12 @@ const html = `
`;

store( 'directive-each', {
state: {
get list() {
const ctx = getContext();
return Object.keys( ctx ).sort();
},
},
actions: {
*navigate() {
const { actions } = yield import(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
privateApis,
} from '@wordpress/interactivity';

const { directive, deepSignal, h } = privateApis(
const { directive, proxifyState, h } = privateApis(
'I acknowledge that using private APIs means my theme or plugin will inevitably break in the next version of WordPress.'
);

Expand Down Expand Up @@ -41,12 +41,12 @@ directive(
'test-context',
( { context: { Provider }, props: { children } } ) => {
executionProof( 'context' );
const value = deepSignal( {
[ namespace ]: {
const value = {
[ namespace ]: proxifyState( namespace, {
attribute: 'from context',
text: 'from context',
},
} );
} ),
};
return h( Provider, { value }, children );
},
{ priority: 8 }
Expand Down
8 changes: 4 additions & 4 deletions packages/interactivity-router/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ const {
initialVdom,
toVdom,
render,
parseInitialData,
populateInitialData,
parseServerData,
populateServerData,
batch,
} = privateApis(
'I acknowledge that using private APIs means my theme or plugin will inevitably break in the next version of WordPress.'
Expand Down Expand Up @@ -103,7 +103,7 @@ const regionsToVdom: RegionsToVdom = async ( dom, { vdom } = {} ) => {
} );
}
const title = dom.querySelector( 'title' )?.innerText;
const initialData = parseInitialData( dom );
const initialData = parseServerData( dom );
return { regions, head, title, initialData };
};

Expand All @@ -119,7 +119,7 @@ const renderRegions = ( page: Page ) => {
}
}
if ( navigationMode === 'regionBased' ) {
populateInitialData( page.initialData );
populateServerData( page.initialData );
const attrName = `data-${ directivePrefix }-router-region`;
document
.querySelectorAll( `[${ attrName }]` )
Expand Down
1 change: 0 additions & 1 deletion packages/interactivity/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
"types": "build-types",
"dependencies": {
"@preact/signals": "^1.2.2",
"deepsignal": "^1.4.0",
"preact": "^10.19.3"
},
"publishConfig": {
Expand Down
75 changes: 45 additions & 30 deletions packages/interactivity/src/directives.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,24 @@
*/
import { h as createElement, type RefObject } from 'preact';
import { useContext, useMemo, useRef } from 'preact/hooks';
import { deepSignal, peek, type DeepSignal } from 'deepsignal';
/**
* Internal dependencies
*/
import { proxifyState, peek } from './proxies';

/**
* Internal dependencies
*/
import { useWatch, useInit, kebabToCamelCase, warn, splitTask } from './utils';
import type { DirectiveEntry } from './hooks';
import { directive, getScope, getEvaluate } from './hooks';
import {
useWatch,
useInit,
kebabToCamelCase,
warn,
splitTask,
isPlainObject,
} from './utils';
import { directive, getEvaluate, type DirectiveEntry } from './hooks';
import { getScope } from './scopes';

// Assigned objects should be ignored during proxification.
const contextAssignedObjects = new WeakMap();
Expand All @@ -23,9 +33,6 @@ const contextObjectToProxy = new WeakMap();
const contextProxyToObject = new WeakMap();
const contextObjectToFallback = new WeakMap();

const isPlainObject = ( item: unknown ): boolean =>
Boolean( item && typeof item === 'object' && item.constructor === Object );

const descriptor = Reflect.getOwnPropertyDescriptor;

/**
Expand All @@ -47,7 +54,7 @@ const proxifyContext = ( current: object, inherited: object = {} ): object => {
contextObjectToFallback.set( current, inherited );
if ( ! contextObjectToProxy.has( current ) ) {
const proxy = new Proxy( current, {
get: ( target: DeepSignal< any >, k ) => {
get: ( target: object, k: string ) => {
const fallback = contextObjectToFallback.get( current );
// Always subscribe to prop changes in the current context.
const currentProp = target[ k ];
Expand All @@ -61,9 +68,9 @@ const proxifyContext = ( current: object, inherited: object = {} ): object => {
if (
k in target &&
! contextAssignedObjects.get( target )?.has( k ) &&
isPlainObject( peek( target, k ) )
isPlainObject( currentProp )
) {
return proxifyContext( currentProp, fallback[ k ] );
return proxifyContext( currentProp );
}

// Return the stored proxy for `currentProp` when it exists.
Expand Down Expand Up @@ -125,22 +132,19 @@ const proxifyContext = ( current: object, inherited: object = {} ): object => {
};

/**
* Recursively update values within a deepSignal object.
* Recursively update values within a context object.
*
* @param target A deepSignal instance.
* @param target A context instance.
* @param source Object with properties to update in `target`.
*/
const updateSignals = (
target: DeepSignal< any >,
source: DeepSignal< any >
) => {
const updateContext = ( target: any, source: any ) => {
for ( const k in source ) {
if (
isPlainObject( peek( target, k ) ) &&
isPlainObject( peek( source, k ) )
isPlainObject( source[ k ] )
) {
updateSignals( target[ `$${ k }` ].peek(), source[ k ] );
} else {
updateContext( peek( target, k ) as object, source[ k ] );
} else if ( ! ( k in target ) ) {
target[ k ] = source[ k ];
}
}
Expand Down Expand Up @@ -257,18 +261,21 @@ export default () => {
// data-wp-context
directive(
'context',
// @ts-ignore-next-line
( {
directives: { context },
props: { children },
context: inheritedContext,
} ) => {
const { Provider } = inheritedContext;
const inheritedValue = useContext( inheritedContext );
const currentValue = useRef( deepSignal( {} ) );
const defaultEntry = context.find(
( { suffix } ) => suffix === 'default'
);
const inheritedValue = useContext( inheritedContext );

const ns = defaultEntry!.namespace;
const currentValue = useRef( {
[ ns ]: proxifyState( ns, {} ),
} );

// No change should be made if `defaultEntry` does not exist.
const contextStack = useMemo( () => {
Expand All @@ -280,11 +287,16 @@ export default () => {
`The value of data-wp-context in "${ namespace }" store must be a valid stringified JSON object.`
);
}
updateSignals( currentValue.current, {
[ namespace ]: deepClone( value ),
} );
updateContext(
currentValue.current[ namespace ],
deepClone( value ) as object
);
currentValue.current[ namespace ] = proxifyContext(
currentValue.current[ namespace ],
inheritedValue[ namespace ]
);
}
return proxifyContext( currentValue.current, inheritedValue );
return currentValue.current;
}, [ defaultEntry, inheritedValue ] );

return createElement( Provider, { value: contextStack }, children );
Expand Down Expand Up @@ -677,11 +689,14 @@ export default () => {
return list.map( ( item ) => {
const itemProp =
suffix === 'default' ? 'item' : kebabToCamelCase( suffix );
const itemContext = deepSignal( { [ namespace ]: {} } );
const mergedContext = proxifyContext(
itemContext,
inheritedValue
const itemContext = proxifyContext(
proxifyState( namespace, {} ),
inheritedValue[ namespace ]
);
const mergedContext = {
...inheritedValue,
[ namespace ]: itemContext,
};

// Set the item after proxifying the context.
mergedContext[ namespace ][ itemProp ] = item;
Expand Down
Loading

0 comments on commit bd9ba41

Please sign in to comment.