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

Explore easier ways to declare and use html ids within blocks #17246

Open
talldan opened this issue Aug 29, 2019 · 23 comments
Open

Explore easier ways to declare and use html ids within blocks #17246

talldan opened this issue Aug 29, 2019 · 23 comments
Labels
[Feature] Block API API that allows to express the block paradigm. [Type] Enhancement A suggestion for improvement.

Comments

@talldan
Copy link
Contributor

talldan commented Aug 29, 2019

Is your feature request related to a problem? Please describe.
Sometimes when implementing block edit and save components, a unique id is required. An example could be for associating a label with an input:

save( { labelId } ) {
  return (
    <div>
      <label htmlFor={ labelId }>MyLabel</label>
      <input id={ labelId } />
    </div>
  )
}

I needed to do this on #15554 and found it surprisingly difficult—it might be that I'm missing a much easier way to do this, I'm open to suggestions.

The option I went with there was to declare an attribute that sources the id, and also uses the block's clientId to build a default uniqueId in the edit component.

This real issue is that this had to be set as an attribute using setAttributes. For many use cases the only option might be to set this attribute when the component first renders, which would be less than ideal as it'd trigger an immediate re-render.

Describe the solution you'd like
A way to declare a unique id in attributes could be an option:

"attributes": {
    "inputId": {
        "type": "uniqueId",
        "prefix": "wp-block-my-block__input-id-",
        "source": "attribute",
        "selector": "input",
        "attribute": "id"
    }
}

This would ensure the attribute is available at first render and stays consistent before the block is saved.

The downside for me is that 'uniqueId' isn't really a JavaScript type. Perhaps there might be other options for declaring this aspect of the attribute.

Describe alternatives you've considered
Exposing the clientId in the save function could be another option to make this easier. That would mean setAttributes wouldn't need to be used as something like this would work:

// block.json
"attributes": {
    "inputId": {
        "type": "string",
        "source": "attribute",
        "selector": "input",
        "attribute": "id"
    }
}

// edit and save both use a default for the initial id value
( { attributes, clientId } ) => {
  const {
     labelId = `wp-block-my-block__${ clientId }`
  } = attributes;
 
  // ...
};

My main problem with this is that it's using clientId for something that it wasn't intended to do.

@talldan talldan added [Type] Enhancement A suggestion for improvement. [Feature] Block API API that allows to express the block paradigm. labels Aug 29, 2019
@ZebulanStanphill
Copy link
Member

ZebulanStanphill commented Apr 19, 2020

Correct me if I'm wrong, but I think useInstanceId might be what you're looking for?

@talldan
Copy link
Contributor Author

talldan commented Apr 19, 2020

@ZebulanStanphill It's pretty intricate the deeper you look into it. The issue with useInstanceId is that it's still possible to get duplicates. Currently it generates an incrementing integer that starts at 0 at runtime. This Id needs to be saved as an attribute for the save function, so if I had one instance of my block with an id of 0, later on if I reload that post and add another block, that might also end up with 0. Also, when duplicating, the id is duplicated.

I think a UUID is the best bet. The clientID at the moment is the closest thing as that's unique for blocks, but having to use setAttributes on first render is still not ideal. React hooks does make it a bit easier to manage when that's called though:

useEffect( () => {
  setAttributes( { myId: `my-id-${ clientId }` } );
},  [ clientId ] );

☝️ That's not too bad, it handles duplicated blocks, but it still results in existing blocks having new ids set again when loading a post, given blocks get new client ids.

@ZebulanStanphill
Copy link
Member

Notably, if you did save a UUID to attributes, and used the attributes as the source of truth, then duplicating that block would result in the duplicate using the same id.

Because of that, I can't think of a better solution than the usage of clientId that you suggested.

@noknokcody
Copy link

Notably, if you did save a UUID to attributes, and used the attributes as the source of truth, then duplicating that block would result in the duplicate using the same id.

