Skip to content

Commit ccb5117

Browse files
Intercept default meta values within REST Requests (#72)
* Intercept default meta values within REST Requests * Preserve default values from the template * Clear placeholder meta values to allow editing the bound blocks * Add change placeholder todo * Remove leftover code * Explain why the new script is needed * Clarify placeholder todo item * Add placeholder text * Add placeholder support for core/button * Use an actual placeholder instead of setting the content * Add removal TODO * Simplify dispatcher use * Change default placeholder when defining the data model --------- Co-authored-by: Candy Tsai <[email protected]>
1 parent 63b04b6 commit ccb5117

5 files changed

+290
-20
lines changed

includes/manager/class-content-model-loader.php

+24
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ private function __construct() {
5252
$this->maybe_enqueue_the_cpt_settings_panel();
5353
$this->maybe_enqueue_the_fields_ui();
5454
$this->maybe_enqueue_content_model_length_restrictor();
55+
$this->maybe_enqueue_the_defaut_value_placeholder();
5556

5657
add_action( 'save_post', array( $this, 'map_template_to_bindings_api_signature' ), 99, 2 );
5758

@@ -300,6 +301,29 @@ function () {
300301
);
301302
}
302303

304+
private function maybe_enqueue_the_defaut_value_placeholder() {
305+
add_action(
306+
'enqueue_block_editor_assets',
307+
function () {
308+
global $post;
309+
310+
if ( ! $post || Content_Model_Manager::POST_TYPE_NAME !== $post->post_type ) {
311+
return;
312+
}
313+
314+
$content_model_length_restrictor_js = include CONTENT_MODEL_PLUGIN_PATH . '/includes/manager/dist/default-value-placeholder-changer.asset.php';
315+
316+
wp_enqueue_script(
317+
'content-model/default-value-placeholder-changer',
318+
CONTENT_MODEL_PLUGIN_URL . '/includes/manager/dist/default-value-placeholder-changer.js',
319+
$content_model_length_restrictor_js['dependencies'],
320+
$content_model_length_restrictor_js['version'],
321+
true
322+
);
323+
}
324+
);
325+
}
326+
303327
/**
304328
* Maps our bindings to the bindings API signature.
305329
*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { __, sprintf } from '@wordpress/i18n';
2+
import { useEffect } from '@wordpress/element';
3+
import { useSelect, useDispatch } from '@wordpress/data';
4+
import { store as blockEditorStore } from '@wordpress/block-editor';
5+
import { registerPlugin } from '@wordpress/plugins';
6+
7+
const ContentModelDefaultValuePlaceholderChanger = () => {
8+
const boundBlocks = useSelect( ( select ) => {
9+
const blocks = select( blockEditorStore ).getBlocks();
10+
const map = {};
11+
12+
const processBlock = ( block ) => {
13+
const name = block.attributes?.metadata?.name;
14+
15+
if ( name ) {
16+
map[ block.clientId ] = block.attributes.metadata.name;
17+
}
18+
19+
// Process inner blocks if they exist, like core/button is inside core/buttons.
20+
if ( block.innerBlocks && block.innerBlocks.length > 0 ) {
21+
block.innerBlocks.forEach( processBlock );
22+
}
23+
};
24+
25+
blocks.forEach( processBlock );
26+
27+
return map;
28+
}, [] );
29+
30+
const { updateBlockAttributes } = useDispatch( blockEditorStore );
31+
32+
useEffect( () => {
33+
Object.entries( boundBlocks ).forEach( ( [ blockId, blockName ] ) => {
34+
updateBlockAttributes( blockId, {
35+
placeholder: sprintf(
36+
// translators: %s is the block name.
37+
__( 'Enter the default value for %s' ),
38+
blockName
39+
),
40+
} );
41+
} );
42+
}, [ boundBlocks, updateBlockAttributes ] );
43+
};
44+
45+
registerPlugin( 'content-model-default-value-placeholder-changer', {
46+
render: ContentModelDefaultValuePlaceholderChanger,
47+
} );

includes/runtime/class-content-model-block.php

+4-14
Original file line numberDiff line numberDiff line change
@@ -295,9 +295,11 @@ public function get_attribute_type( $attribute_name ) {
295295
}
296296

297297
/**
298-
* Get the default value for an attribute.
298+
* Get the default value from an attribute in the template.
299299
*
300-
* @param string $attribute_name The name of the attribute.
300+
* @param string $attribute_name The attribute name.
301+
*
302+
* @return mixed The default value.
301303
*/
302304
public function get_default_value_for_attribute( $attribute_name ) {
303305
$block_attribute = $this->raw_block['attrs'][ $attribute_name ] ?? null;
@@ -321,17 +323,5 @@ public function get_default_value_for_attribute( $attribute_name ) {
321323
return $attribute_value;
322324
}
323325
}
324-
325-
if ( isset( $attribute_metadata['default'] ) ) {
326-
return $attribute_metadata['default'];
327-
}
328-
329-
$attribute_type = $this->get_attribute_type( $attribute_name );
330-
331-
if ( 'string' !== $attribute_type ) {
332-
return 0;
333-
}
334-
335-
return $attribute_name;
336326
}
337327
}

