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

Block API: Add Block Context support #21467

Merged
merged 54 commits into from
Apr 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
ef69571
Framework: Add render_block initial implementation
aduth Apr 6, 2020
ecb311f
Framework: Assign to and consume from block context
aduth Apr 6, 2020
d325f90
Framework: Assign default post context
aduth Apr 6, 2020
bdd8d28
Block Editor: Implement BlockContext component
aduth Apr 6, 2020
c2d9b26
Block Editor: Provide and consume block context
aduth Apr 6, 2020
efdf341
Editor: Assign default post context
aduth Apr 6, 2020
49de552
Block Library: Post Title: Reimplement using block context
aduth Apr 6, 2020
909e637
Framework: Rename block context global to clarify internal
aduth Apr 6, 2020
00a0be5
Documentation: Add Block Context documentation
aduth Apr 7, 2020
13a843c
Docs: Update Block Context text to refer to Context.Provider
aduth Apr 8, 2020
ccb86f5
Block Library: Post Title: Include dependencies for useSelect hook
aduth Apr 10, 2020
0767f60
Block Editor: Use optional chaining for providesContext property access
aduth Apr 10, 2020
c4038c0
Block Editor: Docs: Clarify context consumed in editing interface
aduth Apr 10, 2020
5c8d5ee
Docs: Block Context: Clarify language of providing block context
aduth Apr 10, 2020
8c527aa
Framework: Remove redundant block context assignment
aduth Apr 10, 2020
7a52d0a
Framework: Defer to highest priority block render filtering
aduth Apr 10, 2020
3203233
Block Editor: Memoize block context provider value
aduth Apr 10, 2020
4fa6a5c
Block Editor: Cache block context from InnerBlocks
aduth Apr 10, 2020
c406d4c
Block Library: Post Title: Ensure post entity is fetched
aduth Apr 10, 2020
81c83c4
Docs: Block Context: Grammar and readability
aduth Apr 10, 2020
10125d3
Block API: Add automatic block context namespacing
aduth Apr 10, 2020
d2b5624
Revert "Block API: Add automatic block context namespacing"
aduth Apr 10, 2020
784a057
Block API: Change providesContext from array to object format
aduth Apr 10, 2020
a2dafcf
Framework: Disable PHPCS rule for snake case property
aduth Apr 10, 2020
4f25668
Core Data: Add resolvers for getRawEntityRecord, getEditedEntityRecord
aduth Apr 10, 2020
e773ef6
Block Library: Post Title: Remove redundant entity resolver call
aduth Apr 10, 2020
6b33df2
Block Editor: Reference postType, postId dependency variables
aduth Apr 13, 2020
5005983
Block Editor: Export BlockContextProvider component for native
aduth Apr 13, 2020
c36fd65
Editor: EditorProvider: Cache default block context
aduth Apr 13, 2020
2ebe9f3
Framework: Block Context: Restore block global after inner render
aduth Apr 13, 2020
0594ce7
Framework: Block Context: Rename function to reflect implementation
aduth Apr 13, 2020
fc4c513
Block Editor: InnerBlocks: Fix block context provide attribute picking
aduth Apr 13, 2020
d7eb57c
Docs: Block Context: Document namespace recommendation
aduth Apr 13, 2020
fe97dd8
Framework: Block Context: Prepare attributes for context assignment
aduth Apr 13, 2020
2d06181
E2E Tests: Add Block Context tests
aduth Apr 13, 2020
54f183a
PHPUnit: Add block context PHP unit tests
aduth Apr 13, 2020
293487c
Core Data: Add ifNotResolved internal utility for shared resolvers
aduth Apr 14, 2020
c54c862
Framework: Introduce WP_Block class for block rendering
aduth Apr 14, 2020
0cafbe0
Framework: Assign prepared attributes using original reference
aduth Apr 14, 2020
c98a406
Framework: Accept context, registry arguments to WP_Block
aduth Apr 15, 2020
b0636f8
Docs: Update block context docs for render_callback argument
aduth Apr 16, 2020
728d35d
Docs: Update render_callback references to prefer $block argument
aduth Apr 16, 2020
d36ad8c
Block Editor: Add BlockContext component to type-checking
aduth Apr 16, 2020
207a770
Core Data: Improve ifNotResolved typeability
aduth Apr 16, 2020
06c169a
Revert "Block Editor: Add BlockContext component to type-checking"
aduth Apr 16, 2020
ad677ec
Framework: Block Context: Use verbose array syntax
aduth Apr 17, 2020
262b2a4
Framework: Block Context: Rename WP_Block constructor agument availab…
aduth Apr 17, 2020
f11c515
Framework: Block Context: Support null context overrides
aduth Apr 17, 2020
8665824
E2E Tests: Block Context: Clarify waitForSelector comment expectation
aduth Apr 17, 2020
f8beb9e
Framework: Block Context: Update ArrayAccess to behave exactly as array
aduth Apr 17, 2020
b908cea
Block Library: Post Title: Update render callback to use block argument
aduth Apr 17, 2020
22c699f
Block Library: Post Title: Add missing PHPDoc
aduth Apr 17, 2020
cb4a324
Block Editor: BlockEdit: Provide context to light block wrapper
aduth Apr 17, 2020
2aa56ca
Revert "Block Editor: Use optional chaining for providesContext prope…
aduth Apr 17, 2020
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
76 changes: 76 additions & 0 deletions docs/designers-developers/developers/block-api/block-context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Block Context
aduth marked this conversation as resolved.
Show resolved Hide resolved