Because of that, I can't think of a better solution than the usage of clientId that you suggested.

I think you're right on the money here. I've been fighting this issue all day. I've tried generating an ID when the block is constructed and saving this ID in attributes but there currently is no way to detect when a block is duplicated and purge this id field / regenerate.

From looking at similar posts across the community this appears to be an oversight from the Gutenburg team. There appears to be some expectation that the output of a block save function can be constructed entirely from attributes of that same block but the function of duplication in this mindset completely destroys the idea of any form of uniqueness for a block.

I've seen lots of people solve this issue by loading the client id of a block to its attributes in the edit() function and then accessing them again in the save() function. Ignoring the fact that you're updating state during a render there seems to be some doubt among the community that a blocks clientId can't change. So this solution is very much up in the air.

Personally, I feel that Gutenberg should extend their list of actions and filters to allow people to produce their own solutions. Pre-render attribute filters and an "on-block-duplicated" hook would've provided a means to solve this issue easily.

Alternatively, Gutenberg could update the attribute properties to allow you to mark attributes as non-duplicatable and also allow callbacks as default values for attributes as follows.

var attributes = {

        blockId: {
            type: 'string',
            duplicatable: false,
            default: myGenerateIdCallback
        },
}

@talldan
Copy link
Contributor Author

talldan commented Jul 1, 2021

There's also a discussion/proposal at #32604 about unique ids (though not specifically html ids).

@rmorse
Copy link
Contributor

rmorse commented Jul 22, 2022

I also came up against this problem and managed to solve in a round about way.

Essentially, I'm generating an ID on the server, and assigning it to a block, but how the ID is generated shouldn't really matter.

With a bunch of effects and a custom store, I'm then tracking client IDs against their unique ID (I'm calling this field ID) and if I find a duplicate field ID amongst any of the blocks (client IDs), I'm triggering a reset on the duplicate block (unsetting the field ID) so the process starts again and it gets assigned a fresh field ID.

It seems like a lot of work to ensure unique IDs but I think it solves the duplicate issue...

My block code looks like:

/**
 * WordPress dependencies
 */
import { useSelect, dispatch } from '@wordpress/data';
import { useLayoutEffect } from '@wordpress/element';
import './store';

const storeName = 'plugin-name/block';

function Edit( { attributes, setAttributes, clientId } ) {

	// Get the stored attribute field ID.
	const attributeFieldId = attributes.fieldId;

	// Get the copy of the stored field ID in our custom store.
	const fieldId = useSelect( ( select ) => {
		return select( storeName ).getClientFieldId( clientId );
	}, [ attributeFieldId ] );
	
	// If the attribute field ID has a value, register that in our store so it knows the value.
	useLayoutEffect( () => {
		if ( attributeFieldId !== '' ) {
			dispatch( storeName ).setFieldId( clientId, attributeFieldId );
		}
	}, [ attributeFieldId ] );

	// If there is no value in the store or in the attribute, create a new field ID.
	// Or if there is a field ID in the store (but not attribute), then copy that into
	// the attribute.
	useLayoutEffect( () => {
		if ( attributeFieldId === '' & fieldId === null ) {
			dispatch( storeName ).createFieldId( clientId, attributes );
		} else if ( attributeFieldId === '' & fieldId !== null ) {
			setAttributes( { fieldId } );
		}
	}, [ attributeFieldId, fieldId ] );

	// To handle duplicates, we need to check if a reset is needed, this is handled in
	// the store.
	const shouldResetFieldId = useSelect( ( select ) => {
		return select( storeName ).shouldResetFieldId( clientId );
	}, [ attributeFieldId, fieldId ] );

	// Now wait for the reset to be true before unsettting the attribute field ID and
	// the store field ID.
	useLayoutEffect( () => {
		if ( shouldResetFieldId ) {
			setAttributes( { fieldId: '' } );
			dispatch( storeName ).setFieldId( clientId, '' );
		}
	}, [ shouldResetFieldId ] );
	
	return (
		<>
			Block Field ID: { attributeFieldId }
		</>
	);
}