includes/runtime/class-content-model.php

+136-6
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
* Manages the registered Content Models.
1212
*/
1313
final class Content_Model {
14+
public const FALLBACK_VALUE_PLACEHOLDER = '__CREATE_CONTENT_MODEL__FALLBACK_VALUE__';
15+
1416
/**
1517
* The slug of the content model.
1618
*
@@ -39,6 +41,13 @@ final class Content_Model {
3941
*/
4042
public $blocks = array();
4143

44+
/**
45+
* A reverse map of meta keys, with the values being
46+
* the bound block and which attribute the meta key is bound to.
47+
*
48+
* @var array
49+
*/
50+
private $bound_meta_keys = array();
4251

4352
/**
4453
* Holds the fields of the content model.
@@ -75,9 +84,13 @@ public function __construct( WP_Post $content_model_post ) {
7584
$this->maybe_enqueue_the_fields_ui();
7685
$this->maybe_enqueue_bound_group_extractor();
7786
$this->maybe_enqueue_content_locking();
87+
$this->maybe_enqueue_fallback_value_clearer();
7888

7989
add_filter( 'block_categories_all', array( $this, 'register_block_category' ) );
8090

91+
add_filter( 'rest_request_before_callbacks', array( $this, 'remove_default_meta_keys_on_save' ), 10, 3 );
92+
add_filter( 'rest_post_dispatch', array( $this, 'fill_empty_meta_keys_with_default_values' ), 10, 3 );
93+
8194
add_action( 'rest_after_insert_' . $this->slug, array( $this, 'extract_post_content_from_blocks' ), 99, 1 );
8295

8396
/**
@@ -204,15 +217,27 @@ private function register_meta_fields() {
204217
continue;
205218
}
206219

220+
$this->bound_meta_keys[ $field ] = (object) array(
221+
'block' => $block,
222+
'attribute_name' => $attribute_name,
223+
);
224+
225+
$args = array(
226+
'show_in_rest' => true,
227+
'single' => true,
228+
'type' => $block->get_attribute_type( $attribute_name ),
229+
);
230+
231+
$default_value = $block->get_default_value_for_attribute( $attribute_name );
232+
233+
if ( ! empty( $default_value ) ) {
234+
$args['default'] = $default_value;
235+
}
236+
207237
register_post_meta(
208238
$this->slug,
209239
$field,
210-
array(
211-
'show_in_rest' => true,
212-
'single' => true,
213-
'type' => $block->get_attribute_type( $attribute_name ),
214-
'default' => $block->get_default_value_for_attribute( $attribute_name ),
215-
)
240+
$args
216241
);
217242
}
218243
}
@@ -307,6 +332,70 @@ public function extract_post_content_from_blocks( $post ) {
307332
);
308333
}
309334

335+
/**
336+
* Intercepts the saving request and removes the meta keys with default values.
337+
*
338+
* TODO Remove when Gutneberg 19.2 gets released.
339+
*
340+
* @param WP_HTTP_Response|null $response The response.
341+
* @param WP_REST_Server $server Route handler used for the request.
342+
* @param WP_REST_Request $request The request.
343+
*
344+
* @return WP_REST_Response The response.
345+
*/
346+
public function remove_default_meta_keys_on_save( $response, $server, $request ) {
347+
$is_upserting = in_array( $request->get_method(), array( 'POST', 'PUT' ), true );
348+
$is_touching_post_type = str_starts_with( $request->get_route(), '/wp/v2/' . $this->slug );
349+
350+
if ( $is_upserting && $is_touching_post_type ) {
351+
$meta = $request->get_param( 'meta' ) ?? array();
352+
353+
foreach ( $meta as $key => $value ) {
354+
if ( '' === $value ) {
355+
unset( $meta[ $key ] );
356+
delete_post_meta( $request->get_param( 'id' ), $key );
357+
}
358+
}
359+
360+
$request->set_param( 'meta', $meta );
361+
}
362+
363+
return $response;
364+
}
365+
366+
/**
367+
* Intercepts the response and fills the empty meta keys with default values.
368+
*
369+
* TODO Remove when Gutneberg 19.2 gets released.
370+
*
371+
* @param WP_HTTP_Response $result The response.
372+
* @param WP_REST_Server $server The server.
373+
* @param WP_REST_Request $request The request.
374+
*
375+
* @return WP_REST_Response The response.
376+
*/
377+
public function fill_empty_meta_keys_with_default_values( $result, $server, $request ) {
378+
$is_allowed_method = in_array( $request->get_method(), array( 'GET', 'POST', 'PUT' ), true );
379+
$is_touching_post_type = str_starts_with( $request->get_route(), '/wp/v2/' . $this->slug );
380+
381+
if ( $is_allowed_method && $is_touching_post_type ) {
382+
$data = $result->get_data();
383+
384+
$data['meta'] ??= array();
385+
386+
foreach ( $data['meta'] as $key => $value ) {
387+
$bound_meta_key = $this->bound_meta_keys[ $key ] ?? null;
388+
389+
if ( empty( $value ) && $bound_meta_key ) {
390+
$data['meta'][ $key ] = self::FALLBACK_VALUE_PLACEHOLDER;
391+
}
392+
}
393+
394+
$result->set_data( $data );
395+
}
396+
397+
return $result;
398+
}
310399
/**
311400
* Extracts the post content from the blocks.
312401
*
@@ -495,4 +584,45 @@ function () {
495584
}
496585
);
497586
}
587+
588+
/**
589+
* Conditionally enqueues the fallback value clearer, allowing the block to become editable.
590+
*
591+
* Checks if the current post is of the correct type before enqueueing the script.
592+
*
593+
* @return void
594+
*/
595+
private function maybe_enqueue_fallback_value_clearer() {
596+
add_action(
597+
'enqueue_block_editor_assets',
598+
function () {
599+
global $post;
600+
601+
if ( ! $post || $this->slug !== $post->post_type ) {
602+
return;
603+
}
604+
605+
$asset_file = include CONTENT_MODEL_PLUGIN_PATH . 'includes/runtime/dist/fallback-value-clearer.asset.php';
606+
607+
wp_register_script(
608+
'data-types/fallback-value-clearer',
609+
CONTENT_MODEL_PLUGIN_URL . '/includes/runtime/dist/fallback-value-clearer.js',
610+
$asset_file['dependencies'],
611+
$asset_file['version'],
612+
true
613+
);
614+
615+
wp_localize_script(
616+
'data-types/fallback-value-clearer',
617+
'contentModelFields',
618+
array(
619+
'postType' => $this->slug,
620+
'FALLBACK_VALUE_PLACEHOLDER' => self::FALLBACK_VALUE_PLACEHOLDER,
621+
)
622+
);
623+
624+
wp_enqueue_script( 'data-types/fallback-value-clearer' );
625+
}
626+
);
627+
}
498628
}
+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { useLayoutEffect } from '@wordpress/element';
2+
import { registerPlugin } from '@wordpress/plugins';
3+
import { useEntityProp } from '@wordpress/core-data';
4+
import { store as blockEditorStore } from '@wordpress/block-editor';
5+
import { useSelect, useDispatch } from '@wordpress/data';
6+
7+
/**
8+
* This allows the user to edit values that are bound to an attribute.
9+
* There is a bug in the Bindings API preventing this from working,
10+
* so here's our workaround.
11+
*
12+
* TODO Remove when Gutneberg 19.2 gets released.
13+
*
14+
* See https://github.com/Automattic/create-content-model/issues/63 for the problem.
15+
*/
16+
const CreateContentModelFallbackValueClearer = () => {
17+
const [ meta, setMeta ] = useEntityProp(
18+
'postType',
19+
window.contentModelFields.postType,
20+
'meta'
21+
);
22+
23+
const { updateBlockAttributes } = useDispatch( blockEditorStore );
24+
25+
const blockToMetaMap = useSelect( ( select ) => {
26+
const blocks = select( blockEditorStore ).getBlocks();
27+
const map = {};
28+
29+
const processBlock = ( block ) => {
30+
const bindings = block.attributes?.metadata?.bindings || {};
31+
Object.entries( bindings ).forEach( ( [ , binding ] ) => {
32+
if ( binding.source === 'core/post-meta' ) {
33+
if ( ! map[ block.clientId ] ) {
34+
map[ block.clientId ] = [];
35+
}
36+
map[ block.clientId ].push( {
37+
metaKey: binding.args.key,
38+
blockName: block.attributes.metadata.name,
39+
} );
40+
}
41+
} );
42+
43+
// Process inner blocks if they exist, like core/button is inside core/buttons.
44+
if ( block.innerBlocks && block.innerBlocks.length > 0 ) {
45+
block.innerBlocks.forEach( processBlock );
46+
}
47+
};
48+
49+
blocks.forEach( processBlock );
50+
51+
return map;
52+
}, [] );
53+
54+
useLayoutEffect( () => {
55+
Object.entries( blockToMetaMap ).forEach(
56+
( [ blockId, metaInfos ] ) => {
57+
metaInfos.forEach( ( { metaKey, blockName } ) => {
58+
const value = meta[ metaKey ];
59+
if (
60+
value ===
61+
window.contentModelFields.FALLBACK_VALUE_PLACEHOLDER
62+
) {
63+
setMeta( { [ metaKey ]: '' } );
64+
65+
updateBlockAttributes( blockId, {
66+
placeholder: `Enter a value for ${ blockName }`,
67+
} );
68+
}
69+
} );
70+
}
71+
);
72+
}, [ meta, setMeta, blockToMetaMap, updateBlockAttributes ] );
73+
74+
return null;
75+
};
76+
77+
registerPlugin( 'create-content-model-fallback-value-clearer', {
78+
render: CreateContentModelFallbackValueClearer,
79+
} );

0 commit comments

Comments
 (0)