tiptap
tiptap copied to clipboard
Event on node deleted
What problem are you facing?
Would it be possibile for tiptap to emit events when a node is deleted altogether? I'm trying to track image deletions but it looks like i have to observe the markup and match tags to discern whether a tag has been deleted by the user.
What’s the solution you would like to see?
it would be cool to have a "deleted" or "removed" event that only takes into account deleted nodes.
What alternatives did you consider?
Matching generated markup by regex, but it's cumbersome.
Anything to add? (optional)
No response
Are you sponsoring us?
- [ ] Yes, I’m a sponsor. 💖
I'm also interested in this. I have some interactive node views where I need to perform some cleanup when they're deleted.
Currently planning to track all node ids myself and diff them onUpdate, so I can detect node deletions, but that's a bit cumbersome.
If tiptap had an onNodeDeleted
event, that would be perfect. Or if someone finds another way to implement this, please share here!
I'm also interested in this. I have some interactive node views where I need to perform some cleanup when they're deleted.
Currently planning to track all node ids myself and diff them onUpdate, so I can detect node deletions, but that's a bit cumbersome.
If tiptap had an
onNodeDeleted
event, that would be perfect. Or if someone finds another way to implement this, please share here!
Same problem.
And I found a response from this discuss react to node adding/removing. Maybe we can just pick the first approach before tiptap support that event (if they are going to implement this).
Btw, when going with the first approach, remember tiptap export some useful helpers as well, like findChildren
, which can save us some time.
I came up with a solution that works well for me, happy to share here.
I use an onUpdate
handler to diff nodes and emit onNodeDeleted
events that can handle whatever cleanup you like. I wrote a separate extension to add unique id
attrs to nodes, which are used here. You could probably use the TipTap UniqueId extension to do this, or find some other way to compare nodes across edits.
Hopefully helpful to someone!

I came up with a solution that works well for me, happy to share here.
https://gist.github.com/stevecastaneda/eb50aed3e5903aac2995f7cc850f71b1
Here's a gist of the above to make it easier to copy/paste, including imports. This works great for me.
There is an onDestroy event that exists on all node views. I'm wondering how hard it is to tie into that, even without "first class" support from TipTap for this
@stevecastaneda, since Tiptap gives you transaction.before
in the onUpdate
, I think this approach would be more robust and cleaner:
useEditor({
...
onUpdate: ({ transaction }) => {
const nodeIds = new Set<string>();
transaction.doc.forEach((node) => {
if (node.attrs.id) {
nodeIds.add(node.attrs.id);
}
});
transaction.before.forEach((node) => {
if (node.attrs.id && !nodeIds.has(node.attrs.id)) {
onNodeDeleted(node);
}
});
},
});
There might be even better ways that avoid looping twice over doc
, but I am not very familiar with transactions yet.
@stevecastaneda, since Tiptap gives you
transaction.before
in theonUpdate
, I think this approach would be more robust and cleaner:useEditor({ ... onUpdate: ({ transaction }) => { const nodeIds = new Set<string>(); transaction.doc.forEach((node) => { if (node.attrs.id) { nodeIds.add(node.attrs.id); } }); transaction.before.forEach((node) => { if (node.attrs.id && !nodeIds.has(node.attrs.id)) { onNodeDeleted(node); } }); }, });
There might be even better ways that avoid looping twice over
doc
, but I am not very familiar with transactions yet.
Thanks for the nice snippet! For someone struggling with setup, it's probably because you don't have UniqueID extension. Sharing my implementation in case it helps someone.
//install and import unique-id extension
import UniqueID from "@tiptap-pro/extension-unique-id";
const handleImageDeletes = async (transaction: Transaction) => {
let current: Node[] = [];
transaction.doc.content.forEach((node) => {
if (node.attrs.uid) {
current.push(node);
}
});
let before: Node[] = [];
transaction.docs.forEach((doc) =>
doc.content.forEach((node) => {
if (node.attrs.uid) {
before.push(node);
}
})
);
if (!current || before.length == 0) {
return;
}
const deletedImageNodes = before.filter((node) => {
const uid = node.attrs.uid;
return !current.find((curNode) => curNode.attrs.uid == uid);
});
if (deletedImageNodes.length > 0) {
// do stuff
}
};
const editor = useEditor(
{
onTransaction({ transaction }) {
handleImageDeletes(transaction);
},
extensions: [
//... other extensions
UniqueID.configure({
attributeName: "uid",
types: ["imageBlock"],
}),
// update node types to be included per your needs, for me, I'm only interested in imageBlock changes
],
//... other configs
},
[]
);
Does anyone have a solution if you do not have tiptap pro?
I think the image node now has a type, so replacing the handleImageDeletes this below is working for me without using the UniqueID plugin.
function handleImageDeletes(transaction) {
const getImageSrcs = (fragment) => {
let srcs = new Set();
fragment.forEach((node) => {
if (node.type.name === 'image') {
srcs.add(node.attrs.src);
}
});
return srcs;
};
let currentSrcs = getImageSrcs(transaction.doc.content);
let previousSrcs = getImageSrcs(transaction.before.content);
if (currentSrcs.size === 0 && previousSrcs.size === 0) {
return;
}
// Determine which images were deleted
let deletedImageSrcs = [...previousSrcs].filter((src) => !currentSrcs.has(src));
if (deletedImageSrcs.length > 0) {
//Handle deleting file on cloud here
}
}
You can track current and prev images inside array using onUpdate and editor.getJSON( ), and delete images which dosen't match currentImages ex.
onUpdate: async({ editor })=> {
setContent(editor.getJSON());
console.log(editor.getJSON())
const currentImages=[];
editor.getJSON().content?.forEach((item) => {
if (item.type === "image") {
currentImages.push(item.attrs.src);
}
});
const deletedImages = previousImages.filter((url) => !currentImages.includes(url));
for (const url of deletedImages) {
console.log("Deleting image from blob storage:", url);
await fetch(`/api/upload/delete?url=${encodeURIComponent(url)}`, {
method: "DELETE",
});
}
setPreviousImages(currentImages);
setImagesList(currentImages);
console.log(currentImages);
},
add it inside useEditor({extensions:[...],onUpdate.....given code
I think it should work. It doesn't require TipTap pro.