Changing the attribute set for dynamic Gutenberg blocks

The Block Editor Handbook has excellent documentation for updating static block markup and attributes, but not as much when it comes to updating dynamic blocks. This isn’t surprising, as it’s generally much easier to make updates to dynamic blocks. But what if you need to make non-trivial changes to a dynamic block’s attribute set?

Say, for instance, that you have a block for displaying recent posts, and that this block allows users to select a single taxonomy and a single term from that taxonomy to limit which posts are displayed. Now say your users have requested the ability to display a set of posts based on more complex taxonomical criteria – maybe posts with one or more categories and/or one or more tags. That makes the update a little more complex, but still possible!

This scenario is, of course, based on actual events, so check out the pull request if you’re interested in that kind of thing. And a quick disclaimer before I get started: I’m not confident enough to take on the mantle of expertise for any subject, and always suspect that there’s a simpler, more efficient way to accomplish something than the approach I ended up taking. That said, I do want to share how I went about this update and what I learned during the process. (And please, if you find any factual inaccuracies in this article, don’t hesitate to leave a comment with a correction!)

Cart before the horse

I need to confess that figuring out backward compatibility was actually not the first thing I tackled when making this update. If it had been, I would have saved myself some headache later on.

My plan was to store an object containing a slug, array of terms, and operator for each taxonomy option in the customTaxonomy array. Except that wasn’t just the plan – it was work I’d already done. Now I needed to go back and sort out the block deprecation.

What I needed to account for were instances of the block already in production and using the single taxonomy/term options. These options were stored in the following attributes:

customTaxonomy: {
	type: 'string',
	default: '',
},
termID: {
	type: 'integer',
	default: 0,
},Code language: CSS (css)

My new attributes were going to look like this:

customTaxonomy: {
	type: 'array',
	default: [],
},
taxRelation: {
	type: 'string',
	default: '',
},Code language: CSS (css)

Note that this is JavaScript, and that I reused the customTaxonomy attribute but changed its type, because we’ll return to both points soon.

At first, I attempted to leverage the deprecated property to migrate values from previous attributes to the new ones. In retrospect, it’s clear that this simply won’t work for dynamic blocks.

I do declare!

It turns out there’s no need to declare attributes in the JavaScript registration if your block is dynamic – it’s only necessary to do so in PHP. (This may be true for other block properties, but I don’t know that for sure.) You’ll notice that most of the dynamic blocks in the Gutenberg plugin only have attributes declared via their PHP registration.

So I changed the pertinent block attributes – now declared solely in PHP via the register_block_type function – from:

'customTaxonomy'  => array(
	'type'    => 'string',
	'default' => '',
),
'termID'          => array(
	'type'    => 'integer',
	'default' => 0,
),Code language: PHP (php)

to:

'customTaxonomy'  => array(
	'type'    => 'array',
	'default' => array(),
),
'taxRelation'     => array(
	'type'    => 'string',
	'default' => '',
),Code language: PHP (php)

It took me longer than I’d care to admit to realize why the customTaxonomy and termID attributes from old versions of the block weren’t showing up. Eventually, though, the understanding that I needed to keep any “deprecated” attributes around if I wanted to access their values did dawn on me.

Additionally, those values are validated against the type declared for the attribute. You can probably see where I’m going with this – my plan to reuse the customTaxonomy attribute to store an array instead of a string ended up failing quite spectacularly.

Not your type?

I added the termID attribute back, along with a comment indicating that it was deprecated, but sorting out the customTaxonomy attribute would prove to be more complicated.

Things would have been straightforward if backward compatibility had been the first thing I took care of – I would just have to change the type and default back to what they previously were, and introduce a new attribute for capturing the array of taxonomy option objects.

However, there were versions of the block with the array-typed customTaxonomy attribute already in the wild by this point, so now I needed to be able to access both types of values in order to keep things backward compatible. Agh!

I thought I’d stumbled across an easy solution by removing the type and default from the attribute declaration:

'customTaxonomy'  => array(),Code language: PHP (php)

I was able to access both types of values now, but this just didn’t feel right, and I soon found my misgivings were not unfounded… because I was still attempting to store taxonomy options using the customTaxonomy attribute. After removing the type, updating those options simply didn’t work, regardless of the previous type of value.

Almost there!

Being able to access both types of values was still critical, so I stuck the type-less, default-less customTaxonomy at the bottom of the block registration under my // Deprecated comment along with the termID attribute.

Now I needed to introduce that new attribute for storing taxonomy options, then have fun with conditions to wrangle attribute values from previous versions of the block into the new format so everything would display as expected.

The first part was easy:

'taxonomies'      => array(
	'type'    => 'array',
	'default' => array(),
),Code language: PHP (php)

And this is how I handled shoehorning the “deprecated” attributes into the required format for the PHP rendering callback:

if ( $attributes['termID'] && ! is_array( $attributes['customTaxonomy'] ) ) {
	$attributes['customTaxonomy'] = array(
		array(
			'slug'  => $attributes['customTaxonomy'],
			'terms' => array( $attributes['termID'] ),
		),
	);
}

$taxonomies = ( ! empty( $attributes['taxonomies'] ) )
	? $attributes['taxonomies']
	: $attributes['customTaxonomy'];Code language: PHP (php)

Finally, I did the following to get things into shape in the JavaScript file for the editor side:

const taxonomies = ( 0 < attributes.taxonomies.length )
	? attributes.taxonomies
	: ( !attributes.customTaxonomy )
		? []
		: ( 'string' !== typeof attributes.customTaxonomy )
			? attributes.customTaxonomy
			: [ {
				slug: attributes.customTaxonomy,
				terms: [ `${attributes.termID}` ],
			} ];Code language: JavaScript (javascript)

Closing remarks

To restate my takeaways from this experience:

  • declaring attributes in JavaScript for a dynamic block is not only unnecessary, but really does nothing for you;
  • any attributes that you want to grab values from need to remain declared, with the proper type in place for validation; and
  • an attribute can be declared without a type, but probably shouldn’t be.

Once I’d learned those points, handling backward compatibility for this block update was actually quite simple – and would have been even more so had I known them before starting!

Another bit of knowledge I gleaned from reading the handbook closely is that it often may not be necessary to define a save function for your dynamic blocks – not even to return null. From the first example in the handbook:

Because it is a dynamic block it doesn’t need to override the default save implementation on the client. 

I tested this when making my updates to the block and didn’t seem to have any issues, but ultimately I chickened out and didn’t commit the change.

Hopefully this helps if you run into a similar situation!

2 Comments

I am using dynamic block also but i just want to update the new attribute from old attribute so the value are showing on InspectorControl fields, do you think it is possible ?

Hi Ed! Sorry for the delayed response.

Some conditional logic just within your block’s edit function might work here. Try something like:

if ( oldAttribute && ! newAttribute ) {
	setAttributes( { newAttribute: oldAttribute } );
}

I hope that helps!

Leave a Reply to Phil Cable Cancel reply