export default Edit;

Theres also some logic in the custom store, I've put the whole lot in a gist here:
https://gist.github.com/rmorse/de3df5ac9ff751c4c0fc6234397b7930

Also just found this which should add support for attributes that shouldn't be duplicated: #34750

@dennisheiden
Copy link

dennisheiden commented Aug 16, 2022

I don't think clientId is suitable for identification due it's nature of changing, spend hours figuring that out. What we did is generating an ID in the block on create / init and storing it in the attributes and in a class as suffix. Then we were able to check in the block init for duplicates in the editor dom just by doing a .querySelectorAll. For document you might wanna check for the correct root:

export function getBlockDocumentRoot(props){
	const iframes = document.querySelectorAll('.edit-site-visual-editor__editor-canvas');
	let _document = document;
	
	// check for block editor iframes
	for(let i = 0; i < iframes.length; i++){
		
		let block = iframes[i].contentDocument.getElementById('block-' + props.clientId);
		if(block !== null){
			_document = iframes[i].contentDocument;
			break;
		}
	}
	
	return _document;
}

This solution is relatively stable combined with a random ID generator. Our IDs look like this: MTY5ZDgyN2I2

There is still a chance of getting duplicates if you combine block content of two or more post types (like pages in sidebars or something like that), but with random IDs the chance is relatively low and duplicates in the same page get replaced on creation.

@natirivero
Copy link

natirivero commented Mar 30, 2023

useEffect( () => {
    if ( 0 === id.length ) {
        setAttributes( {
            id: clientId,
        } );
    }
}, [] );

source: https://webdevstudios.com/2020/08/04/frontend-editing-gutenberg-blocks-1/

@dennisheiden
Copy link

useEffect( () => {
    if ( 0 === id.length ) {
        setAttributes( {
            id: clientId,
        } );
    }
}, [] );

source: https://webdevstudios.com/2020/08/04/frontend-editing-gutenberg-blocks-1/

This will just check for an empty "id" attribute and use the clientId (which is not static) as value. This will not work very well if you need a static ID e.g. generating inline CSS for the specific block (for custom block controls). Even if you keep the id in the block attributes, this eventually will generate duplicates. Besides that, I don't know if attributes and clientId is already available on the very first render. IMHO

@natirivero
Copy link

natirivero commented Mar 31, 2023

useEffect( () => {
    if ( 0 === id.length ) {
        setAttributes( {
            id: clientId,
        } );
    }
}, [] );

source: https://webdevstudios.com/2020/08/04/frontend-editing-gutenberg-blocks-1/

This will just check for an empty "id" attribute and use the clientId (which is not static) as value. This will not work very well if you need a static ID e.g. generating inline CSS for the specific block (for custom block controls). Even if you keep the id in the block attributes, this eventually will generate duplicates. Besides that, I don't know if attributes and clientId is already available on the very first render. IMHO

Yeah, you are right, tried yesterday and had issues because it was not available on the very first rendering. I've removed the conditional and it works but I'm worried about duplicates.

I tried to use uuid but It would loop infinitely, I clearly didn't know what I was doing...

I've also used $id = { ...blockProps }.id; before, is that bad?

In the end, I only have workarounds for this, wish I had a better solution.

P.S. I don't need it to be static.

@natirivero
Copy link

I don't think clientId is suitable for identification due it's nature of changing, spend hours figuring that out. What we did is generating an ID in the block on create / init and storing it in the attributes and in a class as suffix. Then we were able to check in the block init for duplicates in the editor dom just by doing a .querySelectorAll. For document you might wanna check for the correct root:

