WordPress / gutenberg

The Block Editor project for WordPress and beyond. Plugin is available from the official repository.
https://wordpress.org/gutenberg/
Other
10k stars 4.02k forks source link

Block API: Allow for internal, non-duplicable block attributes #29693

Open stacimc opened 3 years ago

stacimc commented 3 years ago

What problem does this address?

Blocks cannot use attributes to store information that is unique internal to the block instance but also persisted across page loads. Here's an illustrative example:

I'm working on the Jetpack Pay with PayPal block. It contains a productId attribute which uniquely identifies the actual product record referenced by the block. When a new block is inserted this attribute will be undefined; we hit the API to create a new product, and store the returned id in this attribute.

When the block is duplicated, I'd like to create a new product record with identical fields (title/price/etc). But because the productId is cloned along with all the other attributes, both blocks will point to the same resource. It's possible to keep track of the block's unique clientId and reset the product when this value changes — but while this detects block duplication, it also changes every time the post loads, and I only want to reset the product on duplication.

What is your proposed solution?

A couple of options as a starting point for discussion:

  1. Add support for unique internal attributes, which do not get copied during block duplication.
  2. Allow blocks to listen for/handle the duplication event.
Tropicalista commented 3 years ago

I have similar problem.

Here's my issue: https://github.com/WordPress/gutenberg/issues/29694

I'm creating a form builder. I need a way to store reference to form id in database. When a block is made reusable it stores the reference, but if a user convert to normal block I need a way to detach the old reference in DB and create a new one.

I've just discovered that there's a new __experimentalIsEditingReusableBlock that probably can suite for my use case.

talldan commented 3 years ago

Also related is the difficulty in declaring and using unique HTML ids within a block - https://github.com/WordPress/gutenberg/issues/17246

ntsekouras commented 3 years ago

I've been thinking about this and a possible solution I had in mind to try out involves these:

  1. Set the unique attribute value on load only if it doesn't exist and mark this change as persistent or not depending on your needs (add undo level). Something like this: https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/query/edit/index.js#L94
  2. Use the __experimentalRole attribute to something like id or unique etc.., and then use the existing __experimentalGetBlockAttributesNamesByRole util to get them.
  3. Handle them in the appropriate places, like in the duplicate function and where is needed.

All the above are using experimental APIs though which were introduced here: https://github.com/WordPress/gutenberg/pull/30469. I guess this could be okay though as the new functionality should also be experimental for start.

talldan commented 3 years ago

This could also be relevant for the widget editors, which store an internal widgetId as an attribute. (cc @kevin940726 @noisysocks).

gziolo commented 2 years ago

This could also be relevant for the widget editors, which store an internal widgetId as an attribute. (cc @kevin940726 @noisysocks).

The related PR https://github.com/WordPress/gutenberg/pull/28379. We discussed the potential new APIs for block attributes with @spacedmonkey on Twitter. Related thread:

https://twitter.com/thespacedmonkey/status/1435530281595846663

Well this is a question. I think they could be two kind of attribute. Internal and private/sensitive. Internal attributes, could used for data that is not sensitive but not design to be public. Private / sensitive, like api keys, personal information (like email) or passwords.

The conclusion in the discussion is that "internal" attribute is more something stored in the memory during the block editor's runtime and __internalWidgetId from the linked PR would be a good example. Here, @stacimc desribes the use case where an attribute (productId) should be persisted between the page edits but doesn't necessarily need to be secret. So I have a feeling that it's something between the shared definition of internal and private/sensitive 😄 Well, unless we introduce three types:

noisysocks commented 2 years ago
  • secret - persisted in secure location, not shared when duplicating

Not sure I understand this. What would a "secure location" be?

gziolo commented 2 years ago
  • secret - persisted in a secure location, not shared when duplicating

Not sure I understand this. What would a "secure location" be?

I don’t know yet, too 😅 I understood that the value couldn’t be serialized to content as HTML, so it isn’t easily exposed. I also assume it should live somewhere in the database. I was trying to document potential options and their subtle differences.

stacimc commented 2 years ago

I've just opened #34750 borrowing the internal terminology from @gziolo 😄

I think this issue may be trying to do too many things, largely because of my use of the loaded term unique. To solve my own original use case, it's sufficient to have a way to prevent an attribute from being copied on block duplication. internal feels like a superior descriptor.

Per @ntsekouras I like internal as an __experimentalRole for an attribute, rather than a boolean property. I'm interpreting the internal role strictly as: "attribute does not get copied on block duplication", rather than adding any additional constraints of uniqueness/etc.

