Models and Views

Models and Views are two of the core constructs used by the Toolkit. They are both a set of Node, Edge and Port definitions, but the sorts of things they define vary: Models are used to define constraints on the data model, and Views are used to define connectivity rules (what can be connected to what else, limits on the number of edges, etc) as well as the visual appearance of the various artefacts (which template to use for Nodes/Ports, CSS classes, etc) and event registration.

Models are provided as an argument to jsPlumbToolkit.getInstance, in which you specify constraints on the data, and Views are provided as an argument to the render method of an instance of the Toolkit, in which you specify the visual appearance and behaviour of UI artefacts representing your data.

Example

This shows a view - it contains directives that are used to define the visual appearance of the various artefacts. But the basic structure is the same for Models: an object hash for each of nodes, edges and ports, with directives for each type.

view:{
  nodes:{
    type_a:{
      template:"TypeATemplate"
    },
    type_b:{
      template:"TypeBTemplate"
    }
  },
  edges:{
    "default":{
      connector:"Bezier"
    }
  },
  ports:{
    someType:{
      template:"PortTemplate"
    }
  }
}

Mapping Types

An instance of the Toolkit has the concept of a typeFunction - a function that, when given the data corresponding to some Node, Port or Edge, returns the object's type (as a string). The type returned by this function is what is used as the key into a Model or View.

Default Types

For each of Nodes, Ports and Edges, you can provide a default definition, using the key "default":

{
  ...
  view:{
    nodes:{
      "default":{
        template:"tmplNode"
      }
    }
  },
  ...
}

This definition will be used in two circumstances:

  • no type was derived for some object by the current typeFunction
  • a type was derived but there is no entry in the Model or View for that type
Default Edge Type

If you do not specify an edgeType parameter on a Port definition, the type will be set to default.

Definition Inheritance

Any Node, Port or Edge definition can declare a parent definition. The resulting definition consists of the parent's entries, with the child's entries merged on top. An example:

{
    ...
    view:{
      nodes:{
        "common":{
          events:{
            "click":function(params) {
              console.log("Click on node", params.node);
            }
          }
        },
        bigNode:{
          parent:"common",
          template:"tmplBigNode"
        },
        smallNode:{
          parent:"common",
          template:"tmplSmallNode"
        }
      }
    },
    ...
}

Here, we have defined a common click handler on the parent definition, and then defined templates for each Node type in their own definitions.

Models

The jsPlumbToolkit.getInstance method call gets you an instance of the Toolkit that is 'headless', ie. it does not render to any UI elements. When you work with a Toolkit instance you work strictly on the data, so the model you pass in to this method is used to define constraints on the data and has nothing to do with the appearance or behaviour of a Node/Port/Edge.

Example

var toolkit = jsPlumbToolkit.getInstance({
  model:{
    nodes:{
      "default":{
        maxConnections:3
      }
    }
  }
});

Views

When you call the render method on an instance of the Toolkit, and provide a view, your View contains configuration for the visual appearance and behaviour of the various artefacts.

Example

This example is an edited View from the Database Visualizer example application.

toolkit.render({
  view:{
    // we have two node types - 'table' and 'view'.  
    nodes:{
      "table":{
        // we use 'tmplTable' to render tables
        template:"tmplTable"
      },
      "view":{
        // and 'tmplView' to render views
        template:"tmplView",
        events:{
          // when you click a view Node, we alert its id.
          click:function(params) {
            alert("click on view " + params.node.id);
          }
        }
      }
    },
    edges:{
      // common appearance of all edges
      "common":{
        connector:"StateMachine",
        paintStyle:{ lineWidth:2, strokeStyle:"#CCC" } 
      },
      // a 1:1 relationship
      "1:1":{
        parent:"common",  // declared 'common' as its parent.
        overlays:[
          ["Label", {label:"1", location:0.1 }],
          ["Label", {label:"1", location:0.9 }]
        ]
      },
      "1:N":{
        parent:"common",
        overlays:[
          ["Label", {label:"1", location:0.1 }],
          ["Label", {label:"N", location:0.9 }]
        ]
      }
    },
    ports:{
      // a table has an arbitrary number of columns; it is a table's columns that actually connect to other tables, not a table itself.
      "column":{
        // use 'tmplColumn' to render a Port of type 'column'
        template:"tmplColumn",
        // the appearance of the endpoint on a column
        endpoint:[ "Dot", { radius:7 } ],
        // anchor locations on a column
        anchor:[ "Left", "Right" ],
        // the type of edge that will be created from this port by default when the user drags a connection
        edgeType:"common"
      }
    }
  }
});