export function getBlockDocumentRoot(props){
	const iframes = document.querySelectorAll('.edit-site-visual-editor__editor-canvas');
	let _document = document;
	
	// check for block editor iframes
	for(let i = 0; i < iframes.length; i++){
		
		let block = iframes[i].contentDocument.getElementById('block-' + props.clientId);
		if(block !== null){
			_document = iframes[i].contentDocument;
			break;
		}
	}
	
	return _document;
}

This solution is relatively stable combined with a random ID generator. Our IDs look like this: MTY5ZDgyN2I2

There is still a chance of getting duplicates if you combine block content of two or more post types (like pages in sidebars or something like that), but with random IDs the chance is relatively low and duplicates in the same page get replaced on creation.

Sounds interesting, would be nice to see it in context, like in a repo, to see how you're generating and storing the ids and whatnot.

@dennisheiden
Copy link

In our Beta version (which is around 10 months old), we utilized Higher Order Component to inject custom block controls and settings into standard blocks. At the time, this approach allowed us to implement some custom features. However, given the recent updates, we acknowledge that there might be alternate, more suitable approaches available. One challenge we face is generating unique identifiers that we can use as class selectors in the custom CSS. To do this, we've created our own 'static' IDs to ensure absolute uniqueness.

From our helper.js:

export function getUniqueBlockId(props){
	let id = btoa( props.clientId ).replace(/[^a-z0-9]/gi,'');
	
	return id.substr( id.length - 12, 12);
}
export function isDuplicate(props){
	let output = false;
	const _document = getBlockDocumentRoot(props);
	const elements = _document.querySelectorAll('.sv100-premium-block-core-'+props.attributes.blockId);
	
	if(elements.length > 1){
		output = true;
	}
	
	return output;
}

Simple check after mount:

if(!blockId || isDuplicate(props)){
			// replace old block ID with new one if block is a duplicate
			const newBlockId = getUniqueBlockId(props);
		
			setAttributes({ blockId: newBlockId });
		}

@natirivero
Copy link

Sweet @dennisheiden will need to experiment with it. Thanks!

@Julianoe
Copy link

Julianoe commented Dec 13, 2023

This issue is more than 3 years old and as someone that does not work 100% of time on blocks but needs some occasionally I can't find any recommendation or documentation about the proper way to do this.
Is there any stable recommended way to do this?

The use case here is setting "aria-controls=" and "aria-labelledby=" on some sort of tabs blocks that collapses content, and styling, of course.
I've started digging through #25195 but the discussion seems to indicate using clientId is not a good solution.

Do you have any basic in context example that could be use by people trying to implement this? Any advice on the current state of the art on that matter would be very welcome.

@dennisheiden
Copy link

This issue is more than 3 years old and as someone that does not work 100% of time on blocks but needs some occasionally I can't find any recommendation or documentation about the proper way to do this. Is there any stable recommended way to do this?

The use case here is setting "aria-controls=" and "aria-labelledby=" on some sort of tabs blocks that collapses content, and styling, of course. I've started digging through #25195 but the discussion seems to indicate using clientId is not a good solution.

Do you have any basic in context example that could be use by people trying to implement this? Any advice on the current state of the art on that matter would be very welcome.

You can manipulate blocks in two ways: 1) Settings Injection (see my awnser somewhere above) or 2) at the output side (Filter: "render_block").

@peterbode1989
Copy link

This issue is more than 3 years old and as someone that does not work 100% of time on blocks but needs some occasionally I can't find any recommendation or documentation about the proper way to do this. Is there any stable recommended way to do this?
The use case here is setting "aria-controls=" and "aria-labelledby=" on some sort of tabs blocks that collapses content, and styling, of course. I've started digging through #25195 but the discussion seems to indicate using clientId is not a good solution.
Do you have any basic in context example that could be use by people trying to implement this? Any advice on the current state of the art on that matter would be very welcome.

You can manipulate blocks in two ways: 1) Settings Injection (see my awnser somewhere above) or 2) at the output side (Filter: "render_block").