Block context is a feature which enables ancestor blocks to provide values which can be consumed by descendent blocks within its own hierarchy. Those descendent blocks can inherit these values without resorting to hard-coded values and without an explicit awareness of the block which provides those values.

This is especially useful in full-site editing where, for example, the contents of a block may depend on the context of the post in which it is displayed. A blogroll template may show excerpts of many different posts. Using block context, there can still be one single "Post Excerpt" block which displays the contents of the post based on an inherited post ID.

If you are familiar with [React Context](https://reactjs.org/docs/context.html), block context adopts many of the same ideas. In fact, the client-side block editor implementation of block context is a very simple application of React Context. Block context is also supported in server-side `render_callback` implementations, demonstrated in the examples below.

## Defining Block Context

Block context is defined in the registered settings of a block. A block can provide a context value, or consume a value it seeks to inherit.

### Providing Block Context

A block can provide a context value by assigning a `providesContext` property in its registered settings. This is an object which maps a context name to one of the block's own attribute. The value corresponding to that attribute value is made available to descendent blocks and can be referenced by the same context name. Currently, block context only supports values derived from the block's own attributes. This could be enhanced in the future to support additional sources of context values.

`record/block.json`

```json
{
"name": "my-plugin/record",
"attributes": {
"recordId": {
"type": "number"
}
},
"providesContext": {
"my-plugin/recordId": "recordId"
}
aduth marked this conversation as resolved.
Show resolved Hide resolved
}
```

As seen in the above example, it is recommended that you include a namespace as part of the name of the context key so as to avoid potential conflicts with other plugins or default context values provided by WordPress. The context namespace should be specific to your plugin, and in most cases can be the same as used in the name of the block itself.

### Consuming Block Context

A block can inherit a context value from an ancestor provider by assigning a `context` property in its registered settings. This should be assigned as an array of the context names the block seeks to inherit.

`record-title/block.json`

```json
{
"name": "my-plugin/record-title",
"context": [ "my-plugin/recordId" ]
}
```

## Using Block Context

Once a block has defined the context it seeks to inherit, this can be accessed in the implementation of `edit` (JavaScript) and `render_callback` (PHP). It is provided as an object (JavaScript) or associative array (PHP) of the context values which have been defined for the block. Note that a context value will only be made available if the block explicitly defines a desire to inherit that value.

### JavaScript

`record-title/index.js`

```js
registerBlockType( 'my-plugin/record-title', {
edit( { context } ) {
return 'The current record ID is: ' + context[ 'my-plugin/recordId' ];
},
} );
```

### PHP

A block's context values are available from the `context` property of the `$block` argument passed to the `render_callback` function.

`record-title/index.php`

```php
register_block_type( 'my-plugin/record-title', [
'render_callback' => function( $block ) {
return 'The current record ID is: ' . $block->context['my-plugin/recordId'];
},
] );
```
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ Because it is a dynamic block it doesn't need to override the default `save` imp
* Plugin Name: Gutenberg examples dynamic
*/

function gutenberg_examples_dynamic_render_callback( $attributes, $content ) {
function gutenberg_examples_dynamic_render_callback( $block, $content ) {
$recent_posts = wp_get_recent_posts( array(
'numberposts' => 1,
'post_status' => 'publish',
Expand Down Expand Up @@ -141,7 +141,15 @@ There are a few things to notice:

* The `edit` function still shows a representation of the block in the editor's context (this could be very different from the rendered version, it's up to the block's author)
* The built-in `save` function just returns `null` because the rendering is performed server-side.
* The server-side rendering is a function taking the block attributes and the block inner content as arguments, and returning the markup (quite similar to shortcodes)
* The server-side rendering is a function taking the block and the block inner content as arguments, and returning the markup (quite similar to shortcodes)

Note that for convenience and for backward-compatibility, the first argument of a `render_callback` function can also be referenced as an associative array of the block's attributes:

```php
function gutenberg_examples_dynamic_render_callback( $block_attributes ) {
return 'The record ID is: ' . esc_html( $block_attributes['recordId'] );
}
```

## Live rendering in the block editor

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ You can also use the post meta data in other blocks. For this example the data i
In PHP, use the [register_block_type](https://developer.wordpress.org/reference/functions/register_block_type/) function to set a callback when the block is rendered to include the meta value.

```php
function myguten_render_paragraph( $attributes, $content ) {
function myguten_render_paragraph( $block, $content ) {
$value = get_post_meta( get_the_ID(), 'myguten_meta_block_field', true );
// check value is set before outputting
if ( $value ) {
Expand Down
6 changes: 6 additions & 0 deletions docs/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@
"markdown_source": "../docs/designers-developers/developers/block-api/block-attributes.md",
"parent": "block-api"
},
{
"title": "Block Context",
"slug": "block-context",
"markdown_source": "../docs/designers-developers/developers/block-api/block-context.md",
"parent": "block-api"
},
{
"title": "Deprecated Blocks",
"slug": "block-deprecation",
Expand Down
3 changes: 2 additions & 1 deletion docs/toc.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
{ "docs/designers-developers/developers/block-api/block-registration.md": [] },
{ "docs/designers-developers/developers/block-api/block-edit-save.md": [] },
{ "docs/designers-developers/developers/block-api/block-attributes.md": [] },
{"docs/designers-developers/developers/block-api/block-deprecation.md": [] },
{ "docs/designers-developers/developers/block-api/block-context.md": [] },
{ "docs/designers-developers/developers/block-api/block-deprecation.md": [] },
{ "docs/designers-developers/developers/block-api/block-templates.md": [] },
{ "docs/designers-developers/developers/block-api/block-patterns.md": [] },
{ "docs/designers-developers/developers/block-api/block-annotations.md": [] }
Expand Down
240 changes: 240 additions & 0 deletions lib/class-wp-block.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
<?php
/**
* Blocks API: WP_Block class
*
* @package Gutenberg
*/

/**
* Class representing a parsed instance of a block.
*/
class WP_Block implements ArrayAccess {

/**
* Name of block.
*
* @example "core/paragraph"
*
* @var string
*/
public $name;

/**
* Block type associated with the instance.
*
* @var WP_Block_Type
*/
public $block_type;

/**
* Block context values.
*
* @var array
*/
public $context = array();

/**
* All available context of the current hierarchy.
*
* @var array
* @access protected
*/
protected $available_context;

/**
* Block attribute values.
*
* @var array
*/
public $attributes = array();

/**
* List of inner blocks (of this same class)
*
* @var WP_Block[]
*/
public $inner_blocks = array();

/**
* Resultant HTML from inside block comment delimiters after removing inner
* blocks.
*
* @example "...Just <!-- wp:test /--> testing..." -> "Just testing..."
*
* @var string
*/
public $inner_html = '';

/**
* List of string fragments and null markers where inner blocks were found
*
* @example array(
* 'inner_html' => 'BeforeInnerAfter',
* 'inner_blocks' => array( block, block ),
* 'inner_content' => array( 'Before', null, 'Inner', null, 'After' ),
* )
*
* @var array
*/
public $inner_content = array();

/**
* Constructor.
*
* Populates object properties from the provided block instance argument.
*
* The given array of context values will not necessarily be available on
* the instance itself, but is treated as the full set of values provided by
* the block's ancestry. This is assigned to the private `available_context`
* property. Only values which are configured to consumed by the block via
* its registered type will be assigned to the block's `context` property.
*
* @param array $block Array of parsed block properties.
* @param array $available_context Optional array of ancestry context values.
* @param WP_Block_Type_Registry $registry Optional block type registry.
*/
public function __construct( $block, $available_context = array(), $registry = null ) {
$this->name = $block['blockName'];

if ( is_null( $registry ) ) {
$registry = WP_Block_Type_Registry::get_instance();
}

$this->block_type = $registry->get_registered( $this->name );

if ( ! empty( $block['attrs'] ) ) {
$this->attributes = $block['attrs'];
}

if ( ! is_null( $this->block_type ) ) {
$this->attributes = $this->block_type->prepare_attributes_for_render( $this->attributes );
}

$this->available_context = $available_context;

if ( ! empty( $this->block_type->context ) ) {
foreach ( $this->block_type->context as $context_name ) {
if ( array_key_exists( $context_name, $this->available_context ) ) {
$this->context[ $context_name ] = $this->available_context[ $context_name ];
}
}
}

if ( ! empty( $block['innerBlocks'] ) ) {
$child_context = $this->available_context;

/* phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase */
if ( ! empty( $this->block_type->providesContext ) ) {
foreach ( $this->block_type->providesContext as $context_name => $attribute_name ) {
if ( array_key_exists( $attribute_name, $this->attributes ) ) {
$child_context[ $context_name ] = $this->attributes[ $attribute_name ];
}
}
}
/* phpcs:enable */

$this->inner_blocks = array_map(
function( $inner_block ) use ( $child_context, $registry ) {
return new WP_Block( $inner_block, $child_context, $registry );
},
$block['innerBlocks']
);
}

if ( ! empty( $block['innerHTML'] ) ) {
$this->inner_html = $block['innerHTML'];
}

if ( ! empty( $block['innerContent'] ) ) {
$this->inner_content = $block['innerContent'];
}
}

/**
* Generates the render output for the block.
*
* @return string Rendered block output.
*/
public function render() {
global $post;

$is_dynamic = $this->name && null !== $this->block_type && $this->block_type->is_dynamic();
$block_content = '';

$index = 0;
foreach ( $this->inner_content as $chunk ) {
$block_content .= is_string( $chunk ) ?
$chunk :
$this->inner_blocks[ $index++ ]->render();
}

if ( $is_dynamic ) {
$global_post = $post;
$block_content = (string) call_user_func( $this->block_type->render_callback, $this, $block_content );
$post = $global_post;
}

return $block_content;
}

/**
* Returns true if an attribute exists by the specified attribute name, or
* false otherwise.
*
* @link https://www.php.net/manual/en/arrayaccess.offsetexists.php
*
* @param string $attribute_name Name of attribute to check.
*
* @return bool Whether attribute exists.
*/
public function offsetExists( $attribute_name ) {
return isset( $this->attributes[ $attribute_name ] );
}

/**
* Returns the value by the specified attribute name.
*
* @link https://www.php.net/manual/en/arrayaccess.offsetget.php
*
* @param string $attribute_name Name of attribute value to retrieve.
*
* @return mixed|null Attribute value if exists, or null.
*/
public function offsetGet( $attribute_name ) {
// This may cause an "Undefined index" notice if the attribute name does
// not exist. This is expected, since the purpose of this implementation
// is to align exactly to the expectations of operating on an array.
return $this->attributes[ $attribute_name ];
}

/**
* Assign an attribute value by the specified attribute name.
*
* @link https://www.php.net/manual/en/arrayaccess.offsetset.php
*
* @param string $attribute_name Name of attribute value to set.
* @param mixed $value Attribute value.
*/
public function offsetSet( $attribute_name, $value ) {
if ( is_null( $attribute_name ) ) {
// This is not technically a valid use-case for attributes. Since
// this implementation is expected to align to expectations of
// operating on an array, it is still supported.
$this->attributes[] = $value;
} else {
$this->attributes[ $attribute_name ] = $value;
}
}

/**
* Unset an attribute.
*
* @link https://www.php.net/manual/en/arrayaccess.offsetunset.php
*
* @param string $attribute_name Name of attribute value to unset.
*/
public function offsetUnset( $attribute_name ) {
unset( $this->attributes[ $attribute_name ] );
}

}
Loading