Let's go through this piece by piece.

Nodes

There are two node types defined here - table and view. They each have a single entry that defines the template to use to render a node of this type. The value for this parameter must be the id of some client side template in the format that is appropriate for the template engine you are using. See below for a discussion of templates.

In addition, the view Node declares an event handler for Node clicks. There are many different events to which you can subscribe; these are discussed below.

Supported Parameters
  • [parent] ID of a Node definition from which this definition should inherit properties.
  • template ID of the template to use to render the Node
  • [events] JS object containing mappings of event names to handler functions
  • [dragOptions] JS object containing options for drag behaviour for nodes of the given type. The allowed values in this object are anything that the underlying drag library - Katavorio supports. Common uses of per-node dragOptions are such things as setting handles for dragging, or specifying a filter for which parts of an element should not be able to initiate a drag.

Edges

In the Database Visualizer, an edge maps the concept of a relationship between two tables. This View shows three entries in the edges section. The first, common, contains directives that are common to all of the different relationships.

The second and third entries - 1:1 and 1:N - are concrete edge types, defining a 1:1 and a 1:N relationship respectively. Notice how they both declare their parent to be common, so they derive the styling directives from that type. Then each one declares two overlays that are unique to itself.

There is no limit to the depth of inheritance supported, but multiple inheritance is not supported. It may have occurred to you that we said each of the two concrete edge types declares two overlays that are unique to itself, but in fact that is not the case: they both share the "1" overlay positioned at 0.1. So with no limit on inheritance we could have actually defined some other abstract type:

edges:{

   ...common...

   "1:something":{
     parent:"common",
     overlays:[
       ["Label", {label:"1", location:0.1 }]
     ]
   },
   "1:1":{
     parent:"1:something",
     overlays:[
       ["Label", {label:"1", location:0.9 }]
     ]
   },
   "1:N":{
     parent:"1:something",
     overlays:[
       ["Label", {label:"N", location:0.9 }]
     ]
   }
}

...it all depends on how complex you want or need to be.

Edges are rendered by jsPlumb and are therefore styled using jsPlumb directives - anything that is valid on a call to jsPlumb.connect is valid in an Edge definition.

Note in the Database Visualizer app, there is actually a third edge type, not shown here: N:M.

Supported Parameters
  • [parent] ID of an Edge definition from which this definition should inherit properties.
  • [connector] Name/definition of the jsPlumb connector to use. If you omit this, the default jsPlumb connector will be used.
  • [paintStyle] Definition of the paint style to use. If you omit this, the default jsPlumb paint style will be used.
  • [hoverPaintStyle] Definition of the hover paint style to use. If you omit this, the default jsPlumb hover paint style will be used (which is null, unless you have set it otherwise).
  • [events] JS object containing mappings of event names to handler functions

Ports

This View shows one type of Port - a column. Recall from the data model discussion that a Node has one implicit Port (itself), and an arbitrary number of other Ports. In the case of the Database Visualizer, a Node represents a table, and a Port represents a column. The table's "implicit" Port is not used in the Database Visualizer application. In the Flowchart Builder, each object type's implicit Port is used when the object is the target of some Edge.

As with Nodes, Ports can be associated with a template for rendering. The majority of applications will require this functionality: if your application supports the dynamic addition of new Ports - as in the case of the Database Visualizer when the user adds a new column to a table - then you need to provide the Toolkit with a template to use to render the Port.

The concept of Ports is synonymous with the concept of Endpoints in jsPlumb. Valid values for a Port definition are anything that is valid on a jsPlumb.addEndpoint or jsPlumb.makeSource call: essentially, a description of the appearance and behaviour of the endpoints associated with the Port. Here we are using a Dot endpoint of radius 7, and instructing the Toolkit to use a dynamic anchor with Left and Right locations.

Limiting Connections