The filter for render_block should be avoided for attribute-injection. I tried this for the attribute ID.
Setting the ID through render_block required me to convert the dom-element to string and back. For something as simple as ID-attribute I really recommend using the save-hook.

What did I do: Made a blank block, with clientId as saved-attribute. Then in the render_block set this ID to the dom. Copy it 1000x times and load the page. You'll see a increase in page load time. For one block: yes, multiple NO!

@nisception
Copy link

nisception commented Sep 25, 2024

The filter for render_block should be avoided for attribute-injection. I tried this for the attribute ID. Setting the ID through render_block required me to convert the dom-element to string and back. For something as simple as ID-attribute I really recommend using the save-hook.

What did I do: Made a blank block, with clientId as saved-attribute. Then in the render_block set this ID to the dom. Copy it 1000x times and load the page. You'll see a increase in page load time. For one block: yes, multiple NO!

Why would you see an increase in page load time if you do this server side?
Running over 1000 blocks is not that slow and HTML Output should be cached anyway.

Our way of injection an unique id as class (sv100-premium-block-core-MGJlMDUyNWM1) :

add_filter('render_block', array($this, 'render_block_overwrite'), 99, 2);

public function render_block_overwrite(string $block_content, array $block): string{
	$html = $block_content;
	
	// add extra props to dynamic blocks
	if ( $block_content && isset( $block['attrs']['blockId'] ) ) {
		// add blockId class
		$injected_class = 'sv100-premium-block-core-' . $block['attrs']['blockId'];
		
		if(strpos($html, $injected_class) === false){ // prevent duplicates
			$html = preg_replace(
				'/' . preg_quote( 'class="', '/' ) . '/',
				'class="' . esc_attr( $injected_class ) . ' ',
				$block_content,
				1
			);
		}
		
	}
	
	return $html;
}

But I bet there is a much smarter way now, our code is old and we never checked again, because it works.

@peterbode1989
Copy link

peterbode1989 commented Sep 25, 2024

The filter for render_block should be avoided for attribute-injection. I tried this for the attribute ID. Setting the ID through render_block required me to convert the dom-element to string and back. For something as simple as ID-attribute I really recommend using the save-hook.
What did I do: Made a blank block, with clientId as saved-attribute. Then in the render_block set this ID to the dom. Copy it 1000x times and load the page. You'll see a increase in page load time. For one block: yes, multiple NO!

Why would you see an increase in page load time if you do this server side? Running over 1000 blocks is not that slow and HTML Output should be cached anyway.

Our way of injection an unique id as class (sv100-premium-block-core-MGJlMDUyNWM1) :

add_filter('render_block', array($this, 'render_block_overwrite'), 99, 2);

public function render_block_overwrite(string $block_content, array $block): string{
	$html = $block_content;
	
	// add extra props to dynamic blocks
	if ( $block_content && isset( $block['attrs']['blockId'] ) ) {
		// add blockId class
		$injected_class = 'sv100-premium-block-core-' . $block['attrs']['blockId'];
		
		if(strpos($html, $injected_class) === false){ // prevent duplicates
			$html = preg_replace(
				'/' . preg_quote( 'class="', '/' ) . '/',
				'class="' . esc_attr( $injected_class ) . ' ',
				$block_content,
				1
			);
		}
		
	}
	
	return $html;
}

But I bet there is a much smarter way now, our code is old and we never checked again, because it works.

Everytime you request a page, the code runs. So that adds to the time before the server gives a page response. And by just saying let the server/cache handle it, is in my opinion not a perfect solution. Since 90% of all Wordpress installs are done with shared-hosting.

Currently I haven't found a workaround for the problem.
My current code looks similair to the code you just shared. If I find something "decent" I'll make sure to share it.

@nisception
Copy link

I understand doing stuff on runtime is not "perfect". That's negated by using cache - code is only executed once when the cache gets filled (hosting solution doesn't really matter IMHO). I also doubt the impact is significant. We've had no issues so far doing this un-cached on complex pages (the class is added to every block you insert). It could be slightly improved maybe by using different replacer functions, etc. (But I open to performance / test data if you have some.)

