How to override block ID on block creation through copy/paste

242 views
Skip to first unread message

Max Koretskyi

unread,
Oct 11, 2023, 12:30:56 PM10/11/23
to Blockly
I'm binding some meta information to block ids. Hence when I create the blocks I assigned each block my custom id:

const customBlockId = 'some-custom-id';

this.workspace.newBlock('kw-literal_text', customBlockId);

or when I load them:

const block = {
id: customBlockId,
type: 'my-block'

Blockly.serialization.workspaces.load(block, this.workspace);

Today I discovered that there's a 3rd way to create a block, which is by COPY/PASTE keyboard functionality. In this case in the event BLOCK_CREATE I get the block that already has some ID assigned:

case Blockly.Events.BLOCK_CREATE: {
console.log(e.blockId); // some id assigned by the library
break;
}
Is there any way to override it either before or after the creation process?

Lynn Duke

unread,
Oct 11, 2023, 3:03:30 PM10/11/23
to Blockly
Hi Max,

I implemented this little hacky tacky chunk of code. I didn't find another way myself and haven't revisited. 

// Some blocks need custom IDs generated for them, for example, the choose block.
// Generate a new question nanoid on create (including handling the case
// where a block is duplicated and we can't keep the same question id it already has)
// We have to look at all descendants of block being created as well
if (event.type == Blockly.Events.BLOCK_CREATE) {
const block = workspace.getBlockById(event.blockId)
if (block) {
const allDescendantBlocks = block.getDescendants(false)
allDescendantBlocks.forEach((d) => {
if (d.type == 'choose') {
;(d as any).questionId = safeNanoid()
}
if (d.type == 'choice') {
;(d as any).answerId = safeNanoid()
;(d as any).tag = ''
}
})
}
}

Lynn Duke

unread,
Oct 11, 2023, 5:38:26 PM10/11/23
to Blockly
Max, I just realized that the BLOCK_CREATE fires for blocks that come back after undoing a delete. The extra state from the mutator I added, in this case "questionId", "answerId" etc will not be set!

To reproduce.
1) Add a block 
2) Make sure you set your extra state
3) Delete the block
4) CTRL-Z to undo and get the block back.

Note that the fields returned with their info (if you have any) but the questionId or answerId extra state is empty so it regenerates new values here (safeNanoid()). That breaks me.

My mutator for the choice block looks like this btw:

import * as Blockly from 'blockly'
import { safeNanoid } from '../../../../utils/SafeNanoid'

const choiceMutator = {
answerId: null,
tag: null,

saveExtraState: function () {
return {
answerId: this.answerId,
tag: this.tag,
}
},

loadExtraState: function (state) {
this.answerId = state['answerId'] ?? safeNanoid()
this.tag = state['tag'] ?? ''
},
}
if (!Blockly.Extensions.isRegistered('choice_mutator')) {
Blockly.Extensions.registerMutator('choice_mutator', choiceMutator)
}



I don't want to hijack your question Max, but in case you go my route, you'll run into this too.

Lynn Duke

unread,
Oct 11, 2023, 5:56:09 PM10/11/23
to Blockly
Max, I lied. My bug. The extra state is there on CREATE from an undo, I was just blowing it away. The line should check for the presence of the state.

 d.answerId = d.answerId ?? safeNanoid()

Okay,  bedtime for me. :D

Mark Friedman

unread,
Oct 11, 2023, 7:16:22 PM10/11/23
to blo...@googlegroups.com
I think that you can solve this issue by putting your answerId and tag state on the block's data property.  That way it should get restored when the block is pasted. Then, in your mutator functions, you can check whether they exist and reuse them if so (and create them, if not).

Hope this helps.

-Mark


--
You received this message because you are subscribed to the Google Groups "Blockly" group.
To unsubscribe from this group and stop receiving emails from it, send an email to blockly+u...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/blockly/79bb2398-7615-4f1d-88b9-42505f7a05e8n%40googlegroups.com.

Max Koretskyi

unread,
Oct 12, 2023, 2:47:00 AM10/12/23
to Blockly
Thanks guys, I already use a mutator and extraState, but specifically for blockId, keeping it in the extraState is a workaround which I'd like to avoid.

Max Koretskyi

unread,
Oct 12, 2023, 10:16:25 AM10/12/23
to Blockly
The problem is that even if you put custom ID in the custom `._data` property, on copy/paste Blockly still copies this structure with the custom ID inside. So the only way for us to detect a block that was created through copy/paste and not created by us is to compare the block ID and the id stored in the metadata:

