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

API Question: Copy / Paste Handler Filter for Blocks #23377

Closed
elliotcondon opened this issue Jun 23, 2020 · 6 comments
Closed

API Question: Copy / Paste Handler Filter for Blocks #23377

elliotcondon opened this issue Jun 23, 2020 · 6 comments
Labels
[Feature] Block API API that allows to express the block paradigm. [Type] Help Request Help with setup, implementation, or "How do I?" questions.

Comments

@elliotcondon
Copy link

Hi team,

A quick technical question for you: Is there a way [via the Block API] to customize a block's props.attributes during the copy and past event handler?

For example, a developer may want their block instance to contain a unique identifier. If a block is copied & pasted (or duplicated), the props.attributes will also be duplicated causing two blocks to share the same identifier. 👉 This is just an example, and not a request for clarity on props.clientId.

Perhaps there is a filter, or a way to hook in somewhere and customize the data? I've searched through the plugin source code but haven't found anything obvious yet.

Thanks in advance.

@annezazu annezazu added [Feature] Block API API that allows to express the block paradigm. [Type] Help Request Help with setup, implementation, or "How do I?" questions. labels Jun 30, 2020
@mcsf
Copy link
Contributor

mcsf commented Jul 1, 2020

Hi!

This is an interesting question. I'll try to answer piece by piece:

Is there a way [via the Block API] to customize a block's props.attributes during the copy and past event handler?

Short answer: no, there isn't. The closest would be the 'raw' transform when pasting HTML, but when pasting actual blocks these are accepted verbatim, minus their ID.

Perhaps there is a filter, or a way to hook in somewhere and customize the data?

One way to look at this is to invert the relationship: rather than setting up a filter to automatically change/strip certain data, can the block's edit component be responsible for making sure the block data is good?

Before I explain what I mean by the above, let's discuss this one first:

For example, a developer may want their block instance to contain a unique identifier. […] This is just an example, and not a request for clarity […]

I did my best not to focus on the example at hand :) but I think it's a little unavoidable, because use cases inform our needs. Block attributes really are meant to be an expression of what the block is, and every attribute should be essential to how the block looks and behaves. Under this light, it's hard not to think of identifiers as something different. And, indeed, Gutenberg's own clientId is a part of props and not props.attributes, as you pointed out. It's also not by accident that clientId is never permanent, only a random and ephemeral value to identify blocks while they exist in memory.

Here is the use case that I found that might be the closest to an ID but remain a "meaningful" block attribute: the optional HTML anchor for an element.

So, for the sake of completeness, I changed the Edit component for Heading so that it got rid of the anchor attribute if the same value is already in use:
const isAnchorUnique = useSelect(
	( select ) =>
		select( 'core/block-editor' )
			.getBlocks()
			.filter(
				( { attributes: { anchor: otherAnchor } } ) =>
					otherAnchor !== undefined && otherAnchor === anchor
			).length <= 1
);
useEffect( () => {
	if ( isAnchorUnique ) {
		return;
	}
	console.log(
		'Heading anchor', anchor, 'is already in use. Unsetting value…'
	);
	setAttributes( { anchor: undefined } );
}, [ anchor ] );

This "works" but actually doesn't, because it necessarily breaks Undo. Indeed, setAttribute must always be a consequence of a user action, never an automated action (see more on the "undo trap").

However, it does provide some insight on the problem at hand: let the user know that there is a duplicate anchor and ask the them to fix this. It's the same principle as for mis-leveled headings in a document or low-contrast colour combinations — the editor doesn't dare to "fix" automatically, it informs and recommends. This could be extended to HTML anchors by patching block-editor's hooks/anchor.js to show a notice when an anchor is already in use.

So, in the end, I'd like to hear more about the intended use cases. When would you really need this? What are these IDs that need to be stripped, and could they be hinting at an underlying problem? I don't mean to be blocking a possible new API, I just want to be super mindful about how we approach extending APIs. :)

@elliotcondon
Copy link
Author

Hi @mcsf

Thank you for your attention on this 🙌.

You have answered everything quite nicely and the isAnchorUnique code example is great. This approach is exactly how ACF is currently handling unique block attributes when duplicating a block. Although perfect for a "unique per post" solution, the limitation of this approach becomes apparent when wanting a "unique per site" value.

Okay, let me explain with more detail why this is important for ACF, and other 3rd party developers.

There is a strong philosophy used throughout the ACF plugin, derived from WordPress itself, which is the relational concept of Objects and Meta. Each Object (post, term, user) contains a unique ID, which can be used to target the correct Meta data.

