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

Blocks: Add new utility function to add script / viewScript to an already registered block #48382

Open
fabiankaegy opened this issue Feb 23, 2023 · 20 comments
Labels
[Feature] Block API API that allows to express the block paradigm. [Feature] Extensibility The ability to extend blocks or the editing experience [Feature] Interactivity API API to add frontend interactivity to blocks. Needs Dev Ready for, and needs developer efforts [Type] Enhancement A suggestion for improvement.

Comments

@fabiankaegy
Copy link
Member

fabiankaegy commented Feb 23, 2023

What problem does this address?

Part of #41236.

There are many use-cases why one may want to add additional frontend javascript to existing blocks. This is both for core and custom blocks.

What is your proposed solution?

We already have a really solid system in place for handling the enqueue logic for the scripts that get defined in the block registration. We also have a utility function in core that allows you to add additional styles to any block called wp_enqueue_block_style.

My suggestion here would be to introduce two more similar functions:

  1. wp_enqueue_block_script → adds the script to the scriptHandles which ensures the file gets enqueued both in the editor and on the frontend
  2. wp_enqueue_block_view_script → adds the script to the viewScriptHandles which ensures the file gets enqueued on the frontend

Here is an example of how this function can get implemented:

/**
  * Enqueue a view script for a block
  *
  * @param string $block_name Block name.
  * @param array  $args {
  *    Optional. Array of arguments for enqueuing a view script.
  *    @type string $handle Script handle.
  *    @type string $src Script URL.
  *    @type array $dep Script dependencies.
  *    @type string|bool $ver Script version.
  *    @type bool $in_footer Whether to enqueue the script before </body> instead of in the <head>.
  * }
  * @return void
  */
 function wp_enqueue_block_view_script( $block_name, $args = array() ) {
 	$default_args = array(
 		'handle'    => '',
 		'src'       => '',
 		'dep'       => array(),
 		'ver'       => false,
 		'in_footer' => [ 'strategy' => 'defer' ],
 	);

 	$args = wp_parse_args( $args, $default_args );

 	$block = \WP_Block_Type_Registry::get_instance()->get_registered( $block_name );

 	wp_register_script(
 		$args['handle'],
 		$args['src'],
 		$args['dep'],
 		$args['ver'],
 		$args['in_footer']
 	);

 	if ( ! empty( $block ) ) {
 		$block->view_script_handles[] = $args['handle'];
 	}
 }
@fabiankaegy fabiankaegy added [Type] Enhancement A suggestion for improvement. [Feature] Block API API that allows to express the block paradigm. labels Feb 23, 2023
@gziolo gziolo changed the title Add new utility function to add script / viewScript to an already registered block Blocks: Add new utility function to add script / viewScript to an already registered block Feb 26, 2023
@gziolo gziolo added the Needs Dev Ready for, and needs developer efforts label Feb 26, 2023
@gziolo
Copy link
Member

gziolo commented Feb 26, 2023

Thank you for opening this issue that is now tracked in #41236 with other planned tasks for Block API.

Yes, that makes perfect sense to follow with more utility functions. I'm still unsure whether we need a single function instead that has a 3rd param that let's devs define other contexts: edit, view, or all. The same way we could update the existing function for styles. It's also worth pointing out that we still a way to define a style that loads only in the view context (frontend only). Finally, should we overload the first parameter in the utility function to allow defining multiple block names in case someone wants to load a shared script or style for multiple blocks (see related #41821)?

This is also good timing for this issue as I was about to file one anyway after seeing this tutorial by @justintadlock for Building a book review grid with a Query Loop block variation that surfaced exactly that challenge in the code example included:

add_action( 'enqueue_block_editor_assets', 'myplugin_assets' );

function myplugin_assets() {

    // Get plugin directory and URL paths.
    $path = untrailingslashit( __DIR__ );
    $url  = untrailingslashit( plugins_url( '', __FILE__ ) );

    // Get auto-generated asset file.
    $asset_file = "{$path}/build/index.asset.php";

    // If the asset file exists, get its data and load the script.
    if ( file_exists( $asset_file ) ) {
        $asset = include $asset_file;

        wp_enqueue_script(
            'book-reviews-variation',
            "{$url}/build/index.js",
            $asset['dependencies'],
            $asset['version'],
            true
        );
    }
}

@mrwweb
Copy link

mrwweb commented Nov 29, 2023

