I am implementing a copy-paste workflow in GoJS where a user can copy a Product Node and place it elsewhere in the diagram. The workflow uses a placeholder node (with a distinct category and template) and, upon paste, replaces it with a copied product node (also with its own category and template).
Issue: After replacing the placeholder with the copied product node, the UI sometimes shows blank node for that area and does not render the expected product visuals based on template, even though the node data appears correct in the model from nodeDataArray.
Attempts to Resolve:
Registered separate templates for both the placeholder and copied product node categories.
Verified that the node data for the copied product node contains all required properties (key, category, image, height, etc.). Even tried with dummy placeholder but it is not appearing.
Ensured the Category string matches exactly between node data and template registration.
Used both single and split GoJS commits for removal and addition of nodes.
Checked that the diagram updates automatically after each commit.
Added visual highlights and watermarks to the copied product node template for debugging.
Confirmed that the copied node appears in model.nodeDataArray after the operation.
Inspected the browser console for errors (none found).
Tried toggling between categories to isolate template and data issues.
Ensured when replacing unique Key is added.
Question: What could cause a newly added node (with correct data and template registration) to appear blank or not render as expected in GoJS? Are there best practices or common pitfalls for handling custom node categories and template updates for replace noda data with a new one?
When a user initiates a copy-paste action, a placeholder node (with a distinct category, e.g., COPYPLACEHOLDER) is first created at the target location.
Upon confirming the paste, the placeholder node is removed and replaced with a new node whose category is set to PRODUCTNODECOPIED.
The copied node is constructed by cloning the original product node’s data and updating its category and positioning attributes to match the target location.
This replacement is typically handled in the service layer, where the node data array is updated—first by removing the placeholder, then by adding the copied node.
The category update happens right before the new node is added to the model, ensuring the correct template is used for rendering.
I hope this makes it clear, let me know in case of other queries.
<!DOCTYPE html>
<html>
<head>
<title>Copy to Placeholders</title>
<!-- Copyright 1998-2025 by Northwoods Software Corporation. -->
</head>
<body>
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:400px"></div>
Double click to insert a copy-placeholder.
A paste will move copied nodes to where there are copy-placeholders, which are removed from the diagram.
If there are not enough copy-placeholders in the diagram, the copied nodes are left where they were copied from.
<textarea id="mySavedModel" style="width:100%;height:250px"></textarea>
<script src="https://cdn.jsdelivr.net/npm/gojs/release/go-debug.js"></script>
<script id="code">
class CustomCommandHandler extends go.CommandHandler {
constructor(init) {
super();
if (init) Object.assign(this, init);
}
pasteFromClipboard() {
const coll = super.pasteFromClipboard();
const placeholders = new go.Set(myDiagram.parts.filter(part => part.category === "PH"));
coll.each(copied => {
if (!(copied instanceof go.Node)) return;
const ph = placeholders.first();
if (ph) {
copied.location = ph.location;
placeholders.remove(ph);
myDiagram.remove(ph);
}
});
return coll;
}
}
const myDiagram =
new go.Diagram("myDiagramDiv", {
commandHandler: new CustomCommandHandler(),
"clickCreatingTool.archetypeNodeData": { category: "PH" },
"undoManager.isEnabled": true,
"ModelChanged": e => { // just for demonstration purposes,
if (e.isTransactionFinished) { // show the model data in the page's TextArea
document.getElementById("mySavedModel").textContent = e.model.toJson();
}
}
});
myDiagram.nodeTemplate =
new go.Node("Auto", { locationSpot: go.Spot.Center })
.bindTwoWay("location", "loc", go.Point.parse, go.Point.stringify)
.add(
new go.Shape({ fill: "white" })
.bind("fill", "color"),
new go.TextBlock({ margin: 8 })
.bind("text", "", data => `${data.text} (${data.key})`)
);
myDiagram.nodeTemplateMap.add("PH",
// doesn't need to be a Node; should not be copyable!
new go.Part("Auto", { locationSpot: go.Spot.Center, copyable: false })
.bindTwoWay("location", "loc", go.Point.parse, go.Point.stringify)
.add(
new go.Shape({ fill: "lavender" }),
new go.TextBlock("Copy\nhere", { margin: 8, stroke: "blue", textAlign: "center" })
));
myDiagram.model = new go.GraphLinksModel(
[
{ key: 1, text: "Alpha", color: "lightblue" },
{ key: 2, text: "Beta", color: "orange" },
{ key: 3, text: "Gamma", color: "lightgreen" },
{ key: 4, text: "Delta", color: "pink" }
]);
</script>
</body>
</html>