WordPress.org

WordPress Developer Blog

Understanding block attributes

There’s more to attributes than might initially meet the eye. A block can have as many attributes as you like, and these are defined in the attributes property of the block’s block.json file. 

Block attributes are essentially variables that store data or content for the block. Users can modify and update the values stored there. But where does the data or content come from?

An attribute can get its value from a defined default, or from user input. It can also be populated with a value retrieved from the stored content. The values retrieved in this last way can, in fact, come from a variety of different places in the stored content and can be retrieved in various ways. Let’s dig in.

The Anatomy of a simple block

Consider this simple block. It has a single attribute, namely content:

"attributes": {
  "content": {
    "type": "string"
  }
}

The Edit() function implements a RichText component that displays and updates the content attribute:

import { useBlockProps, RichText } from '@wordpress/block-editor';

export default function Edit( { attributes, setAttributes } ) {
  return (
    <RichText
      { ...useBlockProps() }
      placeholder="Type something here..."
      tagName="div"
      value={ attributes.content }
      onChange={ ( val ) => setAttributes( { content: val } ) }
    />
  );
}

The save() function does what you expect it to do and merely saves the content:

import { useBlockProps, RichText } from '@wordpress/block-editor';

export default function save( { attributes } ) {
  return (
    <RichText.Content
      { ...useBlockProps.save() }
      tagName="div"
      value={ attributes.content }
    />
  );
}

You’ll see this in your editor:

Add some content and save your post and this is what you’ll see in the front end of your site:

All well and good, and no surprises there. But let’s take a look at the code that this block generates.

Click on the three dots in top right of the editor and select “Code editor” – or use the handy shortcut key combination to toggle between the visual editor and the code editor.

This is what the code for this block looks like:

<!-- wp:mb/attributes-test {"content":"My cool content!"} -->
<div class="wp-block-mb-attributes-test">My cool content!</div>
<!-- /wp:mb/attributes-test -->

Notice something here. The text you entered into the RichText component’s text area appears twice – once in the attribute in the block delimiter, and then again in the actual block content.

You may think that this is a bit redundant, which of course it is!

By default block attributes are stored in the block delimiter. But this needn’t be the case. They are only stored in the delimiter if no source property is provided in the attribute definition.

The source and selector properties

The source and selector properties can be used in tandem with each other to target part of the saved content of the block so that the attribute can be populated with a value derived from the targeted area.

Let’s look at an example to see how these two properties work together. Add source and selector properties to the definition of the content attribute:

"attributes": {
  "content": {
    "type": "string",
    "source": "text",
    "selector": "div"
  }
}

Refresh the post in the editor and re-examine the code. Now you’ll see that the text only occurs once, in the block content, and it no longer appears in the block delimiter:

<!-- wp:mb/attributes-test -->
<div class="wp-block-mb-attributes-test">My even cooler content!</div>
<!-- /wp:mb/attributes-test -->

Yet, if you add a console.log statement to the Edit() function to view the attributes:

console.log( attributes );

You’ll see that the content attribute is still being populated:

The content attribute is in fact getting its value from the text, as defined by the source property, within the <div> element, as defined by the selector property – so when you reload the post in the editor the value property of the RichText component is populated with the text within the <div> element retrieved from the saved content.

It’s important to ensure that you’re using the correct value for the source property of your attribute. Let’s see why. 

Use the RichText component’s toolbar to make part of the text bold:

If you inspect the code for this block now you’ll clearly see the <strong> tag wrapping the emboldened text:

<!-- wp:mb/attributes-test -->
<div class="wp-block-mb-attributes-test">My <strong>even cooler</strong> content!</div>
<!-- /wp:mb/attributes-test -->

Save the post, and check that it’s all looking good in the front-end.

Great! Now reload the post in the editor and…. oh yikes, that’s unexpected!

Check in the browser console and you’ll see an error something like this:

Content generated by `save` function:

<div class="wp-block-mb-attributes-test">My even cooler content!</div>

Content retrieved from post body:

<div class="wp-block-mb-attributes-test">My <strong>even cooler</strong> content!</div>

This is because you now have markup embedded in the content, namely the <strong></strong> tags and so it’s not pure text anymore. Change the value of the source property in the attribute from text to html:

"attributes": {
  "content": {
    "type": "string",
    "source": "html",
    "selector": "div"
  }
}

Now when you save the post and then reload it in the editor all will be well.

In effect the text value works akin to HTML’s textContent property, and the html value works akin to HTML’s innerHTML property.

Using HTML attributes as the source

As well as text and html, the source property can also take a value of an HTML attribute. In this case an additional attribute property must also be supplied.

Add a link to your text in the block:

If you now look at the block’s code you’ll see additional markup, namely the <a> tag with the URL you chose in the href attribute: 

<!-- wp:mb/attributes-test -->
<div class="wp-block-mb-attributes-test">My <strong>even cooler</strong> content - powered by <a href="https://wordpress.org">WordPress</a>.</div>
<!-- /wp:mb/attributes-test -->

Let’s get that URL into a block attribute. 

Add a new attribute to block.json and call it embedded-link:

"embedded-link": {
  "type": "string",
  "source": "attribute",
  "selector": "a",
  "attribute": "href"
}

