Skip to content
Closed
64 changes: 58 additions & 6 deletions src/wp-includes/interactivity-api/class-wp-interactivity-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,18 @@ final class WP_Interactivity_API {
*/
private $config_data = array();

/**
* Keeps track of all derived state closures accessed during server-side rendering.
*
* This data is serialized and sent to the client as part of the interactivity
* data, and is handled later in the client to support derived state props that
* are lazily hydrated.
*
* @since 6.9.0
* @var array
*/
private $derived_state_closures = array();

/**
* Flag that indicates whether the `data-wp-router-region` directive has
* been found in the HTML and processed.
Expand Down Expand Up @@ -236,12 +248,17 @@ public function filter_script_module_interactivity_router_data( array $data ): a
* interactivity stores and the configuration will be available using a `getConfig` utility.
*
* @since 6.7.0
* @since 6.9.0 Serializes derived state props accessed during directive processing.
*
* @param array $data Data to filter.
* @return array Data for the Interactivity API script module.
*/
public function filter_script_module_interactivity_data( array $data ): array {
if ( empty( $this->state_data ) && empty( $this->config_data ) ) {
if (
empty( $this->state_data ) &&
empty( $this->config_data ) &&
empty( $this->derived_state_closures )
) {
return $data;
}

Expand All @@ -265,6 +282,16 @@ public function filter_script_module_interactivity_data( array $data ): array {
$data['state'] = $state;
}

$derived_props = array();
foreach ( $this->derived_state_closures as $key => $value ) {
if ( ! empty( $value ) ) {
$derived_props[ $key ] = $value;
}
}
if ( ! empty( $derived_props ) ) {
$data['derivedStateClosures'] = $derived_props;
}

return $data;
}

Expand Down Expand Up @@ -598,7 +625,7 @@ private function evaluate( $directive_value ) {
// Extracts the value from the store using the reference path.
$path_segments = explode( '.', $path );
$current = $store;
foreach ( $path_segments as $path_segment ) {
foreach ( $path_segments as $index => $path_segment ) {
/*
* Special case for numeric arrays and strings. Add length
* property mimicking JavaScript behavior.
Expand Down Expand Up @@ -647,6 +674,20 @@ private function evaluate( $directive_value ) {
array_push( $this->namespace_stack, $ns );
try {
$current = $current();

/*
* Tracks derived state properties that are accessed during
* rendering.
*
* @since 6.9.0
*/
$this->derived_state_closures[ $ns ] = $this->derived_state_closures[ $ns ] ?? array();

// Builds path for the current property and add it to tracking if not already present.
$current_path = implode( '.', array_slice( $path_segments, 0, $index + 1 ) );
if ( ! in_array( $current_path, $this->derived_state_closures[ $ns ], true ) ) {
$this->derived_state_closures[ $ns ][] = $current_path;
}
} catch ( Throwable $e ) {
_doing_it_wrong(
__METHOD__,
Expand Down Expand Up @@ -1160,6 +1201,7 @@ private function data_wp_router_region_processor( WP_Interactivity_API_Directive
* `template` tag.
*
* @since 6.5.0
* @since 6.9.0 Include the list path in the rendered `data-wp-each-child` directives.
*
* @param WP_Interactivity_API_Directives_Processor $p The directives processor instance.
* @param string $mode Whether the processing is entering or exiting the tag.
Expand Down Expand Up @@ -1205,8 +1247,8 @@ private function data_wp_each_processor( WP_Interactivity_API_Directives_Process
}

// Extracts the namespace from the directive attribute value.
$namespace_value = end( $this->namespace_stack );
list( $namespace_value ) = is_string( $attribute_value ) && ! empty( $attribute_value )
$namespace_value = end( $this->namespace_stack );
list( $namespace_value, $path ) = is_string( $attribute_value ) && ! empty( $attribute_value )
? $this->extract_directive_value( $attribute_value, $namespace_value )
: array( $namespace_value, null );

Expand All @@ -1228,10 +1270,20 @@ private function data_wp_each_processor( WP_Interactivity_API_Directives_Process
return;
}

// Adds the `data-wp-each-child` to each top-level tag.
/*
* Adds the `data-wp-each-child` directive to each top-level tag
* rendered by this `data-wp-each` directive. The value is the
* `data-wp-each` directive's namespace and path.
*
* Nested `data-wp-each` directives could render
* `data-wp-each-child` elements at the top level as well, and
* they should be overwritten.
*
* @since 6.9.0
*/
$i = new WP_Interactivity_API_Directives_Processor( $processed_item );
while ( $i->next_tag() ) {
$i->set_attribute( 'data-wp-each-child', true );
$i->set_attribute( 'data-wp-each-child', $namespace_value . '::' . $path );
$i->next_balanced_tag_closer_tag();
}
$processed_content .= $i->get_updated_html();
Expand Down
Loading
Loading