It would be really helpful to have this function(s)! (I don't really have a preference on the 1-vs-2 functions debate.)

Last week, I was working on an enhancement to the core YouTube embed block that would have benefited from this. I discovered @skorasaurus was also doing the exact same thing and shared this need.

During today's excellent developer meetup with @ndiego on extending blocks, the example plugin to flip the order of columns on mobile could have been further optimized with this functionality as well. cc: @ryanwelcher

It's possible to enqueue the script on the_content and probably render_block, but those definitely feel like hacks. The presence of wp_enqueue_block_style just really begs for a script equivalent!

@skorasaurus
Copy link
Member

I had been wondering about this again today (and as another use case) as I was trying to make a block variation on a dynamic block (custom one that I'm making) and wanted to enqueue a separate javascript file only when that variation was used.

@mrwweb
Copy link

mrwweb commented Dec 5, 2023

Quick note: enqueuing a script on render_block / render_block_{block} works. However, that technique and all others I know of have the limitation of only allowing scripts in wp_footer. That's a key reason for a first-class method to enqueue a specific script for a block.

@gziolo
Copy link
Member

gziolo commented Dec 6, 2023

wp_enqueue_block_view_script that @fabiankaegy included as an example implementation should allow enqueuing in both the header or footer. However, for blocks, we default now to the defer strategy:

https://github.com/WordPress/wordpress-develop/blob/ee461f010a87d806d31367d047a0d20804a80dde/src/wp-includes/blocks.php#L183-L186

The biggest challenge with the proposed implementation is that a registered block gets modified directly, so the function call needs to happen after the block gets registered. The way wp_enqueue_block_style is implemented more nuanced, but therefore it doesn't have these limitations. However, I'm not entirely sure that using the render_callback filter for every registered style is the most efficient approach.

Let's work on adding a helper function like this in Gutenberg or even WordPress core first. For simplicity we could fully mirror wp_enqueue_block_style and add wp_enqueue_block_script. If we want to have control over script type, we could do

wp_enqueue_block_script( 'core/button', array(
    'handle' => 'my-custom-view-script',
    'type'   => 'view',   
) );

By the way, if you feel like we miss viewStyle, there is an active proposal that awaits feedback:

@mediaformat
Copy link
Contributor

I like this terseness:

For simplicity we could fully mirror wp_enqueue_block_style and add wp_enqueue_block_script. If we want to have control over script type, we could do

wp_enqueue_block_script( 'core/button', array(
    'handle' => 'my-custom-view-script',
    'type'   => 'view',   
) );

Alternatively, the place I immediately thought to add a viewScript was mirroring the block.json property into registerBlockVariation:

wp.blocks.registerBlockVariation(
        'core/embed',
        [{
            name: 'exampleVariation',
            title: 'Example Service',
            viewScript: [ 'file:./view.js', 'example-embed-variation-view-script' ],
        }]
);

Which I suppose could also apply to the $variations object returned from the get_block_type_variations filter.

At this point I'd be happy with any alternative to using has_block() and parse_blocks( $post ) for conditionally enqueuing ;-)

@gziolo gziolo added the [Feature] Interactivity API API to add frontend interactivity to blocks. label Sep 11, 2024
@gziolo
Copy link
Member

gziolo commented Sep 11, 2024

I'm curious how that functionality fits into the world of frontend powered by Interactivity API. In that case, developers no longer enqueue regular scripts but Script Modules instead. In effect, there is a new field in block.json - viewScriptModule that should be used instead of viewScript. @sirreal, I would be curious to hear your thoughts about how this extensibility feature fits into the new model of script modules.

@gziolo gziolo added the [Feature] Extensibility The ability to extend blocks or the editing experience label Sep 11, 2024
@poof86
Copy link

poof86 commented Sep 11, 2024

Maybe this will help someone:

You can also do this by registering extra view (module) scripts during the 'register_block_type_args' hook and adding the ids/handles to the 'view_script_module_ids'/view_script_handles args properties. If a script needs to be enqueued dependent on an attribute value, you can use a 'render_callback' wrapper and do your enqueueing there.

This makes it really easy to extend exiting blocks with scripts but also adjust rendering or add Interactivity API directives and even make them interactive by setting the arg ['supports']['interactivity'] = true;

@fabiankaegy
Copy link
Member Author

@gziolo from my perspective we should have utility functions for both cases :)

@sirreal
Copy link
Member

sirreal commented Sep 11, 2024

This seems to be related in some ways to discussions around lazy hydration techniques: #52723

@poof86 related directly to your comment: #48382 (comment).

@sirreal
Copy link
Member

sirreal commented Sep 11, 2024

I'm trying to understand the use case this seeks to support. Concrete examples would help. It's possible I'm missing the point of this discussion.


I believe we're talking about a case like the core/file block:

// If it's interactive, enqueue the script module and add the directives.
if ( ! empty( $attributes['displayPreview'] ) ) {
$suffix = wp_scripts_get_suffix();
if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) {
$module_url = gutenberg_url( '/build-module/block-library/file/view.min.js' );
}
wp_register_script_module(
'@wordpress/block-library/file',
isset( $module_url ) ? $module_url : includes_url( "blocks/file/view{$suffix}.js" ),
array( '@wordpress/interactivity' ),
defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' )
);
wp_enqueue_script_module( '@wordpress/block-library/file' );

I'll start by saying that with Script Modules, it should be easy to conditionally load some some Script Modules if some JavaScript is is unconditionally loaded (1 or >1 assets). import( 'conditional-script-module' ) can be used for conditional loading. What's difficult today is deciding to whether to load JavaScript assets completely (0 or >0 assets).

In the case of core/file, the View Script Module should be enqueued conditionally based on the presence of the displayPreview attribute. There's no way to do that right now. It seems like the the proposed function would support that, but I'm not sure it's any better for that use case. The core/file block's render callback should only need to include this:

function render_block_core_file( $attributes, $content ) {
	// If it's interactive, enqueue the script module and add the directives.
	if ( ! empty( $attributes['displayPreview'] ) ) {
		wp_enqueue_script_module( '@wordpress/block-library/file' );
	}
}

The proposed wp_enqueue_block_view_script function doesn't seem better than this way of conditionally enqueuing the Script Module, a different function would still have to be called under the same conditions.

@mrwweb
Copy link

mrwweb commented Sep 11, 2024

@sirreal

I'm trying to understand the use case this seeks to support. Concrete examples would help.

My comment offered at least two examples and I think @skorasaurus had another.

  1. Enqueue a script module when the YouTube embed block is present in order to swap the default iframe with a lite-youtube custom element (hence the script)
  2. (My memory is a little fuzzy on this one) @ndiego showed an example of a small script that flipped the source order of columns on small screens

@sirreal
Copy link
Member

sirreal commented Sep 11, 2024

Thanks for the details 👍

Enqueue a script module when the YouTube embed block is present in order to swap the default iframe with a lite-youtube custom element (hence the script)

In the case of the Youtube embed, is this is the relevant snippet of code?

function replace_youtube_embed_with_web_component( $content, $block ) {
	$isValidYouTube = 'youtube' === $block['attrs']['providerNameSlug'] && isset( $block['attrs']['url'] );
	if( ! $isValidYouTube || is_feed() ) {
		return $content;
	}
	$video_id = extract_youtube_id_from_uri( $block['attrs']['url'] );
	if ( ! $video_id ) {
		return $content;
	}
	wp_enqueue_script_module( 'lite-youtube' );
	// …
}

If I'm understanding correctly, most of that snippet would remain the same (enqueue the module only if conditions are met). The change would be from enqueuing the module manually to calling the proposed function:

function replace_youtube_embed_with_web_component( $content, $block ) {
	$isValidYouTube = 'youtube' === $block['attrs']['providerNameSlug'] && isset( $block['attrs']['url'] );
	if( ! $isValidYouTube || is_feed() ) {
		return $content;
	}
	$video_id = extract_youtube_id_from_uri( $block['attrs']['url'] );
	if ( ! $video_id ) {
		return $content;
	}
-	wp_enqueue_script_module( 'lite-youtube' );
+	wp_enqueue_block_view_script( 'core/embed', /* … */ );
	// …
}

@skorasaurus
Copy link
Member

skorasaurus commented Sep 11, 2024

ahh, yep, great memory @mrwweb:

Another example/context @sirreal:

I made a custom dynamic block for my workplace at https://gitlab.com/cpl/cpl-libcal-block (visual example at https://gitlab.com/cpl/cpl-libcal-block/-/blob/main/example.png?ref_type=heads) that fetches some data from a 3rd party API in JSON format, and then displays it neatly on the frontend.

I received a request to present the same data (and same attributes underneath the hood) in an accordion-like style.
I wanted to make a block variation so I could quickly swap between variations instead of deleting an entire block; minimize code duplication (same data fetching underneath the hood) but only enqueue the accordion JS library when the Accordion block variation was used.

I was pressed for time so I made a separate block but that has created more technical debt (2x the maintenance and duplicate code).

@mrwweb
Copy link

mrwweb commented Sep 11, 2024

@sirreal

In the case of the Youtube embed, is this is the relevant snippet of code?

Yup. That's it. You're right that this specific case with the conditional check for YouTube complicates things and so might not be much of a change in that case. (Though that said, you've highlighted other cases where there is an additional condition for enqueueing, so maybe there's some kind of case for supporting an array of attributes that needs to match in order to enqueue the script!)

Here's another example I've considered building in the past (it'll happen some day):

I want some kind of interactive expandable caption for the Media & Text block that requires JS for the show/hide functionality. (Maybe with the interactivity API, maybe not.) In that case, I'd want to be able to use wp_enqueue_block_view_script( 'core/media-text', 'custom-caption-script' ); from functions.php (or equivalent). That is much cleaner than having to call it from render_block and feels much better. I think going back to the OP helps here. There is already a wp_enqueue_block_style function that is very helpful. Having the equivalent for scripts will be equivalently helpful.

@sirreal
Copy link
Member

sirreal commented Sep 12, 2024

@ndiego
Copy link
Member

ndiego commented Sep 12, 2024

(My memory is a little fuzzy on this one) @ndiego showed an example of a small script that flipped the source order of columns on small screens

Here's that demo plugin: https://github.com/ndiego/enable-column-direction. The frontend js is just enqueued with wp_enqueue_scripts

@sirreal
Copy link
Member

sirreal commented Sep 12, 2024

Thanks @ndiego, is this also a case of needing a way to conditionally load the assets?

snippet
/**
 * Render icons on the frontend.
 */
function enable_column_direction_render_block_columns( $block_content, $block ) {
    $reverse_direction_on_mobile = isset( $block['attrs']['isReversedDirectionOnMobile'] ) ? $block['attrs']['isReversedDirectionOnMobile'] : false;
    
    if ( ! $reverse_direction_on_mobile ) {
		return $block_content;
	}


    // Since we will need the JavaScript for this block, now enqueue it.
    // Note: Remove if not using front-end JavaScript to control column order.
    wp_enqueue_script( 'enable-column-direction-frontend-scripts' );


    // Append the custom class to the block.
    $p = new WP_HTML_Tag_Processor( $block_content );
    if ( $p->next_tag() ) {
        $p->add_class( 'is-reversed-direction-on-mobile' );
    }
    $block_content = $p->get_updated_html();


	return $block_content;
}
add_filter( 'render_block_core/columns', 'enable_column_direction_render_block_columns', 10, 2 );

@ndiego
Copy link
Member

ndiego commented Sep 12, 2024

Yeah, that was my somewhat hacky way of conditionally enqueuing the script if the block is present. 😅

@gziolo
Copy link
Member

gziolo commented Sep 13, 2024

Let me summarize what I understand is necessary:

  1. For viewScript, there should be a way to enqueue it in the code separately from the block registration so it could be reused in many places. In effect, the same script handle can be used with multiple blocks based on the needs of the plugin author. Example code:
    wp_enqueue_block_view_script( 'core/columns', 'enable-column-direction-frontend-script' );
    WordPress would ensure that this script is only printed on the frontend when core/columns gets rendered.
  2. It gets less clear for me in the case of script as the reasoning is that it should always be enqueued in the editor. The example code would be nearly identical:
    wp_enqueue_block_script( 'core/columns', 'enable-column-direction-script' );
    In effect, it would be up to the developer to ensure it gets enqueued in the editor. So it shouldn't really be put into the parts of the code that gets processed only on the frontend.
  3. viewScriptModule is the most recent one, and it's more nuanced as it enforces ES Modules and would offer a potential integration with the Interactivity API. However, if it isn't related to at all to Interactivity API then it could work like (1). The example code wouldn't differ that much:
     wp_enqueue_block_view_script_module( 'core/columns', 'enable-column-direction-frontend-script-module' );

I'd want to be able to use wp_enqueue_block_view_script( 'core/media-text', 'custom-caption-script' ); from functions.php (or equivalent).

That would work for all 3 cases, meaning that all of them get alwys enqueue on the frontend, and 2 always gets enqueued in the editor.

That is much cleaner than having to call it from render_block and feels much better. I think going back to the OP helps here. There is already a wp_enqueue_block_style function that is very helpful. Having the equivalent for scripts will be equivalently helpful.

Replicating wp_enqueue_block_style is doable, but it's worth noting that behind the scenes it conditionally decides whether to enqueue the style when in the editor, or use render_block filter to conditional enqueue the style only when the block gets rendered. So something similar would be necessary here, too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Feature] Block API API that allows to express the block paradigm. [Feature] Extensibility The ability to extend blocks or the editing experience [Feature] Interactivity API API to add frontend interactivity to blocks. Needs Dev Ready for, and needs developer efforts [Type] Enhancement A suggestion for improvement.
Projects
None yet
Development

No branches or pull requests

8 participants