I think this would go a long way to addressing many of the use cases described here, excluding the private or secret ideas. I'd like to edit the title and description of this issue slightly to replace the loaded unique terminology, and perhaps open a separate issue to track the desire for private/secure attributes, if that makes sense to others here. What do you think?

gziolo commented 2 years ago

I've just opened #34750 borrowing the internal terminology from @gziolo 😄

Thank you for a quick follow-up 👍🏻

I think this would go a long way to addressing many of the use cases described here, excluding the private or secret ideas. I'd like to edit the title and description of this issue slightly to replace the loaded unique terminology, and perhaps open a separate issue to track the desire for private/secure attributes, if that makes sense to others here. What do you think?

Feel free to edit the issue title and description according to the current direction of the PR you opened. It sounds like a good idea to open another issue to continue the discussion for other common scenarios that block authors have to deal with.

Tropicalista commented 1 year ago

Any update on this?

albanyacademy commented 1 year ago

Does copy+paste count as duplication event?

noisysocks commented 3 months ago

Some other use cases for an API such as this were discussed in https://github.com/WordPress/gutenberg/issues/23377.

anver commented 2 weeks ago

Any solution to this issue ?

I’m creating a custom block and one of it’s attribute is a unique id which doesn’t have any UI, so that’s not editable by the user. It’s created automatically by useEffect hook on the component startup if that attribute is not set. If I duplicate the block then the attribute is copied too which makes a duplicate id. Is there any way so that I can detect if a block is being duplicated and generate a unique id and overwrite the duplicate id with the generated one programmatically ?

ntsekouras commented 2 weeks ago

Any solution to this issue ?

Unfortunately not yet. Your use case is exactly the same with Query Loop block and queryId.

acketon commented 2 weeks ago

@anver I had a similar problem for our Accordian block. We needed to generate unique ID's for use on the frontend. These were needed for aria labels and some frontend JS, etc. I put together a work around that appears to work. It's hacky but I think it will suffice for our needs for now. It's based on some great discussion here in a few issues and on what was done in the core heading block for generating anchors.

Basically uses useEffect() combined with searching all the blocks in the editor to check for a duplicate id stored as a data attribute. If a duplicate is found it generates a new unique ID. That seems so far to work when a block is duplicated or copy/pasted, etc. https://github.com/bu-ist/bu-blocks/issues/355.

Example: https://github.com/bu-ist/bu-blocks/pull/356

anver commented 2 weeks ago

@anver I had a similar problem for our Accordian block. We needed to generate unique ID's for use on the frontend. These were needed for aria labels and some frontend JS, etc. I put together a work around that appears to work. It's hacky but I think it will suffice for our needs for now. It's based on some great discussion here in a few issues and on what was done in the core heading block for generating anchors.

Basically uses useEffect() combined with searching all the blocks in the editor to check for a duplicate id stored as a data attribute. If a duplicate is found it generates a new unique ID. That seems so far to work when a block is duplicated or copy/pasted, etc. bu-ist/bu-blocks#355.

Example: bu-ist/bu-blocks#356

@acketon Thanks for the right direction, i had to change few things on that code to make it work, but i had to ditch that completely and go for a reliable solution, finally I found a solution :) Thx for the comments bro.

const getBlockCount = () => select( 'core/block-editor' ).getGlobalBlockCount();
let blockCount = getBlockCount();

    useEffect( () => {
        const unsub = subscribe( () => {
            const newCount = getBlockCount();

            if ( newCount === blockCount ) return;

            blockCount = newCount;

            const wrapper = select( 'core/block-editor' ).getBlock( clientId )!;

            const formBlocks = wrapper.innerBlocks.filter(
                ( block ) => block.name === 'surveyboss/form'
            );

            const duplicates = formBlocks.filter( ( obj, index, arr ) =>
                arr.find(
                    ( innerObj ) =>
                        innerObj.attributes.formUuid ===
                            obj.attributes.formUuid &&
                        innerObj.clientId !== obj.clientId
                )
            );

            if ( duplicates.length <= 1 ) return;

            dispatch( 'core/block-editor' ).updateBlockAttributes(
                duplicates[ 1 ].clientId,
                { formUuid: generateId() }
            );

            dispatch( 'core/block-editor' ).updateBlockAttributes(
                duplicates[ 1 ].clientId,
                {
                    metadata: {
                        ...duplicates[ 1 ].attributes.metadata,
                        name: 'Untitled Form',
                    },
                }
            );
        } );

        return unsub;
    }, [ clientId ] );

This is the part of the code i implemented and it just works fine. You got to put the effect in the parent of the duplicate components you are working with.