Many of the previous examples have provided custom templates for nodes, groups, or links. Those examples have shown how to make simple adaptations of the templates for particular data instances via data binding. But what if you want to have nodes with drastically different appearances or behaviors in a single diagram at the same time?
It is possible to define a node template that includes all possible configurations for all of the kinds of nodes that you want to display. There would need to be a lot of data binding and/or code to make the needed changes. Often you will want to make not-GraphObject.visible large parts of the template in order to make visible the one panel that you want to show. But this technique is difficult to use -- templates get way too complicated too quickly.
Instead GoJS supports as many templates as you want -- you choose dynamically which one you want to use to represent a particular node data. This does mean potentially a lot of templates, but each one will be much simpler, easier to write, and easier to maintain.
Each Diagram actually holds a Map of templates for each type of Part: Node, Group, and Link. Each Map associates a "category" name with a template. For example, when the diagram wants to create a Node for a particular node data object, the diagram uses that node data's category to look up the node template in the Diagram.nodeTemplateMap. Similar lookups are done using the Diagram.groupTemplateMap and the Diagram.linkTemplateMap.
Each Diagram initially has its own template maps stocked with predefined categories. The default category for any data object is the empty string, "". The Diagram.nodeTemplateMap initially contains for the empty string a very simple Node template holding a TextBlock whose TextBlock.text property is data bound to the data converted to a string. You can see the default templates for nodes, groups, and links in a number of the previous examples, such as the Groups and Links example.
The value of Diagram.nodeTemplate is just the value of thatDiagram.nodeTemplateMap.get("")
.
Setting Diagram.nodeTemplate just replaces the template in Diagram.nodeTemplateMap named with the empty string.
The implementations of all predefined templates are provided in Templates.js in the Extensions directory. You may wish to copy and adapt these definitions when creating your own templates.
// the "simple" template just shows the key string and the color in the background,
// but it also includes a tooltip that shows the description
const simpletemplate =
new go.Node("Auto", {
toolTip:
go.GraphObject.build("ToolTip")
.add(
new go.TextBlock({ margin: 4 })
.bind("text", "desc")
)
})
.add(
new go.Shape("Ellipse")
.bind("fill", "color"),
new go.TextBlock()
.bind("text", "key")
);
// the "detailed" template shows all of the information in a Table Panel
const detailtemplate =
new go.Node("Auto")
.add(
new go.Shape("RoundedRectangle")
.bind("fill", "color"),
new go.Panel("Table", { defaultAlignment: go.Spot.Left })
.add(
new go.TextBlock({ row: 0, column: 0, columnSpan: 2, font: "bold 12pt sans-serif" })
.bind("text", "key"),
new go.TextBlock("Description:", { row: 1, column: 0 }),
new go.TextBlock({ row: 1, column: 1 })
.bind("text", "desc"),
new go.TextBlock("Color:", { row: 2, column: 0 }),
new go.TextBlock({ row: 2, column: 1 })
.bind("text", "color")
)
);
// create the nodeTemplateMap, holding three node templates:
const templmap = new go.Map(); // In TypeScript you could write: new go.Map<string, go.Node>();
// for each of the node categories, specify which template to use
templmap.add("simple", simpletemplate);
templmap.add("detailed", detailtemplate);
// for the default category, "", use the same template that Diagrams use by default;
// this just shows the key value as a simple TextBlock
templmap.add("", diagram.nodeTemplate);
diagram.nodeTemplateMap = templmap;
diagram.model.nodeDataArray = [
{ key: "Alpha", desc: "first letter", color: "green" }, // uses default category: ""
{ key: "Beta", desc: "second letter", color: "lightblue", category: "simple" },
{ key: "Gamma", desc: "third letter", color: "pink", category: "detailed" },
{ key: "Delta", desc: "fourth letter", color: "cyan", category: "detailed" }
];
If you hover the mouse over the "Beta" node, you will see the tooltip showing the description string. The detailed template does not bother using tooltips to show extra information because everything is already shown.
By default the way that the model and diagram know about the category of a node data or a link data is by looking at its category property. If you want to use a different property on the data, for example because you want to use the category property to mean something different, set Model.nodeCategoryProperty to be the name of the property that results in the actual category string value. Or set Model.nodeCategoryProperty to be the empty string to cause all nodes to use the default node template.
For Panels with a value for Panel.itemArray, there is also the Panel.itemTemplateMap. As with Nodes and Groups and Links, the Panel.itemTemplate is just a reference to the template named with the empty string in the Panel.itemTemplateMap. Similarly, the Panel.itemCategoryProperty names the property on the item data that identifies the template to use from the itemTemplateMap.
// create a template map for items
const itemtemplates = new go.Map(); // In TypeScript you could write: new go.Map<string, go.Panel>();
// the template when type == "text"
itemtemplates.add("text",
new go.Panel()
.add(
new go.TextBlock()
.bind("text")
));
// the template when type == "button"
itemtemplates.add("button",
go.GraphObject.build("Button")
// convert a function name into a function value,
// because functions cannot be represented in JSON format
.bind("click", "handler",
name => {
if (name === "alert") return raiseAlert; // defined below
return null;
})
.add(
new go.TextBlock()
.bind("text")
));
diagram.nodeTemplate =
new go.Node("Vertical")
.add(
new go.TextBlock()
.bind("text", "key"),
new go.Panel("Auto")
.add(
new go.Shape({ fill: "white" }),
new go.Panel("Vertical", {
margin: 3,
defaultAlignment: go.Spot.Left,
itemCategoryProperty: "type", // this property controls the template used
itemTemplateMap: itemtemplates // map was defined above
})
.bind("itemArray", "info")
)
);
function raiseAlert(e, obj) { // here OBJ will be the item Panel
const node = obj.part;
alert(node.data.key + ": " + obj.data.text);
}
// The model data includes item arrays in the node data.
diagram.model = new go.GraphLinksModel( [
{ key: "Alpha",
info: [
{ type: "text", text: "some text" },
{ type: "button", text: "Click me!", handler: "alert"}
]
},
{ key: "Beta",
info: [
{ type: "text", text: "first line" },
{ type: "button", text: "First Button", handler: "alert"},
{ type: "text", text: "second line" },
{ type: "button", text: "Second Button", handler: "alert" }
]
}
],[
{ from: "Alpha", to: "Beta" }
]);
The natural way to have a distinct header for a Table Panel is to have the first row (i.e. the first item) hold the data for the header, but have it be styled differently. In this example we define a "Header" item template in the Panel.itemTemplateMap.
const itemTemplateMap = new go.Map();
itemTemplateMap.add("",
new go.Panel("TableRow")
.add(
new go.TextBlock({ column: 0, margin: 2, font: "bold 10pt sans-serif" })
.bind("text", "name"),
new go.TextBlock({ column: 1, margin: 2 })
.bind("text", "phone"),
new go.TextBlock({ column: 2, margin: 2 })
.bind("text", "loc")
));
itemTemplateMap.add("Header",
new go.Panel("TableRow")
.add(
new go.TextBlock({ column: 0, margin: 2, font: "bold 10pt sans-serif" })
.bind("text", "name"),
new go.TextBlock({ column: 1, margin: 2, font: "bold 10pt sans-serif" })
.bind("text", "phone"),
new go.TextBlock({ column: 2, margin: 2, font: "bold 10pt sans-serif" })
.bind("text", "loc")
));
diagram.nodeTemplate =
new go.Node("Auto")
.add(
new go.Shape({ fill: "white" }),
new go.Panel("Table", {
defaultAlignment: go.Spot.Left,
defaultColumnSeparatorStroke: "black",
itemTemplateMap: itemTemplateMap
})
.bind("itemArray", "people")
.addRowDefinition(0, { background: "lightgray" })
.addRowDefinition(1, { separatorStroke: "black" })
);
diagram.model =
new go.GraphLinksModel(
{
nodeDataArray: [
{ key: "group1",
people: [
{ name: "Person", phone: "Phone", loc: "Location", category: "Header" },
{ name: "Alice", phone: "2345", loc: "C4-E18" },
{ name: "Bob", phone: "9876", loc: "E1-B34" },
{ name: "Carol", phone: "1111", loc: "C4-E23" },
{ name: "Ted", phone: "2222", loc: "C4-E197" },
{ name: "Robert", phone: "5656", loc: "B1-A27" },
{ name: "Natalie", phone: "5698", loc: "B1-B6" }
] }
],
linkDataArray: [
]
}
);
If you do not want to have the header data in the itemArray, and you want to define the header in the node template rather than as an item template, see the example in Item Arrays.
To change the representation of a data object, call Model.setCategoryForNodeData or GraphLinksModel.setCategoryForLinkData. (If you set the Part.category of a data bound Part, it will call the Model method for you.) This causes the diagram to discard any existing Part for the data and re-create it using the new template that is associated with the new category value.
// this function changes the category of the node data to cause the Node to be replaced
function changeCategory(e, obj) {
const node = obj.part;
if (node) {
const diagram = node.diagram;
diagram.startTransaction("changeCategory");
let cat = diagram.model.getCategoryForNodeData(node.data);
if (cat === "simple")
cat = "detailed";
else
cat = "simple";
diagram.model.setCategoryForNodeData(node.data, cat);
diagram.commitTransaction("changeCategory");
}
}
// The "simple" template just shows the key string and the color in the background.
// There is a Button to invoke the changeCategory function.
const simpletemplate =
new go.Node("Spot")
.add(
new go.Panel("Auto")
.add(
new go.Shape("Ellipse")
.bind("fill", "color"),
new go.TextBlock()
.bind("text")
),
go.GraphObject.build("Button", {
alignment: go.Spot.TopRight,
click: changeCategory
})
.add(
new go.Shape("AsteriskLine", { width: 8, height: 8 })
)
);
// The "detailed" template shows all of the information in a Table Panel.
// There is a Button to invoke the changeCategory function.
const detailtemplate =
new go.Node("Spot")
.add(
new go.Panel("Auto")
.add(
new go.Shape("RoundedRectangle")
.bind("fill", "color"),
new go.Panel("Table", { defaultAlignment: go.Spot.Left })
.add(
new go.TextBlock({ row: 0, column: 0, columnSpan: 2, font: "bold 12pt sans-serif" })
.bind("text"),
new go.TextBlock("Description:", { row: 1, column: 0 }),
new go.TextBlock({ row: 1, column: 1 })
.bind("text", "desc"),
new go.TextBlock("Color:", { row: 2, column: 0 }),
new go.TextBlock({ row: 2, column: 1 })
.bind("text", "color")
)
),
go.GraphObject.build("Button", {
alignment: go.Spot.TopRight,
click: changeCategory
})
.add(
new go.Shape("AsteriskLine", { width: 8, height: 8 })
)
);
const templmap = new go.Map(); // In TypeScript you could write: new go.Map<string, go.Node>();
templmap.add("simple", simpletemplate);
templmap.add("detailed", detailtemplate);
diagram.nodeTemplateMap = templmap;
diagram.layout = new go.TreeLayout();
diagram.model.nodeDataArray = [
{ key: 2, text: "Beta", desc: "second letter", color: "lightblue", category: "simple" },
{ key: 3, text: "Gamma", desc: "third letter", color: "pink", category: "detailed" },
{ key: 4, text: "Delta", desc: "fourth letter", color: "cyan", category: "detailed" }
];
diagram.model.linkDataArray = [
{ from: 2, to: 3 },
{ from: 3, to: 4 }
];
Click on the "asterisk" button on any node to toggle dynamically between the "simple" and the "detailed" category for each node.
You can also replace one or all of the diagram's template maps (e.g. Diagram.nodeTemplateMap) in order to discard and re-create all of the nodes in the diagram. If you are only using the default template for nodes, you would only need to replace the Diagram.nodeTemplate.
One common circumstance for doing this is as the Diagram.scale changes. When the user zooms out far enough, there is no point in having too much detail about each of the nodes.
If you zoom out in this example, the DiagramEvent listener will detect when the Diagram.scale becomes small enough to use the simpler template for all of the nodes. Zoom in again and suddenly it uses the more detailed template.
// The "simple" template just shows the key string and the color in the background.
const simpletemplate =
new go.Node("Spot")
.add(
new go.Panel("Auto")
.add(
new go.Shape("Ellipse")
.bind("fill", "color"),
new go.TextBlock()
.bind("text", "key")
)
);
// The "detailed" template shows all of the information in a Table Panel.
const detailtemplate =
new go.Node("Spot")
.add(
new go.Panel("Auto")
.add(
new go.Shape("RoundedRectangle")
.bind("fill", "color"),
new go.Panel("Table", { defaultAlignment: go.Spot.Left })
.add(
new go.TextBlock({ row: 0, column: 0, columnSpan: 2, font: "bold 12pt sans-serif" })
.bind("text"),
new go.TextBlock("Description:", { row: 1, column: 0 }),
new go.TextBlock({ row: 1, column: 1 })
.bind("text", "desc"),
new go.TextBlock("Color:", { row: 2, column: 0 }),
new go.TextBlock({ row: 2, column: 1 })
.bind("text", "color")
)
)
);
diagram.layout = new go.TreeLayout();
diagram.model.nodeDataArray = [
{ key: 2, text: "Beta", desc: "second letter", color: "lightblue" },
{ key: 3, text: "Gamma", desc: "third letter", color: "pink" },
{ key: 4, text: "Delta", desc: "fourth letter", color: "cyan" }
];
diagram.model.linkDataArray = [
{ from: 2, to: 3 },
{ from: 3, to: 4 }
];
// initially use the detailed templates
diagram.nodeTemplate = detailtemplate;
diagram.addDiagramListener("ViewportBoundsChanged",
e => {
if (diagram.scale < 0.9) {
diagram.nodeTemplate = simpletemplate;
} else {
diagram.nodeTemplate = detailtemplate;
}
});
// make accessible to the HTML buttons
myDiagram = diagram;
Caution: if you modify a template Map, there is no notification that the map has changed. You will need to call Diagram.rebuildParts explicitly. If you are replacing the Diagram.nodeTemplate or the Diagram.nodeTemplateMap or the corresponding properties for Groups or Links, the Diagram property setters will automatically call Diagram.rebuildParts.
When one or more templates are replaced in a diagram, layouts are automatically performed again.