As mentioned earlier, when you specify attribute as the value of source then you must supply an attribute property, in addition to the selector property.

So here we’re telling the block’s embedded-link attribute that its source is an HTML attribute, and that it should select the <a> element and get its value from the href attribute of that element.

Note: in the foregoing paragraph it’s important to distinguish between what is a block attribute and what is an HTML attribute. Don’t confuse the two!

Make sure you’ve still got the console.log( attributes ) statement in your  Edit() function and reload the post containing your block in the editor. 

You should now see that the attributes object has two properties, content and embedded-link:

  • content contains the markup entered by the user into the RichText component in the block
  • embedded-link contains the URL retrieved from the href attribute in the <a> element
{content: 'My <strong>even cooler</strong> content - powered by <a href=\"https://wordpress.org\">WordPress</a>.', embedded-link: 'https://wordpress.org'}

It should be noted that the value of selector can be any HTML tag or any CSS selector queryable with querySelector. So if in the markup of your block the <a> element had a class:

<a class="wp-link" href=\"https://wordpress.org\">WordPress</a>

Then you could instead target it thus:

"embedded-link": {
  "type": "string",
  "source": "attribute",
  "selector": ".wp-link",
  "attribute": "href"
}

Likewise, if the <a> element instead had a name attribute:

<a name="wp-link" href=\"https://wordpress.org\">WordPress</a>

It could then be targeted like this: 

"embedded-link": {
  "type": "string",
  "source": "attribute",
  "selector": "a[name='wp-link']",
  "attribute": "href"
}

This is all well and good for targeting single items of content, but what if you want to get multiple items? For example, what if you want to get all the URLs from all the <a> elements?

Using query as the source

At the moment there’s only a single <a> element, so add an additional link to your block’s content:

Now your block’s code contains two links and looks something like this:

<!-- wp:mb/attributes-test -->
<div class="wp-block-mb-attributes-test">My <strong>even cooler</strong> content - powered by <a href="https://wordpress.org">WordPress</a>, made with <a href="https://en.wikipedia.org/wiki/Love">love</a>.</div>
<!-- /wp:mb/attributes-test -->

Change the embedded-link attribute in block.json to instead be called embedded-links, as this is more meaningful for storing multiple values. Define it as follows:

"embedded-links": {
  "type": "array",
  "source": "query",
  "selector": "a",
  "query": {
    "link": {
      "type": "string",
      "source": "attribute",
      "attribute": "href"
    }
  }
}

Here we’ve changed the value of the type property to array, as we’re going to be storing multiple values. We’ve also changed the source to query, while the selector remains as a.

Just as when the source was set to attribute it was necessary to define an attribute property, when the source is set as query you must define a query property. 

The query yields an array of objects and it is in the definition of the query property that you define the structure of the object. Here, each object in the array will have just one property, namely link which will store a string, and it will use as its source the href attribute of each <a> element iterated over. 

Refresh the post in the editor and, assuming you still have the console.log( attributes ) statement in your  Edit() function, if you look in the browser console you should see that both the URLs have been captured.

embedded-links: Array(2)
  0: {link: 'https://wordpress.org'}
  1: {link: 'https://en.wikipedia.org/wiki/Love'}

Let’s also try getting the text content of the links. Add a link-content property to the query:

"embedded-links": {
  "type": "array",
  "source": "query",
  "selector": "a",
  "query": {
    "link": {
      "type": "string",
      "source": "attribute",
      "attribute": "href"
    },
    "link-content": {
      "type": "string",
      "source": "text"
    }
  }
}

And you should see something like this in the console:

embedded-links: Array(2)
  0: {
    link: 'https://wordpress.org'.
    link-content: "WordPress"
  }
  1: {
    link: 'https://en.wikipedia.org/wiki/Love',
    link-content: "love"
  }

Conclusion

Attributes provide a means for users to change the content and other data for the block. If you can retrieve attribute values from within the block’s saved content then you should do so by defining a source property to specify where you want to retrieve the value from. 

However, not all values can be retrieved from the content. It is still perfectly legitimate to store attributes in the block’s delimiter. Values stored in the delimiter would generally come from fields that you provide for the user, for example fields within an <InspectorControls> component that appear in the settings sidebar. 

For the full low-down on working with attributes see the Attributes page in the Block Editor Handbook.

Kudos for reviewing this post go to: @bph, @juanmaguitar, and @thatdevgirl

4 responses to “Understanding block attributes”

  1. Deryck Avatar

    Great article Michael. I always struggled with using HTML attributes as the source and this makes it so clear. Thanks!

  2. Mateus Machado Luna Avatar
    Mateus Machado Luna

    I’ve been creating blocks since 5.0 and this is the first time that I actually understood how the query works! Thanks man!

  3. Bridget Avatar
    Bridget

    Thank you so much for writing this article. It cleared up how/when/why to use attributes’ source and selector properties. I definitely had a few ah-ha moments reading through it.

  4. Egor Avatar
    Egor

    don’t get how to work with type array.
    setAttributes() sets all values, render works, but on setAttribute() when POST request is triggered, new data for a attribute with type array is never sent, also tried query, effect is the same.

Leave a Reply

Your email address will not be published. Required fields are marked *