To integrate this philosophy (needed for ACF functions to work) into blocks, we opted to give each block a persistent unique ID, which is saved in the props.attributes data. I can respect that this might go "against the grain" of the Block editor's API, but it's a unique requirement to a unique problem and works quite well. So, each block has a unique ID that looks like "block_123jhg24k5" which never changes, allowing us to perform relational data.

I mentioned that we are already using an "is unique" solution in our edit function. This takes care of the conflicts when duplicating a block - easy. The problem we now face is when a user copy's a block from one post and pastes it on another post. Without a filter in place for this event, we can't (in any performant manor) detect that this is actually a new block which requires a new unique ID. A filter on "copy" (to unset the ID) or a filter on "paste" (to generate a new ID) would solve our problem 100%.

Now, for a non "ID" related scenario. Imagine a block type that displays a colorful tile like the CSS-Tricks Popular this month one. The developer expects authors to insert or copy/paste this block around the site quite often and wants to ensure some variety in the gradients colors. Specific filters for "new block", "copy block" and "paste block" would allow a simple way to generate new gradients at the right times (not on every "edit" render).

I hope these examples are enough to get this feature request considered. It would help out a lot of ACF block users 👍.

Thanks
Elliot

@mcsf
Copy link
Contributor

mcsf commented Jul 3, 2020

Thanks for all the details, @elliotcondon!

I think this could be a worthwhile addition. It does raises a few UX questions, especially as far as user expectations and consistency go. For instance, what happens when a user uses the Duplicate action instead of Copy followed by Paste? Actually, this question made me find #20237, which happens to be a useful cross-reference with many parallels with the present issue.

I'm also thinking that, rather than a wide "filter" to intercept certain block actions, a simpler and more effective interface for this might be a new property in block attributes:

{
  align: {
    type: 'string',
  },
  acfId: {
    type: 'string',
    portable: false,
  },

portable is a pretty bad name, but you get the idea. I also thought about "copiable", "transferrable", etc. Having this might allow us to implement something at the level of or closer to createBlock, which might be more effective.

@elliotcondon
Copy link
Author

elliotcondon commented Jul 4, 2020

@mcsf This portable attribute setting is a great idea, and I hope to see it pursued 🙌.

As you mention, the benefit here is a more "universal" approach that covers more use-cases. The downside is that it may require more architecture & strategy as it will have global implications. This ultimately increases the chance of it getting buried in the already large number of feature requests.

Thinking back to the "classic way" of customizing WordPress, how do the Gutenberg development team feel about adding in actions / filters in general? Is there a tendency to avoid them?

I'm a firm believer that the hook system in WordPress is what allowed the development community to thrive and would love to see hooks for all the major events in block editing such as duplicate, copy, paste, insert. I can see these working in a similar way to the editor.BlockEdit or blocks.getSaveContent.extraProps.

Let me know what I can do to help aid this improvement 👍

@mrwweb
Copy link

mrwweb commented Nov 28, 2021

I'd be happy to offer a real-world use case where having a stable unique block ID is extremely helpful: an accordion block.

An accordion block needs an ID for at least two reasons:

  1. Allow anchor links to a specific section on a page
  2. Correctly associate the accordion heading and accordion content via the aria-controls attribute.

While editors certainly can specify a custom ID, I want to automatically provide a unique ID for every accordion block so that using a "pretty" ID is optional. Due to the nature of anchor links, they need to be both unique and stable. And similar to the OP, when copied or duplicated, the IDs need to be regenerated.

This isn't just an issue I've run into but is also mentioned in this accordion block. I also imagine that a Table of Contents block would run into a similar issue in wanting to offer stable page anchors is a high priority.

@noisysocks
Copy link
Member

As @mcsf notes, there's currently no API that nicely solves this use case.

There's discussion about creating such an API in #29693. There's been several proposals but unfortunately none that have gained a broad consensus among contributors. It's a big change to the block API so everyone understandably wants to get it right.

In the meantime you might be able to make use of the same hack that the widgets editor uses which is store the ID in an attribute that isn't defined in the block's block.json. The block editor will ignore such attributes when serialising the block to HTML and copying/duplicating it which makes this a good solution for storing foreign keys.

I'm going to close this help request as answered (though I appreciate it's not a satisfying answer 😀) and let's use #29693 to track the creation of a better API for 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] Help Request Help with setup, implementation, or "How do I?" questions.
Projects
None yet
Development

No branches or pull requests

5 participants