Skip to content

Commit

Permalink
Prototype asynchronous notices for blocks.
Browse files Browse the repository at this point in the history
The POST to the REST API in the JS file doesn't work yet.
And this doesn't apply a requirement in PR #1019:
'Pass validation errors back in REST API response when Gutenberg saves a post.'
But this is a prototype of a way to display notices for each block, async.
The UX would be like that in the previous PR #902.
As Weston mentioned earlier, edit() is synchronous.
And REST requests to validate content could delay it.
So add a component for 'edit.'
Display a notice, based on the state of isValidAMP.
The REST API request will update the state.
  • Loading branch information
Ryan Kienstra committed Mar 18, 2018
1 parent 27e8bec commit 590cd76
Show file tree
Hide file tree
Showing 3 changed files with 243 additions and 4 deletions.
185 changes: 185 additions & 0 deletions assets/js/amp-block-validation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/*jshint esversion: 6 */
/*global _, wp:true */
/**
* AMP Gutenberg integration.
*
* On editing a block, this checks that the content is AMP-compatible.
* And it displays a notice if it's not.
*/

/* exported ampBlockValidation */
var ampBlockValidation = ( function( $ ) {
var module = {

/**
* Holds data.
*/
data: {},

/**
* Boot module.
*
* @param {Object} data Object data.
* @return {void}
*/
boot: function( data ) {
module.data = data;
$( document ).ready( function() {
if ( 'undefined' !== typeof wp.blocks ) {
module.processBlocks();
}
} );
},

/**
* Gets all of the registered blocks, and overwrites their edit() functions.
*
* The new edit() functions will check if the content is AMP-compliant.
* If not, it will display a notice.
*
* @returns {void}
*/
processBlocks: function() {
var blocks = wp.blocks.getBlockTypes();
blocks.forEach( function( block ) {
if ( block.hasOwnProperty( 'name' ) ) {
module.overwriteEdit( block.name );
}
} );
},

/**
* Overwrites the edit() function of a block.
*
* Outputs the original edit function, stored in OriginalBlockEdit.
* This also appends a notice to the block.
* It only displays if the block's content isn't valid AMP,
*
* @see https://riad.blog/2017/10/16/one-thousand-and-one-way-to-extend-gutenberg-today/
* @param {string} blockType the type of the block, like 'core/paragraph'.
* @returns {void}
*/
overwriteEdit: function( blockType ) {
let block = wp.blocks.unregisterBlockType( blockType );
let OriginalBlockEdit = block.edit;

block.edit = class AMPNotice extends wp.element.Component {

/**
* The AMPNotice constructor.
*
* @param {object} props The component properties.
* @returns {void}
*/
constructor( props ) {
props.attributes.pendingValidation = false;
super( props );
this.validateAMP = _.throttle( this.validateAMP, 5000 );
this.state = { isInvalidAMP: false };
}

/**
* Outputs the existing block, with a Notice element below.
*
* The Notice only appears if the state of isInvalidAMP is false.
* It also displays the name of the block.
*
* @returns {array} The elements.
*/
render() {
let originalEdit;
let result;

result = [];
originalEdit = wp.element.createElement( OriginalBlockEdit, this.props );
if ( originalEdit ) {
result.push( originalEdit );
}
if ( this.state.isInvalidAMP && wp.components.hasOwnProperty( 'Notice' ) ) {
result.push( wp.element.createElement(
wp.components.Notice,
{
status: 'warning',
content: module.data.i18n.notice.replace( '%s', this.props.name ),
isDismissible: false
}
) );
}

this.props.attributes.pendingValidation = false;
return result;
}

/**
* Handler for after the component mounting.
*
* If validateAMP() changes the isInvalidAMP state, it will result in this method being called again.
* There's no need to check if the state is valid again.
* So this skips the check if pendingValidation is true.
*
* @returns {void}
*/
componentDidMount() {
if ( ! this.props.attributes.pendingValidation ) {
let content = this.props.attributes.content;
if ( 'string' !== typeof content ) {
content = wp.element.renderToString( content );
}
if ( content.length > 0 ) {
this.validateAMP( this.props.attributes.content );
}
}
this.props.attributes.pendingValidation = false;
}

/**
* Validates the content for AMP compliance, and sets the state of the Notice.
*
* Depending on the results of the validation,
* it sets the Notice component's isInvalidAMP state.
* This will cause the notice to either display or be hidden.
*
* @param {string} content The block content, from calling save().
* @returns {void}
*/
validateAMP( content ) {
this.setState( function() {

// Changing the state can cause componentDidMount() to be called, so prevent it from calling validateAMP() again.
component.props.attributes.pendingValidation = true;
return { isInvalidAMP: ( Math.random() > 0.5 ) };
} );

let component = this;
$.post(
module.data.endpoint,
{
markup: content
}
).done( function( data ) {
if ( data.hasOwnProperty( 'removed_elements' ) && ( 0 === data.removed_elements.length ) && ( 0 === data.removed_attributes.length ) ) {
component.setState( function() {

// Changing the state can cause componentDidMount() to be called, so prevent it from calling validateAMP() again.
component.props.attributes.pendingValidation = true;
return { isInvalidAMP: false };
} );
} else {
component.setState( function() {
component.props.attributes.pendingValidation = true;
return { isInvalidAMP: true };
} );
}
} );
}

};

wp.blocks.registerBlockType( blockType, block );
}

};

return module;

} )( window.jQuery );
42 changes: 38 additions & 4 deletions includes/utils/class-amp-validation-utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,13 @@ class AMP_Validation_Utils {
*/
const VALIDATION_ERRORS_META_BOX = 'amp_validation_errors';

/**
* The namespace of the REST API request.
*
* @var string
*/
const REST_NAMESPACE ='amp-wp/v1';

/**
* The errors encountered when validating.
*
Expand Down Expand Up @@ -211,6 +218,7 @@ public static function init() {
add_filter( 'dashboard_glance_items', array( __CLASS__, 'filter_dashboard_glance_items' ) );
add_action( 'rightnow_end', array( __CLASS__, 'print_dashboard_glance_styles' ) );
add_action( 'save_post', array( __CLASS__, 'handle_save_post_prompting_validation' ), 10, 2 );
add_action( 'enqueue_block_editor_assets', array( __CLASS__, 'enqueue_block_validation' ) );
}

add_action( 'edit_form_top', array( __CLASS__, 'print_edit_form_validation_status' ), 10, 2 );
Expand Down Expand Up @@ -406,7 +414,7 @@ public static function process_markup( $markup ) {
* @return void
*/
public static function amp_rest_validation() {
register_rest_route( 'amp-wp/v1', '/validate', array(
register_rest_route( self::REST_NAMESPACE, '/validate', array(
'methods' => 'POST',
'callback' => array( __CLASS__, 'handle_validate_request' ),
'args' => array(
Expand Down Expand Up @@ -437,15 +445,15 @@ public static function has_cap() {
* @return array|WP_Error.
*/
public static function handle_validate_request( WP_REST_Request $request ) {
$json = $request->get_json_params();
if ( empty( $json[ self::MARKUP_KEY ] ) ) {
$markup = $request->get_param( self::MARKUP_KEY );
if ( empty( $markup ) ) {
return new WP_Error( 'no_markup', 'No markup passed to validator', array(
'status' => 404,
) );
}

// @todo Add request param to indicate whether the supplied content is raw (and needs the_content filters applied).
$processed = self::process_markup( $json[ self::MARKUP_KEY ] );
$processed = self::process_markup( $markup );
$response = self::summarize_validation_errors( self::$validation_errors );
self::reset_validation_results();
$response['processed_markup'] = $processed;
Expand Down Expand Up @@ -1939,4 +1947,30 @@ public static function get_recheck_link( $post, $redirect_url, $recheck_url = nu
);
}

/**
* Enqueues the block validation script.
*
* @return void
*/
public static function enqueue_block_validation() {
$slug = 'amp-block-validation';

wp_enqueue_script(
$slug,
amp_get_asset_url( "js/{$slug}.js" ),
array( 'jquery' ),
AMP__VERSION,
true
);

$data = wp_json_encode( array(
'i18n' => array(
/* translators: %s: the name of the block */
'notice' => __( 'The %s block above has invalid AMP', 'amp' ),
),
'endpoint' => get_rest_url( null, self::REST_NAMESPACE . '/validate' ),
) );
wp_add_inline_script( $slug, sprintf( 'ampBlockValidation.boot( %s );', $data ) );
}

}
20 changes: 20 additions & 0 deletions tests/test-class-amp-validation-utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -1449,6 +1449,26 @@ public function test_get_recheck_link() {
$this->assertContains( 'Recheck the URL for AMP validity', $link );
}

/**
* Test enqueue_block_validation.
*
* @covers AMP_Theme_Support::enqueue_block_validation()
*/
public function test_enqueue_block_validation() {
global $post;
$post = $this->factory()->post->create(); // WPCS: global override ok.
$slug = 'amp-block-validation';
AMP_Validation_Utils::enqueue_block_validation();

$script = wp_scripts()->registered[ $slug ];
$this->assertContains( 'js/amp-block-validation.js', $script->src );
$this->assertEquals( array( 'jquery' ), $script->deps );
$this->assertEquals( AMP__VERSION, $script->ver );
$this->assertTrue( in_array( $slug, wp_scripts()->queue, true ) );
$this->assertContains( 'ampBlockValidation.boot', $script->extra['after'][1] );
$this->assertContains( 'The %s block above has invalid AMP', $script->extra['after'][1] );
}

/**
* Creates and inserts a custom post.
*
Expand Down

0 comments on commit 590cd76

Please sign in to comment.