A Port may support an arbitrary number of Edges. You can limit this with the maxConnections argument in the Port definition, but note that you would ideally set this in the Toolkit's model rather than in a View, as setting it here will set it only for the current renderer.

The default maximum - which is jsPlumb's default - maximum is 1 Edge. A value of -1 means there is no upper limit.

Edge Type

Notice in the column Port there is an edgeType parameter. This tells the Toolkit what type of Edge to create when a new connection is dragged from the Port. In this example we are using the type common, because when the user drags a new relationship between two tables we do not yet know the cardinality of the relationship.
The Database Visualizer subscribes to the edgeAdded event from the Toolkit and prompts the user to specify the cardinality of the new Edge; the Edge is then assigned the appropriate type.

Dragging Connections

As mentioned above, a Port is synonymous with the concept of an Endpoint in jsPlumb. By default, Endpoints in jsPlumb do not have mouse support enabled. You need to indicate whether or not the Endpoint should be allowed to be the source and/or target of connections created with the mouse through the use of the isSource/isTarget parameters.

Supported Parameters
  • [parent] ID of a Port definition from which this definition should inherit properties.
  • template ID of the template to use to render the Node
  • [edgeType] Type of Edge to create when a new connection is dragged from the Port
  • [maxConnections] Defaults to 1. The maximum number of connections the Port allows. Set to -1 to allow unlimited connections.
  • [events] JS object containing mappings of event names to handler functions
  • [isSource=false] If true, indicates the Port can be a source for Edges dragged with the mouse.
  • [isTarget=false] If true, indicates the Port can be a target for Edges dragged with the mouse.

Events

Each type of object in the View supports declarative registration of events, through the inclusion of an events member in the appropriate definition. The list of supported events is as follows:

  • click
  • dblclick
  • mouseover
  • mouseout
  • mousedown
  • mouseup
  • contextmenu
view:{
  nodes:{
    someNodeType:{
      events:{
        click:function(params) {
          // params.node is the Toolkit Node
          // params.el is the element from the DOM representing the Node
        }
      }
    }
  },
  edges:{
    someEdgeType:{
      events:{
        mouseover:function(params) {
          // params.edge is the Toolkit Edge
          // params.connection is a jsPlumb Connection object.
          console.dir(params.edge, params.connection);
        }
      }
    }
  },
  ports:{
    somePortType:{
      events:{
        mousedown:function(params) {
          // params.port is the Toolkit Port
          // params.endpoint is a jsPlumb Endpoint object
          console.dir(params.port, params.endpoint);
        }
      }
    }
  }
}

Interceptors

Interceptors are jsPlumb events that can be used to cancel some proposed activity. jsPlumb currently supports two of these:

  • beforeDrop Called when the user has attempted to drop a connection. Returning anything other than true aborts the connection.
  • beforeDetach Called when the user has attempted to detach a connection. Returning anything other than true aborts the detachment of the connection.

You can map these in the View by including an interceptors object. They are only supported on Port definitions, as they are functionality that is handled by the underlying Endpoints.

An example:

var tk = jsPlumbToolkit.newInstance();
tk.render({
  container:"foo",
  view:{
    ...
    ports:{
      "default":{
        template:"tmplFoo",
        interceptors:{
          beforeDrop:function(params) {
            // returning anything but true will cause the connection to be aborted.
          },
          beforeDetach:function(params) {
            // returning anything but true will cause the detach to be aborted.
          }
        }
      }
    }
  }
});

The contents of params is as follows:

  • connection associated jsPlumb connection
  • scope scope of the edge
  • source source info
    • el:source DOM element
    • id:id of the source Node or Port.
    • obj: Source Node or Port
    • type:"Node" or "Port"
  • target target info
    • el:target DOM element
    • id:id of the target Node or Port.
    • obj: Target Node or Port
    • type:"Node" or "Port"

Preconfigured Parameters

Note Preconfigured parameters are known to break 2-way data binding when using the Toolkit with AngularJS, because the data object passed to the template is a copy of the original. To switch off preconfigured parameters, set enhancedview:false in your render call.

You may have a template that you'd like to reuse for a collection of nodes, with slight variations between each.
Consider this example:

