UNPKG

create-gojs-kit

Version:

A CLI for downloading GoJS samples, extensions, and docs

992 lines (937 loc) 41.6 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, viewport-fit=cover"/> <link rel="preconnect" href="https://rsms.me/"> <link rel="stylesheet" href="../assets/css/style.css"> <!-- Copyright 1998-2025 by Northwoods Software Corporation. --> <meta itemprop="name" content="Validation" /> <meta property="og:title" content="Validation" /> <meta name="twitter:title" content="Validation" /> <meta property="og:image" content="https://gojs.net/latest/assets/images/fp/defaultCard.png" /> <meta itemprop="image" content="https://gojs.net/latest/assets/images/fp/defaultCard.png" /> <meta name="twitter:image" content="https://gojs.net/latest/assets/images/fp/defaultCard.png" /> <meta property="og:url" content="https://gojs.net/latest/intro/validation.html" /> <meta property="twitter:url" content="https://gojs.net/latest/intro/validation.html" /> <meta name="twitter:card" content="summary_large_image" /> <meta property="og:type" content="website" /> <meta property="twitter:domain" content="gojs.net" /> <title> Validation | GoJS </title> <link rel="stylesheet" href="../assets/css/prism.css"/> </head> <script> window.diagrams = []; window.goCode = function (pre, w, h, parentid, animation) { window .diagrams .push([pre, w, h, parentid, animation]); } </script> <body> <nav id="navTop" class=" w-full h-[var(--topnav-h)] z-30 bg-white border-b border-b-gray-200"> <div class="max-w-screen-xl mx-auto flex flex-wrap items-start justify-between px-4"> <a class="text-white bg-nwoods-primary font-bold !leading-[calc(var(--topnav-h)_-_1px)] my-0 px-2 text-4xl lg:text-5xl logo" href="../"> GoJS </a> <div class="relative"> <button id="topnavButton" class="h-[calc(var(--topnav-h)_-_1px)] px-2 m-0 text-gray-900 bg-inherit shadow-none md:hidden hover:!bg-inherit hover:!text-nwoods-accent hover:!shadow-none" aria-label="Navigation"> <svg class="h-7 w-7 block" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"> <path d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" stroke-linecap="round" stroke-linejoin="round"/> </svg> </button> <div id="topnavList" class="hidden md:block"> <div class="absolute right-0 z-30 flex flex-col items-end rounded border border-gray-200 p-4 pl-12 shadow bg-white text-gray-900 font-semibold md:flex-row md:space-x-4 md:items-start md:border-0 md:p-0 md:shadow-none md:bg-inherit"> <a href="../learn/">Learn</a> <a href="../samples/">Samples</a> <a href="../intro/">Intro</a> <a href="../api/">API</a> <a href="../download.html">Download</a> <a href="https://forum.nwoods.com/c/gojs/11" target="_blank" rel="noopener">Forum</a> <a id="tc" href="https://nwoods.com/contact.html" target="_blank" rel="noopener" onclick="getOutboundLink('https://nwoods.com/contact.html', 'contact');">Contact</a> <a id="tb" href="https://nwoods.com/sales/index.html" target="_blank" rel="noopener" onclick="getOutboundLink('https://nwoods.com/sales/index.html', 'buy');">Buy</a> </div> </div> </div> </div> </nav> <script> window.addEventListener("DOMContentLoaded", function () { // topnav var topButton = document.getElementById("topnavButton"); var topnavList = document.getElementById("topnavList"); if (topButton && topnavList) { topButton.addEventListener("click", function (e) { topnavList .classList .toggle("hidden"); e.stopPropagation(); }); document.addEventListener("click", function (e) { // if the clicked element isn't the list, close the list if (!topnavList.classList.contains("hidden") && !e.target.closest("#topnavList")) { topButton.click(); } }); // set active <a> element var url = window .location .href .toLowerCase(); var aTags = topnavList.getElementsByTagName('a'); for (var i = 0; i < aTags.length; i++) { var lowerhref = aTags[i] .href .toLowerCase(); if (lowerhref.endsWith('.html')) lowerhref = lowerhref.slice(0, -5); if (url.startsWith(lowerhref)) { aTags[i] .classList .add('active'); break; } } } }); </script> <div class="sticky top-0 left-0 z-10 px-2 w-full bg-white border-b border-b-gray-200 md:hidden"> <button id="sidenavButton" class="flex p-2 text-gray-900 bg-inherit shadow-none items-center text-sm font-semibold hover:!bg-inherit hover:!text-nwoods-accent hover:!shadow-none" aria-label="Navigation"> <svg class="h-7 w-7 block mr-2" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"> <path d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" stroke-linecap="round" stroke-linejoin="round"/> </svg> <span>Menu</span> </button> </div> <script> window.addEventListener("DOMContentLoaded", function () { // sidenav var sideButton = document.getElementById("sidenavButton"); var sidenav = document.getElementById("sidenav"); if (sideButton && sidenav) { sideButton.addEventListener("click", function (e) { sidenav .classList .toggle("hidden"); e.stopPropagation(); }); document.addEventListener("click", function (e) { // if the clicked element isn't the list, close the list if (!sidenav.classList.contains("hidden") && !e.target.closest("#sidenavList")) { sideButton.click(); } }); } }); </script> <div class="flex flex-row md:min-h-screen w-full max-w-screen-xl mx-auto"> <aside id="sidenav" class="hidden fixed top-0 left-0 z-10 w-full bg-black/10 min-h-screen max-h-screen overflow-x-hidden overflow-y-auto shrink-0 md:block md:sticky md:w-52 md:min-h-0 md:bg-inherit md:border-r md:border-r-gray-200 md:overscroll-auto"> <nav id="sidenavList" class="flex flex-col bg-white w-52 min-h-screen pl-2 pt-4 pb-24 md:w-full md:min-h-0"> <a href="index.html">Basics</a> <a href="buildingObjects.html">Building Parts</a> <a href="usingModels.html">Using Models</a> <a href="dataBinding.html">Data Binding</a> <a href="react.html">GoJS with React</a> <a href="svelte.html">GoJS with Svelte</a> <a href="angular.html">GoJS with Angular</a> <a href="textBlocks.html">TextBlocks</a> <a href="shapes.html">Shapes</a> <a href="pictures.html">Pictures</a> <a href="panels.html">Panels</a> <a href="tablePanels.html">Table Panels</a> <a href="brush.html">Brushes</a> <a href="sizing.html">Sizing Objects</a> <a href="itemArrays.html">Item Arrays</a> <a href="changedEvents.html">Changed Events</a> <a href="transactions.html">Transactions</a> <a href="viewport.html">Coordinates</a> <a href="initialView.html">Initial View</a> <a href="collections.html">Collections</a> <a href="links.html">Links</a> <a href="linkLabels.html">Link Labels</a> <a href="connectionPoints.html">Link Points</a> <a href="ports.html">Ports</a> <a href="nodes.html">Nodes</a> <a href="typings.html">Typings</a> <a href="debugging.html">Debugging</a> <a href="layouts.html">Layouts</a> <a href="routers.html">Routers</a> <a href="trees.html">Trees</a> <a href="subtrees.html">SubTrees</a> <a href="groups.html">Groups</a> <a href="subgraphs.html">SubGraphs</a> <a href="sizedGroups.html">Sized Groups</a> <a href="selection.html">Selection</a> <a href="highlighting.html">Highlighting</a> <a href="theming.html">Theming</a> <a href="tooltips.html">ToolTips</a> <a href="contextMenus.html">Context Menus</a> <a href="events.html">Diagram Events</a> <a href="tools.html">Tools</a> <a href="commands.html">Commands</a> <a href="accessibility.html">Accessibility</a> <a href="buttons.html">Buttons</a> <a href="permissions.html">Permissions</a> <a href="validation.html">Validation</a> <a href="animation.html">Animation</a> <a href="HTMLInteraction.html">HTML Interaction</a> <a href="layers.html">Layers &amp; Z-ordering</a> <a href="palette.html">Palette</a> <a href="overview.html">Overview</a> <a href="replacingDeleting.html">Replacing and Deleting</a> <a href="templateMaps.html">Template Maps</a> <a href="legends.html">Legends and Titles</a> <a href="extensions.html">Extensions</a> <a href="geometry.html">Geometry Strings</a> <a href="grids.html">Grid Patterns</a> <a href="graduatedPanels.html">Graduated Panels</a> <a href="SVGContext.html">Rendering to SVG</a> <a href="makingSVG.html">Snapshot to SVG</a> <a href="makingImages.html">Diagram Images</a> <a href="printing.html">Printing</a> <a href="serverSideImages.html">Server-side Images</a> <a href="nodeScript.html">GoJS in Node.js</a> <a href="testing.html">Testing</a> <a href="performance.html">Performance</a> <a href="platforms.html">Platforms</a> <a href="deployment.html">Deployment</a> </nav> </aside> <script> var navList = document.getElementById('sidenavList'); if (navList !== null) { var url = window.location.href; var lindex = url.lastIndexOf('/'); url = url .slice(lindex + 1) .toLowerCase(); var aTags = navList.getElementsByTagName('a'); var currentindex = -1; for (var i = 0; i < aTags.length; i++) { var lowerhref = aTags[i] .href .toLowerCase(); if (lowerhref.indexOf('/' + url) !== -1) { currentindex = i; aTags[i] .classList .add('active'); break; } } } </script> <div class="px-4 pb-16 w-full overflow-hidden prose"> <h1>Validation</h1> <p> Some operations require more sophisticated controls than the binary permission flags discussed in the previous <a href="permissions.html">section</a>. When the user tries to draw a new link or reconnect an existing link, your application may want to restrict which links may be made, depending on the data. When the user tries to add a node to a group, your application may want to control whether it is permitted for that particular node in that particular group. When the user edits some text, your application may want to limit the kinds of strings that they enter. </p> <p> Although not exactly "validation", you can also limit how users drag (move or copy) parts by setting several properties on <a>Part</a> and customizing the <a>DraggingTool</a>. </p> <h2 id="LinkingValidation">Linking Validation</h2> <p> There are a number of <a>GraphObject</a> properties that let you control what links the user may draw or reconnect. These properties apply to each port element and affect the links that may connect with that port. </p> <h3 id="LinkableProperties">Linkable properties</h3> <p> The primary properties are <a>GraphObject.fromLinkable</a> and <a>GraphObject.toLinkable</a>. If you do not have a <a>Node</a> containing an element with fromLinkable: true and another node with toLinkable: true, the user will not be able to draw a new link between the nodes. </p> <pre class="lang-js" id="linkable"><code> diagram.nodeTemplate = new go.Node("Auto") .bind("location", "loc", go.Point.parse) .add( new go.Shape("Ellipse", { fill: "green", portId: "", cursor: "pointer" }) .bind("fromLinkable", "from") .bind("toLinkable", "to"), new go.TextBlock({ stroke: "white", margin: 3 }) .bind("text", "key") ); const nodeDataArray = [ { key: "From1", loc: "0 0", from: true }, { key: "From2", loc: "0 100", from: true }, { key: "To1", loc: "150 0", to: true }, { key: "To2", loc: "150 100", to: true } ]; const linkDataArray = [ // initially no links ]; diagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray); </code></pre> <script> goCode('linkable', 600, 150); </script> <p> Mouse down on the green ellipse (the cursor changes to a "pointer") and drag to start drawing a new link. Note how the only permitted links are those going from a "From" node to a "To" node. This is true even if you start the linking gesture on a "To" node. </p> <h3 id="SpanOfLinkableProperties">Span of Linkable properties</h3> <p> Because the <a>TextBlock</a> in the above example is not declared to be a port (i.e. there is no value for <a>GraphObject.portId</a>), mouse events on the TextBlock do not start the <a>LinkingTool</a>. This allows users the ability to select and move the node as well as any number of other operations. </p> <p> You can certainly declare a <a>Panel</a> to have <a>GraphObject.fromLinkable</a> or <a>GraphObject.toLinkable</a> be true. This will cause all elements inside that panel to behave as part of the port, including starting a linking operation. Sometimes you will want to make the whole <a>Node</a> linkable. If you still want the user to be able to select and drag the node, you will need to make some easy-to-click elements not-"linkable" within the node. You can do that by explicitly setting <a>GraphObject.fromLinkable</a> and/or <a>GraphObject.toLinkable</a> to false. The default value for those two properties is null, which means the "linkable"-ness is inherited from the containing panel. </p> <h2 id="OtherLinkingPermissionProperties">Other linking permission properties</h2> <p> Just because you have set <a>GraphObject.fromLinkable</a> and <a>GraphObject.toLinkable</a> to true on the desired port objects does not mean that you want to allow users to create a link from every such port/node to every other port/node. There are other <a>GraphObject</a> properties governing linkability for both the "from" and the "to" ends. </p> <h3 id="LinkableDuplicatesProperties">LinkableDuplicates properties</h3> <p> One restriction that you may have noticed before is that the user cannot draw a second link between the same pair of nodes in the same direction. This example sets <a>GraphObject.fromLinkableDuplicates</a> and <a>GraphObject.toLinkableDuplicates</a> to true, in order to permit such duplicate links between nodes. </p> <pre class="lang-js" id="linkableDuplicates"><code> diagram.nodeTemplate = new go.Node("Auto") .bind("location", "loc", go.Point.parse) .add( new go.Shape("Ellipse", { fill: "green", portId: "", cursor: "pointer", fromLinkableDuplicates: true, toLinkableDuplicates: true }) .bind("fromLinkable", "from") .bind("toLinkable", "to"), new go.TextBlock({ stroke: "white", margin: 3 }) .bind("text", "key") ); const nodeDataArray = [ { key: "From1", loc: "0 0", from: true }, { key: "From2", loc: "0 100", from: true }, { key: "To1", loc: "150 0", to: true }, { key: "To2", loc: "150 100", to: true } ]; const linkDataArray = [ // initially no links ]; diagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray); </code></pre> <script> goCode('linkableDuplicates', 600, 150); </script> <p> Now try drawing multiple links between "From1" and "To1". You can see how the links are automatically spread apart. Try dragging one of the nodes to see what happens with the link routing. A similar effect occurs also when the link's <a>Link.curve</a> is <a>Curve.Bezier</a>. </p> <h3 id="LinkableSelfNodeProperties">LinkableSelfNode properties</h3> <p> Another standard restriction is that the user cannot draw a link from a node to itself. Again it is easy to remove that restriction: just set <a>GraphObject.fromLinkableSelfNode</a> and <a>GraphObject.toLinkableSelfNode</a> to true. Note though that each node has to be both <a>GraphObject.fromLinkable</a> and <a>GraphObject.toLinkable</a>. </p> <pre class="lang-js" id="linkableSelfNodes"><code> diagram.nodeTemplate = new go.Node("Auto") .bind("location", "loc", go.Point.parse) .add( new go.Shape("Ellipse", { fill: "green", portId: "", cursor: "pointer", fromLinkable: true, toLinkable: true, fromLinkableDuplicates: true, toLinkableDuplicates: true, fromLinkableSelfNode: true, toLinkableSelfNode: true }), new go.TextBlock({ stroke: "white", margin: 3 }) .bind("text", "key") ); const nodeDataArray = [ { key: "Node1", loc: "0 0" }, { key: "Node2", loc: "150 50" } ]; const linkDataArray = [ // initially no links ]; diagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray); </code></pre> <script> goCode('linkableSelfNodes', 600, 150); </script> <p> To draw a reflexive link, start drawing a new link but stay near the node when you release the mouse button. This example also sets the "...Duplicates" properties to true, so that you can draw multiple reflexive links. </p> <p> In these examples there is only one port per node. When there are multiple ports in a node, the restrictions actually apply per port, not per node. But the restrictions of the "...LinkableSelfNode" properties do span the whole node, so they must be applied to both ports within a node for a link to connect to its own node. </p> <h3 id="MaxLinksProperties">MaxLinks properties</h3> <p> The final linking restriction properties control how many links may connect to a node/port. This example sets the <a>GraphObject.toMaxLinks</a> property to 2, even though <a>GraphObject.toLinkableDuplicates</a> is true, to limit how many links may go into "to" nodes. </p> <pre class="lang-js" id="linkableMax"><code> diagram.nodeTemplate = new go.Node("Auto") .bind("location", "loc", go.Point.parse) .add( new go.Shape("Ellipse", { fill: "green", portId: "", cursor: "pointer", fromLinkableDuplicates: true, toLinkableDuplicates: true, toMaxLinks: 2 }) // at most TWO links can come into this node .bind("fromLinkable", "from") .bind("toLinkable", "to"), new go.TextBlock({ stroke: "white", margin: 3 }) .bind("text", "key") ); const nodeDataArray = [ { key: "From1", loc: "0 0", from: true }, { key: "From2", loc: "0 100", from: true }, { key: "To1", loc: "150 0", to: true }, { key: "To2", loc: "150 100", to: true } ]; const linkDataArray = [ // initially no links ]; diagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray); </code></pre> <script> goCode('linkableMax', 600, 150); </script> <p> This example has no limit on the number of links that may come out of "from" nodes. </p> <p> If this property is set, it is most commonly set to one. Of course it should depend on the nature of the application. </p> <p> Note that the <a>GraphObject.toMaxLinks</a> and <a>GraphObject.fromMaxLinks</a> properties are independent of each other. If you want to control the total number of links connecting with a port, not only "to" or "from" but both directions, then you cannot use those two properties and instead must implement your own link validation predicate, as discussed below. </p> <h2 id="CyclesInGraphs">Cycles in graphs</h2> <p> If you want to make sure that the graph structure that your users create never have any cycles of links, or that the graph is always tree-structured, <b>GoJS</b> makes that easy to enforce. Just set <a>Diagram.validCycle</a> to <a>CycleMode.NotDirected</a> or <a>CycleMode.DestinationTree</a>. The default value is <a>CycleMode.All</a>, which imposes no restrictions -- all kinds of link cycles are allowed. </p> <p> This example has nodes that allow links both to and from each node. However the assignment of <a>Diagram.validCycle</a> will prevent the user from drawing a second incoming link to any node and also ensures that the user draw no cycles in the graph. </p> <pre class="lang-js" id="tree"><code> diagram.nodeTemplate = new go.Node("Auto") .add( new go.Shape("Ellipse", { fill: "green", portId: "", cursor: "pointer", fromLinkable: true, toLinkable: true }), new go.TextBlock({ stroke: "white", margin: 3 }) .bind("text", "key") ); const nodeDataArray = [ { key: "Node1" }, { key: "Node2" }, { key: "Node3" }, { key: "Node4" }, { key: "Node5" }, { key: "Node6" }, { key: "Node7" }, { key: "Node8" }, { key: "Node9" } ]; const linkDataArray = [ // initially no links ]; diagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray); // only allow links that maintain tree-structure diagram.validCycle = go.CycleMode.DestinationTree; </code></pre> <script> goCode('tree', 600, 250); </script> <p> As you draw more links you can see how the set of potential linking destinations keeps getting smaller. </p> <h2 id="GeneralLinkingValidation">General linking validation</h2> <p> It may be the case that the semantics of your application will cause the set of valid link destinations to depend on the node data (i.e. at the node and port at which the link started from and at the possible destination node/port) in a manner that can only be implemented using code: a predicate function. </p> <p> You can implement such domain-specific validation by setting <a>LinkingBaseTool.linkValidation</a> or <a>Node.linkValidation</a>. These predicates, if supplied, are called for each pair of ports that the linking tool considers. If the predicate returns false, the link may not be made. Setting the property on the <a>LinkingTool</a> or <a>RelinkingTool</a>causes the predicate to be applied to all linking operations, whereas setting the property on the <a>Node</a> only applies to linking operations involving that node. The predicates are called only if all of the standard link checks pass, based on the properties discussed above. </p> <p> In this example there are nodes of three different colors. The <a>LinkingTool</a> and <a>RelinkingTool</a> are customized to use a function, <code>sameColor</code>, to make sure the links only connect nodes of the same color. Mouse-down and drag on the ellipses (where the cursor changes to a "pointer") to start drawing a new link. You will see that the only permitted link destinations are nodes of the same color that do not already have a link to it from the same node. </p> <pre class="lang-js" id="linking"><code> diagram.nodeTemplate = new go.Node("Auto") .add( new go.Shape("Ellipse", { cursor: "pointer", portId: "", fromLinkable: true, toLinkable: true }) .bind("fill", "color"), new go.TextBlock({ stroke: "white", margin: 3 }) .bind("text", "key") ); diagram.linkTemplate = new go.Link({ curve: go.Curve.Bezier, relinkableFrom: true, relinkableTo: true }) .add( new go.Shape({ strokeWidth: 2 }) .bindObject("stroke", "fromNode", n => n.data.color), new go.Shape({ toArrow: "Standard", stroke: null }) .bindObject("fill", "fromNode", n => n.data.color) ); // this predicate is true if both nodes have the same color function sameColor(fromnode, fromport, tonode, toport) { return fromnode.data.color === tonode.data.color; // this could look at the fromport.fill and toport.fill instead, // assuming that the ports are Shapes, which they are because portID was set on them, // and that there is a data Binding on the Shape.fill } // only allow new links between ports of the same color diagram.toolManager.linkingTool.linkValidation = sameColor; // only allow reconnecting an existing link to a port of the same color diagram.toolManager.relinkingTool.linkValidation = sameColor; const nodeDataArray = [ { key: "Red1", color: "red" }, { key: "Blue1", color: "blue" }, { key: "Green1", color: "green" }, { key: "Green2", color: "green" }, { key: "Red2", color: "red" }, { key: "Blue2", color: "blue" }, { key: "Red3", color: "red" }, { key: "Green3", color: "green" }, { key: "Blue3", color: "blue" } ]; const linkDataArray = [ // initially no links ]; diagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray); </code></pre> <script> goCode('linking', 600, 250); </script> <p> To emphasize the color restriction, links have their colors bound to the "from" node data. </p> <h3 id="Limitingtotalnumberoflinksconnectingwithanode">Limiting total number of links connecting with a node</h3> <p> One can limit the number of links coming into a port by setting <a>GraphObject.toMaxLinks</a>. Similarly, one can limit the number of links coming out of a port by setting <a>GraphObject.fromMaxLinks</a>. But what if you want to limit the total number of links connecting with a port regardless of whether they are coming into or going out of a port? Such constraints can only be implemented by a link validation predicate. </p> <p> When wanting to limit the total number of links in either direction, connecting with each port, one can use this <a>Node.linkValidation</a> predicate: </p> <pre class="lang-js"><code> new go.Node({ linkValidation: (fromnode, fromport, tonode, toport) => { // total number of links connecting with a port is limited to 1: return fromnode.findLinksConnected(fromport.portId).count + tonode.findLinksConnected(toport.portId).count < 1; }, . . . }) . . . </code></pre> <p> When wanting to limit the total number of links in either direction, connecting with a node for all of its ports, one can use this <a>Node.linkValidation</a> predicate: </p> <pre class="lang-js"><code> new go.Node({ linkValidation: (fromnode, fromport, tonode, toport) => { // total number of links connecting with all ports of a node is limited to 1: return fromnode.linksConnected.count + tonode.linksConnected.count < 1; }, . . . }) . . . </code></pre> <h2 id="GroupingValidation">Grouping validation</h2> <p> When you want to limit the kinds of nodes that the user may add to a particular group, you can implement a predicate as the <a>CommandHandler.memberValidation</a> or <a>Group.memberValidation</a> property. Setting the property on the <a>CommandHandler</a> causes the predicate to be applied to all Groups, whereas setting the property on the <a>Group</a> only applies to that group. </p> <p> In this example the <code>samePrefix</code> predicate is used to determine if a Node may be dropped into a Group. Try dragging the simple textual nodes on the left side into either of the groups on the right side. Only when dropping the node onto a group that is highlit "green" will the node be added as a member of the group. You can verify that by moving the group to see if the textual node moves too. </p> <pre class="lang-js" id="grouping"><code> // this predicate is true if both node data keys start with the same letter function samePrefix(group, node) { if (group === null) return true; // when maybe dropping a node in the background if (node instanceof go.Group) return false; // don't add Groups to Groups return group.data.key.charAt(0) === node.data.key.charAt(0); }; diagram.nodeTemplate = new go.Node() .bind("location", "loc", go.Point.parse) .add( new go.TextBlock() .bind("text", "key") ); diagram.groupTemplate = new go.Group("Vertical", { // only allow those simple nodes that have the same data key prefix: memberValidation: samePrefix, // don't need to define handlers on member Nodes and Links handlesDragDropForMembers: true, // support highlighting of Groups when allowing a drop to add a member mouseDragEnter: (e, grp, prev) => { // this will call samePrefix; it is true if any node has the same key prefix if (grp.canAddMembers(grp.diagram.selection)) { const shape = grp.findObject("SHAPE"); if (shape) shape.fill = "green"; grp.diagram.currentCursor = ""; } else { grp.diagram.currentCursor = "not-allowed"; } }, mouseDragLeave: (e, grp, next) => { const shape = grp.findObject("SHAPE"); if (shape) shape.fill = "rgba(128,128,128,0.33)"; grp.diagram.currentCursor = ""; }, // actually add permitted new members when a drop occurs mouseDrop: (e, grp) => { if (grp.canAddMembers(grp.diagram.selection)) { // this will only add nodes with the same key prefix grp.addMembers(grp.diagram.selection, true); } else { // and otherwise cancel the drop grp.diagram.currentTool.doCancel(); } }, // make sure all Groups are behind all regular Nodes layerName: "Background" }) .bind("location", "loc", go.Point.parse) .add( new go.TextBlock({ alignment: go.Spot.Left, font: "Bold 12pt Sans-Serif" }) .bind("text", "key"), new go.Shape({ name: "SHAPE", width: 100, height: 100, fill: "rgba(128,128,128,0.33)" }) ); diagram.mouseDrop = e => { // dropping in diagram background removes nodes from any group diagram.commandHandler.addTopLevelParts(diagram.selection, true); }; const nodeDataArray = [ { key: "A group", isGroup: true, loc: "100 10" }, { key: "B group", isGroup: true, loc: "100 140" }, { key: "A1", loc: "10 30" }, // can be added to "A" group { key: "A2", loc: "10 60" }, { key: "B1", loc: "10 90" }, // can be added to "B" group { key: "B2", loc: "10 120" }, { key: "C1", loc: "10 150" } // cannot be added to either group ]; diagram.model = new go.GraphLinksModel(nodeDataArray, []); </code></pre> <script> goCode('grouping', 600, 300); </script> <p> These groups are fixed size groups -- they do not use <a>Placeholder</a>s. So when a node is dropped into them the group does not automatically resize itself to surround its member nodes. But that is also a benefit when dragging a node out of a group. </p> <p> The validation predicate is also called when dragging a node that is already a member of a group. You can see how it is acceptable to drop the node into its existing containing group. And when it is dragged outside of the group into the diagram's background, the predicate is called with null as the "group" argument. </p> <p> In this example it is always OK to drop a node in the background of the diagram rather than into a group. If you want to disallow dropping in the background, you can call <code>myDiagram.currentTool.doCancel()</code> in the <a>Diagram.mouseDrop</a> event handler. If you want to show feedback during the drag in the background, you can implement a <a>Diagram.mouseDragOver</a> event handler that sets <code>myDiagram.currentCursor = "not-allowed"</code>. This would be behavior similar to that implemented above when dragging inside a Group. </p> <h2 id="TextEditingValidation">Text editing validation</h2> <p> You can also limit what text the user enters when they do in-place text editing of a <a>TextBlock</a>. First, to enable any editing at all, you will need to set <a>TextBlock.editable</a> to true. There may be many TextBlocks within a Part, but you might want to limit text editing to particular TextBlocks. </p> <p> Normally there is no limitation on what text the user may enter. If you want to provide a predicate to approve the input when the user finishes editing, set the <a>TextEditingTool.textValidation</a> or <a>TextBlock.textValidation</a> property. Setting the property on the <a>TextEditingTool</a> causes the predicate to be applied to the editing of all TextBlocks, whereas setting the property on the <a>TextBlock</a> only applies to that object's text. </p> <pre class="lang-js" id="textEditing"><code> // this predicate is true if the new string has at least three characters // and has a vowel in it function okName(textblock, oldstr, newstr) { return newstr.length >= 3 && /[aeiouy]/i.test(newstr); }; diagram.nodeTemplate = new go.Node("Auto") .add( new go.Shape({ fill: "lightyellow" }), new go.Panel("Vertical", { margin: 3 }) .add( new go.TextBlock({ editable: true }) // no validation predicate .bind("text", "text1"), new go.TextBlock({ editable: true, isMultiline: false, // don't allow embedded newlines textValidation: okName }) // new string must be an OK name .bind("text", "text2") ) ); const nodeDataArray = [ { key: 1, text1: "Hello", text2: "Dolly!" }, { key: 2, text1: "Goodbye", text2: "Mr. Chips" } ]; diagram.model = new go.GraphLinksModel(nodeDataArray, []); </code></pre> <script> goCode('textEditing', 600, 100); </script> <p> Note how editing the top TextBlock accepts text without any vowels, but the bottom one does not accept it (or if it's too short) and instead leaves the text editor open. </p> <p> If you want to execute code after a text edit completes, implement a "TextEdited" <a>DiagramEvent</a> listener. </p> <h3 id="ShowingTextEditingErrorMessage">Showing a Text Editing Error Message</h3> <p> If you would like to show a custom error message when text validation fails, one way is to show a tooltip <a>Adornment</a>. Here is an example where a valid string must contain the letter "W". </p> <pre class="lang-js" id="textEditingMessage"><code> diagram.nodeTemplate = new go.Node("Auto") .add( new go.Shape({ fill: "white", portId: "", fromLinkable: true, toLinkable: true, cursor: "pointer" }) .bind("fill", "color"), new go.TextBlock({ margin: 8, editable: true, isMultiline: false, textValidation: (tb, olds, news) => news.indexOf("W") >= 0, // new string must contain a "W" errorFunction: (tool, olds, news) => { // create and show tooltip about why editing failed for this textblock const mgr = tool.diagram.toolManager; mgr.hideToolTip(); // hide any currently showing tooltip const node = tool.textBlock.part; // create a GoJS tooltip, which is an Adornment const tt = go.GraphObject.build("ToolTip", { "Border.fill": "pink", "Border.stroke": "red", "Border.strokeWidth": 2 }) .add( new go.TextBlock( "Unable to replace the string '" + olds + "' with '" + news + "' on node '" + node.key + "'\nbecause the new string does not contain the capital letter 'W'.")); mgr.showToolTip(tt, node); }, textEdited: (tb, olds, news) => { const mgr = tb.diagram.toolManager; mgr.hideToolTip(); } }) .bindTwoWay("text") ); diagram.model = new go.GraphLinksModel([ { key: 1, text: "Alpha" }, { key: 2, text: "Beta" } ], [ { from: 1, to: 2 } ]); </code></pre> <script> goCode('textEditingMessage', 600, 200); </script> <p> Try editing the text of a node by twice clicking on some text. If the string does not have the letter "W" in it, it will show an error message describing the problem. </p> </div> </div> <footer class="bg-white text-gray-900 border-t border-t-gray-200"> <div class="w-full max-w-screen-lg mx-auto px-4 py-6"> <p id="version" class="text-xs text-gray-900 m-0"></p> <div class="text-sm px-0 mb-4 grid grid-cols-2 sm:grid-cols-3 gap-y-10"> <div> <h2 class="text-base font-semibold text-nwoods-primary">GoJS</h2> <ul class="list-none space-y-4 md:space-y-1 px-0"> <li> <a href="../samples/index.html">Samples</a> </li> <li> <a href="../learn/index.html">Learn</a> </li> <li> <a href="../intro/index.html">Intro</a> </li> <li> <a href="../api/index.html">API</a> </li> <li> <a href="../changelog.html">Changelog</a> </li> <li> <a href="https://github.com/NorthwoodsSoftware/GoJS" target="_blank" rel="noopener">GitHub</a> </li> </ul> </div> <div> <h2 class="text-base font-semibold text-nwoods-primary">Support</h2> <ul class="list-none space-y-4 md:space-y-1 px-0"> <li> <a href="https://nwoods.com/contact.html" target="_blank" rel="noopener" onclick="getOutboundLink('https://nwoods.com/contact.html', 'contact');">Contact</a> </li> <li> <a href="https://forum.nwoods.com/c/gojs" target="_blank" rel="noopener">Forum</a> </li> <li> <a href="https://nwoods.com/app/activate.aspx?sku=gojs" target="_blank" rel="noopener">Activate</a> </li> <li> <a href="https://nwoods.com/sales/index.html" target="_blank" rel="noopener" onclick="getOutboundLink('https://nwoods.com/sales/index.html', 'buy');">Buy</a> </li> <li> <a href="https://nwoods.com/register.html" target="_blank" rel="noopener">Register</a> </li> </ul> </div> <div> <h2 class="text-base font-semibold text-nwoods-primary">Company</h2> <ul class="list-none space-y-4 md:space-y-1 px-0"> <li> <a target="_blank" href="https://nwoods.com" target="_blank" rel="noopener">Northwoods</a> </li> <li> <a target="_blank" href="https://nwoods.com/about.html" target="_blank" rel="noopener">About Us</a> </li> <li> <a target="_blank" href="https://nwoods.com/contact.html" target="_blank" rel="noopener">Contact Us</a> </li> <li> <a target="_blank" href="https://nwoods.com/consulting.html" target="_blank" rel="noopener">Consulting</a> </li> <li> <a target="_blank" href="https://twitter.com/northwoodsgo" target="_blank" rel="noopener">Twitter</a> </li> </ul> </div> </div> <p class="text-sm text-gray-900 md:mb-6"> Copyright 1998-2025 <a href="https://nwoods.com">Northwoods Software</a> </p> </div> </footer> </body> <script async src="https://www.googletagmanager.com/gtag/js?id=G-S5QK8VSK84"></script> <script> window.dataLayer = window.dataLayer || []; function gtag() { dataLayer.push(arguments); } gtag('js', new Date()); gtag('config', 'G-S5QK8VSK84'); var getOutboundLink = function (url, label) { gtag('event', 'click', { 'event_category': 'outbound', 'event_label': label, 'transport_type': 'beacon' }); } const params = new URL(document.location).searchParams let a = params.get('a'); if (a) localStorage.setItem('a', a); a = localStorage.getItem('a'); if (a) { const links = [...document.body.getElementsByTagName("a")].filter((l) => l.href.includes('nwoods.com')); for (const l of links) { const url = new URL(l.href); url.searchParams.set('a', a); l.href = url; } } </script> <script src="../assets/js/prism.js"></script> <script src="https://cdn.jsdelivr.net/npm/gojs@3.1.0"></script> <script src="../assets/js/goDoc.js"></script> <script> document.addEventListener("DOMContentLoaded", function () { if (window.go) document .getElementById('version') .textContent = "GoJS version " + go.version; if (window.goDoc) window.goDoc(); var d = window.diagrams; for (var i = 0; i < d.length; i++) { var dargs = d[i]; goCodeExecute(dargs[0], dargs[1], dargs[2], dargs[3], dargs[4]); } if (window.extra) window.extra(); } ); </script> </html>