// we create blocks and assigned them custom ID and also put that ID in the metadata of the block
block = this.workspace.newBlock('kw-call_expression', blockId);
block._data.meta = {id};

// in the event handler
case Blockly.Events.BLOCK_CREATE: {
    const block = this.workspace.getBlockById(e.blockId);
      // check if block's ID and id in the metadata matches
      const blockCreatedOnCopy = (bl) => bl.id !== bl._data.meta.id;

      // if the block is created on copy, we need to create new IDs and register them for children
      if (blockCreatedOnCopy(block)) {
          visitBlocks(block, blocksWithAttachedMeta, (b) => {
        // we expect this always to be true
        if (blockCreatedOnCopy(b)) {
            const id = this.registerBlock(b._data.meta.id, b.type);
            b._data.meta = {id};
        } else {
            console.error('Somehow the Id in the metadata of the child block wasn't copied?');
        }
    });
}

Beka Westberg

unread,
Oct 12, 2023, 12:24:45 PM10/12/23
to Blockly
Hello,

So it sounds like what you want to do is associate some data with a block, and you want this to be unique for each block?

If that is the case, what I would do is instead of associating the data with a custom block ID, add extra serialization to your block. In your `loadState` method you can check if the data is already associated with a block, and create new data instead of loading the existing data. The `loadState` method gets called on copy paste, undo, deserialization, etc, so you can be sure it always gets called.

But if I'm misunderstanding something about your use case let me know and I can try to find an alternative suggestion!

Best wishes,
--Beka

Max Koretskyi

unread,
Oct 12, 2023, 1:06:52 PM10/12/23
to Blockly
> So it sounds like what you want to do is associate some data with a block, and you want this to be unique for each block?

yes, correct, unique for each instance of the block

> add extra serialization to your block. In your `loadState` method you can check if the data is already associated with a block, and create new data instead of loading the existing data

Do you mean `loadExtraState` method? Could you give an example how it can be used for my purpose? 
I put a debugger into `loadExtraState` and the function's argument  `state` already has some data prefilled, presumably by what's returned from ` saveExtraState ` in the existing block.

I already use this method to keep some information related the block, but this information is defined by the block's itself, not external information that is bound through the block's ID:

const callExpressionMutator = {
    _data: null,
      inputsCount: 0,

setData(data) {
      this._data = data;
      this.inputsCount = data.inputsCount ?? 0;
      this.rebuildShape_();
},

saveExtraState: function (this: any) {
      return {
          ...this._data,
        inputsCount: this.inputsCount,
    };
},

loadExtraState: function (this: any, state) {
      this._data = state;
      this.inputsCount = state['inputsCount'] || 0;
      this.rebuildShape_();
},

Beka Westberg

unread,
Oct 13, 2023, 12:33:47 PM10/13/23
to Blockly
Hello,

This will probably look a bit different depending on how your modules (i.e. scoping) are set up  but this is generally what I was thinking:

```
// You don't have to use a set, this is just one way of tracking whether your
// data is already associated with a block or not
const claimedData = new Set();

loadExtraState: function(this: any, state) {
  if (!claimedData.has(state.data)) {  // This is the important bit. Check if you need new data.
    this.generateData();
  } else {
    this.data = state;
  }
  claimedData.add(this.data);
}
```

Hopefully the general gist of that makes sense! I don't think I understand your use case enough to use less abstract terms :/

Happy to keep chatting about this more!
--Beka

Max Koretskyi

unread,
Oct 13, 2023, 1:58:42 PM10/13/23
to Blockly
Thanks for an eloborate example, but I'm still a bit confused. Where does this `state.data` come from to the `loadExtraState` method? And what is supposed to be the unique identifier of the data? By using `claimedData.has(state.data)`, it looks like it should be object reference, but why is this object's reference will be different for the cloned block? I guess this confusion also relates to me now understanding where this `state` comes from into the `loadExtraState` method

Beka Westberg

unread,
Oct 13, 2023, 2:47:00 PM10/13/23
to blo...@googlegroups.com
Hello,

Sorry for the confusion :/ It's difficult for me to give a more explicit and reply when I'm not sure what data you're associating with your blocks.

>  Where does this `state.data` come from to the `loadExtraState` method?

`loadExtraState` receives anything in the `extraState` property of your JSON. If you're manually constructing your JSON, it's whatever you manually stuck in there. If you're serializing an existing block, it's whatever is returned by `saveState`.

So for example, your json could look like:
```
blocks: [
  {
    "type": "my_block_type",
    "extraState": {  // This gets assigned to the `state` parameter.
      "data": "some arbitrary data string",
    }
  }
]
```

But it could also look like:
```
blocks: [
  {
    "type": "my_block_type",
    "extraState": {  // This gets assigned to the `state` parameter.
      "randomProperty1": "some arbitrary data string",
      "randomProperty2": ["some", "arbitrary", "data"],
    }
  }
]
```

It just has to be JSON serializable! E.g. no functions.

> And what is supposed to be the unique identifier of the data? By using `claimedData.has(state.data)`, it looks like it should be object reference, but why is this object's reference will be different for the cloned block?

It was just meant to be a stand in for any unique key you could use to identify your data. I maybe should have called it `state.uniqueKey` instead of `state.data` :P

I hope that helps! If you have any further questions please reply!
--Beka

Max Koretskyi

unread,
Oct 15, 2023, 7:43:06 AM10/15/23
to Blockly

Thanks :)

>  `loadExtraState` receives anything in the `extraState` property of your JSON.

Right, and I'm already using it for some properties like inputsCount in the IF custom block. But that covers the case when the blocks are created through loading JSON. For this case my approach works OK. My struggle is particularly with the case of when a block is created by COPY/PASTE. In the case of copy/paste, what does the new cloned block receive into `loadExtraState`? Where does this data come from? Because right now I get exactly the same set of data that I put into the original block from which the copy is created. And that creates problems, because for the newly created/cloned block I need to generate new set of data.

Lynn Duke

unread,
Oct 15, 2023, 3:04:07 PM10/15/23
to Blockly
@Max, @Beka same here. If I copy paste my block with extra state already set. My new block will have a duplicate of the same extra state. That makes sense really. But, if you're like us trying to put new unique ids in blocks, it'll fail our systems. We might need to maintain a map of "custom unique id" to block id. And if a second or more block id comes in with the same "custom unique id", then replace the "custom unique id" on the spot.

Beka Westberg

unread,
Oct 16, 2023, 11:42:26 AM10/16/23
to Blockly
Hello again!

Yes, this is a relatively common problem =) We actually run into the same thing with the shareable procedure blocks, and we've solved it in the way that I suggested above. That is, in your `loadState` function, check if the data is already associated with (or "claimed by") a block, and generate new data if so.

> what does the new cloned block receive into `loadExtraState`?

Copy and paste uses the serialization system (as seen here). So it gets the same data returned from `saveExtraState` as it would when serializing and deserializing a workspace.

---

Since this worked for the shareable procedure blocks, I think it should work for your case as well! But it might be easier to discuss if I have a more explicit idea of what you're doing. Right now I'm using general terms like "data" and "claimed", which I think might be causing confusion :P If you could let me know exactly what you're trying to do that would be really helpful!

Best wishes,
--Beka

Mark Friedman

unread,
Oct 16, 2023, 4:10:55 PM10/16/23
to blo...@googlegroups.com
Lynn and Max,

  I think I'm getting your issue here.  Please let me know if I am still misunderstanding.

  You want to associate a unique ID with each block which is different from the ID that Blockly creates for you. Presumably, you generate this ID in the init method for the block and store it via either the saveExtraState function or in the data property of the block.  All is well and good, until there is a copy/paste operation, because now you end up with a new block that contains the same (non-Blockly) ID as the copied block.  Do I have that right?

  If so, Lynn's solution (which Beka also mentions) seems like a reasonable one.  Another one might to also store the original Blockly generated ID property as well as your generated ID. Then, in your loadExtraState function (or BLOCK_CREATE event handler) you could check to see if the block's current Blockly generated ID is the same as its original Blockly generated ID.  If it isn't, then that indicates that it is a copy-pasted block and that you need to generate a new custom unique ID.

  I'll just note that if you're already using saveExtraState/loadExtraState then it would make sense to just the check I mentioned above in your loadExtraState function.  However, if you don't otherwise need saveExtraState/loadExtraState then it might be simpler for you to just save the original Blocky generated ID and your custom ID on the data (not _data) property, since that property is automatically serialized and deserialized by Blockly, and then do the check in a BLOCK_CREATE event handler.

  Hope this helps.

-Mark


Reply all
Reply to author
Forward
0 new messages