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 · 15 comments
Open

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

talldan opened this issue Aug 29, 2019 · 15 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").

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

7 participants