<script type="jtk" id="tmplRectangle-svg">
  <svg style="position:absolute;left:0;top:0;" version="1.1" xmlns="http://www.w3.org/1999/xhtml">
    <rect width="${width}" height="${height}" x="${strokeWidth}" y="${strokeWidth}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" transform="rotate(${rotate} ${(width / 2) + strokeWidth} ${(height/2) + strokeWidth})"/>
  </svg>
</script>

This template draws SVG rectangles, of any given size, with arbitrary fill and stroke, and possibly rotated. Let's say we're creating an application in which there are two types of rectangle shapes: big red ones, and little yellow ones. In our View we can define custom parameters for each of these node types, which will be mixed in with the node's data when it comes time to render:

view:{
  nodes:{
    "bigRed":{
      template:"tmplRectangle",
      parameters:{
        width:250,
        height:250,
        fill:"red"
      }
    },
    "smallYellow":{
      template:"tmplRectangle",
      parameters:{
        width:50,
        height:50,
        fill:"yellow"
      }
    }
  }
}

Preconfigured Parameters & Inheritance

Preconfigured parameters are merged when one definition inherits from another, but only to the first level. Consider this arrangement:

view:{
  nodes:{
    "base":{
      template:"tmplRectangle",
      parameters:{
        lineWidth:2,
        foo:{
          bar:"baz"
        }
      }
    },
    "bigRed":{
      parent:"base",
      parameters:{
        width:250,
        height:250,
        fill:"red",
        foo:{
          qux:"FOO"
        }
      }
    },
    "smallYellow":{
      parent:"base",
      parameters:{
        width:50,
        height:50,
        fill:"yellow"
      }
    }
  }
}

Note the foo parameter is declared in all three definitions. After merging, the two concrete node definitions have these values:

"bigRed":{
  parent:"base",
  parameters:{
    width:250,
    height:250,
    fill:"red",
    foo:{
      qux:"baz"
    }
  }
}

"smallYellow":{
  parent:"base",
  parameters:{
    width:50,
    height:50,
    fill:"yellow",
    foo:{
      bar:"baz"
    }
  }
}

So the foo entry in bigRed would completely overwrite the foo entry from base; the two are not merged together. smallYellow does not have foo entry and therefore inherits it from base.

Note Preconfigured parameters operate at the view level only: they are not written into your data model. So you cannot use this mechanism to provide parameters that you will subsequently update (such as the w/h parameters that the Flowchart Builder demo uses: if provided via the preconfigured parameters mechanism the UI would initially render correctly, but if the w/h values were updated, the template would not re-render. In the Flowchart Builder, the solution is to provide the w/h in the data model).

Switching off Preconfigured Parameters

As mentioned above, there are some cases in which you cannot use preconfigured parameters. One such known case is when you are using AngularJS and you're taking advantage of the two-way data binding provided by its template engine.
Preconfigured parameters (and function parameters, discussed below), cause a copy of the original data to be created, which then breaks Angular's two-way binding. A copy is created because the only other option is to copy the preconfigured parameters into the original data, which is almost certainly not what you want, given that they are a view concern.

To switch off preconfigured parameters and function parameters, set the enhancedView flag on your render call to false:

_toolkit.render({
  container:"someElement",
  view:{
    nodes:{
      "circleNodeDef" : {
        template:"tmplCircle",
        parameters:{
          lineWidth:5,
          radius:10,
          fill:"black"
        }
      }
    }
  },
  enhancedView:false
});

Function Parameters

Note Function parameters are known to break 2-way data binding when using the Toolkit with AngularJS, because the data object passed to the template is a copy of the original. To switch off function parameters, set enhancedView:false in your render call.

By default you can provide values in definitions as functions. These will be given the backing data for the object being rendered. An example definition:

_toolkit.render({
  container:"someElement",
  view:{
    nodes:{
      "circleNodeDef" : {
        template:"tmplCircle",
        parameters:{
          lineWidth:5,
          radius:10,
          fill:"black",
          stroke:function(data) { return data.error ? "red" : "green"; }
        }
      }
    }
  }
});

If we were to make this call:

var node = _toolkit.addNode({
  type:"circleNodeDef",
  error:true
});

we'd end up with a circle that has a red outline. Otherwise we'd get a green outline.

Switching off Function Parameters

See Switching off Preconfigured Parameters.