Back to unique IDs:

  • ids are set through "anchor" attribute - which is not a good idea -> user can edit this in the editor / not supported on all blocks
  • unique ids could be added to the classNamesList of the block, but I remember that this had some issues when rendering the block back then (reason for the php filter use)
  • alteranative would be registering a custom data attribute like "data-element-id" or "key" and let Gutenberg render that into the element / block ouput -> don't know if this is possible right now

@peterbode1989
Copy link

Currently (still need to do a lot of testen), got a somewhat working "solution".
So basically I sets a className in the back-end. This is used to check for duplicates.
And in the save-fn the blockId is added as a default ID-attribute for the front end to be used.

I modified the ID checking functions (this idea /method was mentioned above):

export function isDuplicate({ attributes }) { const { blockId } = attributes const nodes = document.querySelectorAll(`.editor-styles-wrapper .block-${blockId}`) return nodes.length > 1 }

Edit.js contains:

React.useEffect(() => { if (!blockId || isDuplicate(props)) { setAttributes({ blockId: clientId }); }

const blockProps = useBlockProps({ className: `block-${blockId}`, });

Save.js contains:
Return ( <div id={`block-${blockId}`} {...blockProps}> <InnerBlocks.Content /> </div> )

@peterbode1989
Copy link

* ids are set through "anchor" attribute - which is not a good idea -> user can edit this in the editor / not supported on all blocks

I agree, for a personal / closed project this would be fine. But not for a production-application.

* unique ids could be added to the classNamesList of the block, but I remember that this had some issues when rendering the block back then (reason for the php filter use)

True, this was/is very unstable. And you mean triggering Block Recovery mode?

* alteranative would be registering a custom data attribute like "data-element-id" or "key" and let Gutenberg render that into the element / block ouput -> don't know if this is possible right now

This is somewhat possible. If the value is immutable, then I should work (currently trying/testen this with setting a id on the save-hook.

Also, this is what I did first. Just update the blockID with the value of clientID every page-load (back-end). This resulted in having the save option enabled. This was in my opinion not a good solution. Since the IDs of the front-end blocks constantly change.

@nisception
Copy link

nisception commented Sep 26, 2024

Interesting input, might check that on a custom block in some time.

Yes clientID is not static. Generating your own IDs is necessary and checking against duplicates. BUT, there might be some more issues:

  • handle "duplicate block" events
  • handle inserts of global blocks / patterns or anything that duplicates blocks (page duplication?)
  • server side rendered blocks

Or maybe just run with the clientID like Gutenberg does it for inline styles and refresh the ID on save?

@remcotolsma
Copy link

remcotolsma commented Sep 26, 2024

We were developing a form block with various field blocks and aimed to implement conditional logic functionality, similar to the systems found in plugins like Gravity Forms or Advanced Custom Fields:

Gravity Forms

Scherm­afbeelding 2024-09-26 om 14 01 37

Advanced Custom Fields

acf-settings-conditions


We wanted to store the conditional logic rules directly within the attributes of the form field blocks. These rules would contain references (IDs) to other fields, indicating that the visibility the field block depends on the values of these referenced field blocks.

We ran into the challenge that when duplicating blocks there were fields with the same IDs and conditional logic was also applied to wrong field blocks. If block IDs would change every time this would be difficult for referencing blocks.

We therefore leaned towards using the standard ID/anchor functionality within the block editor. These IDs are also visible in the "Document Overview".

Scherm­afbeelding 2024-09-26 om 14 12 29

For now we have moved away from the conditional logic functionality, we also wondered whether this should be arranged at block level. Maybe something to keep in mind, in any case good to see that there are multiple users working on this. 💪

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. [Type] Enhancement A suggestion for improvement.
Projects
None yet
Development

No branches or pull requests

10 participants