-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Add Server Side Render component. #5602
Changes from 30 commits
cd58776
b0ede76
340136e
514dd4f
a90f40d
0c127e1
b0b977a
395a168
1d05c2d
c40dd25
f323651
c4abc69
6557877
228c756
b48ef81
e39332d
46506a9
ce1f7c6
e0fec83
17e404a
39b867a
4c3129b
3c60d4b
a10bfac
a166324
f0f4a77
6d1ee65
2bbbeb7
6f7d62e
67af6b7
007fb1d
1c9ef56
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
ServerSideRender | ||
======= | ||
|
||
ServerSideRender component is used for server-side rendering preview in Gutenberg editor, specifically for dynamic blocks. Server-side rendering in a block's `edit` function should be mostly limited for blocks which are heavily dependent on (existing) PHP rendering logic that is heavily intertwined with data, such as when there are no endpoints available. | ||
|
||
Usage of ServerSideRender component could also be justified in the following two cases: | ||
* Lack of potential to take some existing widget-like functionality and improve its user-facing editing experience. | ||
* Unwillingness to create a full JS experience for a block considered legacy. | ||
|
||
Note that ServerSideRender should be regarded as a fallback. | ||
|
||
New blocks should be built in conjunction with any necessary REST API endpoints so that JavaScript can be used for rendering client-side in the `edit` function for the best user experience, instead of relying on using the PHP `render_callback`. The logic necessary for rendering should be included in the endpoint so that both the client-side JS and server-side PHP logic should require a mininal amount of differences. | ||
|
||
## Usage | ||
|
||
Render core/archives preview. | ||
```jsx | ||
<ServerSideRender | ||
block="core/archives" | ||
attributes={ this.props.attributes } | ||
/> | ||
``` | ||
|
||
## Output | ||
|
||
Output is using the `render_callback` set when defining the block. For example if `block="core/archives"` as in the example then the output will match `render_callback` output of that block. | ||
|
||
## API Endpoint | ||
API endpoint for getting the output for ServerSideRender is `/gutenberg/v1/block-renderer/:block`. It accepts any params which are used as `attributes` for the block's `render_callback` method. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
/** | ||
* External dependencies. | ||
*/ | ||
import { isEqual, isObject, map } from 'lodash'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { | ||
Component, | ||
RawHTML, | ||
} from '@wordpress/element'; | ||
import { __ } from '@wordpress/i18n'; | ||
|
||
export class ServerSideRender extends Component { | ||
constructor( props ) { | ||
super( props ); | ||
this.state = { | ||
response: null, | ||
}; | ||
} | ||
|
||
componentDidMount() { | ||
this.fetch( this.props ); | ||
} | ||
|
||
componentWillReceiveProps( nextProps ) { | ||
if ( ! isEqual( nextProps, this.props ) ) { | ||
this.fetch( nextProps ); | ||
} | ||
} | ||
|
||
fetch( props ) { | ||
this.setState( { response: null } ); | ||
const { block, attributes } = props; | ||
|
||
const path = '/gutenberg/v1/block-renderer/' + block + '?context=edit&' + this.getQueryUrlFromObject( { attributes } ); | ||
|
||
return wp.apiRequest( { path: path } ).then( ( response ) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is available a mapping to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed within #6571. |
||
if ( response && response.rendered ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's nothing to say that the component is still mounted by the time this response is received, so attempting to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed within #6571. |
||
this.setState( { response: response.rendered } ); | ||
} | ||
} ); | ||
} | ||
|
||
getQueryUrlFromObject( obj, prefix ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This sounds like a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In fact, it could probably be built-in as the default behavior for the existing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the behavior is needed, it could be added to the existing function via pull request to the packages repository. |
||
return map( obj, ( paramValue, paramName ) => { | ||
const key = prefix ? prefix + '[' + paramName + ']' : paramName, | ||
value = obj[ paramName ]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed within #6571. |
||
return isObject( paramValue ) ? this.getQueryUrlFromObject( value, key ) : | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe not intentional:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed within #6571. |
||
encodeURIComponent( key ) + '=' + encodeURIComponent( value ); | ||
} ).join( '&' ); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
import { map } from 'lodash';
return map( obj, ( paramValue, paramName ) => {
const key = prefix ? …;
return isObject( paramValue ) ? this.getQ… : encodeURI…;
} ).join( '&' );
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you, replaced the method in e0fec83. |
||
|
||
render() { | ||
const response = this.state.response; | ||
if ( ! response || ! response.length ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we check There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed within #6571, let me know if it looks OK now. |
||
return ( | ||
<div key="loading" className="wp-block-embed is-loading"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For what reason do we need a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replaced with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We shouldn't reuse classes but components. If there's something in the way the embed blocks handles the loading state that is useful, it should be extracted in a separate UI component. |
||
|
||
<p>{ __( 'Loading...' ) }</p> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need both a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replaced with |
||
</div> | ||
); | ||
} | ||
|
||
return ( | ||
<RawHTML key="html">{ response }</RawHTML> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess not because then styles won't be applied. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @westonruter Sandbox doesn't stop styles being applied (If I understand correctly it ensures no css / js bleeds into admin experience). It's used in #4710. Should work on such block-specifics defer to this once it's released? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I mean the CSS from the editor. Otherwise, the CSS has to be re-printed in the iframe. |
||
); | ||
} | ||
} | ||
|
||
export default ServerSideRender; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
<?php | ||
/** | ||
* Block Renderer REST API: WP_REST_Block_Renderer_Controller class | ||
* | ||
* @package gutenberg | ||
* @since ? | ||
*/ | ||
|
||
/** | ||
* Controller which provides REST endpoint for rendering a block. | ||
* | ||
* @since ? | ||
* | ||
* @see WP_REST_Controller | ||
*/ | ||
class WP_REST_Block_Renderer_Controller extends WP_REST_Controller { | ||
|
||
/** | ||
* Constructs the controller. | ||
* | ||
* @access public | ||
*/ | ||
public function __construct() { | ||
$this->namespace = 'gutenberg/v1'; | ||
$this->rest_base = 'block-renderer'; | ||
} | ||
|
||
/** | ||
* Registers the necessary REST API routes, one for each dynamic block. | ||
* | ||
* @access public | ||
*/ | ||
public function register_routes() { | ||
$block_types = WP_Block_Type_Registry::get_instance()->get_all_registered(); | ||
foreach ( $block_types as $block_type ) { | ||
if ( ! $block_type->is_dynamic() ) { | ||
continue; | ||
} | ||
|
||
register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<name>' . $block_type->name . ')', array( | ||
'args' => array( | ||
'name' => array( | ||
'description' => __( 'Unique registered name for the block.', 'gutenberg' ), | ||
'type' => 'string', | ||
), | ||
), | ||
array( | ||
'methods' => WP_REST_Server::READABLE, | ||
'callback' => array( $this, 'get_item' ), | ||
'permission_callback' => array( $this, 'get_item_permissions_check' ), | ||
'args' => array( | ||
'context' => $this->get_context_param( array( 'default' => 'view' ) ), | ||
'attributes' => array( | ||
/* translators: %s is the name of the block */ | ||
'description' => sprintf( __( 'Attributes for %s block', 'gutenberg' ), $block_type->name ), | ||
'type' => 'object', | ||
'additionalProperties' => false, | ||
'properties' => $block_type->attributes, | ||
), | ||
'post_id' => array( | ||
'description' => __( 'ID of the post context.', 'gutenberg' ), | ||
'type' => 'integer', | ||
), | ||
), | ||
), | ||
'schema' => array( $this, 'get_public_item_schema' ), | ||
) ); | ||
} | ||
} | ||
|
||
/** | ||
* Checks if a given request has access to read blocks. | ||
* | ||
* @since ? | ||
* @access public | ||
* | ||
* @param WP_REST_Request $request Request. | ||
* @return true|WP_Error True if the request has read access, WP_Error object otherwise. | ||
*/ | ||
public function get_item_permissions_check( $request ) { | ||
global $post; | ||
|
||
$post_id = isset( $request['post_id'] ) ? intval( $request['post_id'] ) : 0; | ||
|
||
if ( 0 < $post_id ) { | ||
$post = get_post( $post_id ); | ||
if ( ! $post || ! current_user_can( 'edit_post', $post->ID ) ) { | ||
return new WP_Error( 'gutenberg_block_cannot_read', __( 'Sorry, you are not allowed to read Gutenberg blocks of this post', 'gutenberg' ), array( | ||
'status' => rest_authorization_required_code(), | ||
) ); | ||
} | ||
} else { | ||
if ( ! current_user_can( 'edit_posts' ) ) { | ||
return new WP_Error( 'gutenberg_block_cannot_read', __( 'Sorry, you are not allowed to read Gutenberg blocks as this user.', 'gutenberg' ), array( | ||
'status' => rest_authorization_required_code(), | ||
) ); | ||
} | ||
} | ||
|
||
return true; | ||
} | ||
|
||
/** | ||
* Returns block output from block's registered render_callback. | ||
* | ||
* @since ? | ||
* @access public | ||
* | ||
* @param WP_REST_Request $request Full details about the request. | ||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. | ||
*/ | ||
public function get_item( $request ) { | ||
global $post; | ||
|
||
$post_id = isset( $request['post_id'] ) ? intval( $request['post_id'] ) : 0; | ||
|
||
if ( 0 < $post_id ) { | ||
$post = get_post( $post_id ); | ||
|
||
// Set up postdata since this will be needed if post_id was set. | ||
setup_postdata( $post ); | ||
} | ||
$registry = WP_Block_Type_Registry::get_instance(); | ||
$block = $registry->get_registered( $request['name'] ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you return an error here if the block is invalid? |
||
|
||
if ( null === $block ) { | ||
return new WP_Error( 'gutenberg_block_invalid', __( 'Invalid block.', 'gutenberg' ), array( | ||
'status' => 404, | ||
) ); | ||
} | ||
|
||
$data = array( | ||
'rendered' => $block->render( $request->get_param( 'attributes' ) ), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @westonruter This isn't working with the current SSR component code since the attributes are all sent as separate params and not as one
Just sending There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a good question. I think it's important for const apiURL = addQueryArgs( '/gutenberg/v1/block-renderer/' + block, {
attributes,
_wpnonce: wpApiSettings.nonce,
} ); In other words, I think There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Couldn't find an existing method at this moment, apparently query-string is intentionally not supporting nested attributes and suggests sending the object as a JSON string. Added a custom method for now to the class to put together the query string supporting objects ( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We may also be passing the full block content to the render callback in #6239 Rather than needing to remember passing all supported arguments to the |
||
); | ||
return rest_ensure_response( $data ); | ||
} | ||
|
||
/** | ||
* Retrieves block's output schema, conforming to JSON Schema. | ||
* | ||
* @since ? | ||
* @access public | ||
* | ||
* @return array Item schema data. | ||
*/ | ||
public function get_item_schema() { | ||
return array( | ||
'$schema' => 'http://json-schema.org/schema#', | ||
'title' => 'rendered-block', | ||
'type' => 'object', | ||
'properties' => array( | ||
'rendered' => array( | ||
'description' => __( 'The rendered block.', 'gutenberg' ), | ||
'type' => 'string', | ||
'required' => true, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added within a10bfac. |
||
'context' => array( 'edit' ), | ||
), | ||
), | ||
); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should check if
this.state.response
is alreadynull
here, as it will be when this function is called bycomponentDidMount
and we'll wastefully cause an immediate re-render (setState
always incurs a render unless prevented byshouldComponentUpdate
).https://reactjs.org/docs/react-component.html#componentdidmount
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed within #6571.