UI / Views

Views

Views are used to define the visual appearance of the various artefacts (which template to use for Nodes/Ports, CSS classes, etc) and event registration. If you want to read about defining connectivity rules, see the Data Model documentation.

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 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"
      }
    }
  }
});

TOP


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, Edge or Group, returns the object's type (as a string). The type returned by this function is what is used as the key into a 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 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.

TOP


Definition Inheritance

Any Node, Port, Edge or Group 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.

TOP


Nodes

There are two node types defined in the example above - 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

All parameters are optional.

  • 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
  • label Either a plain string or a parameterised string (eg "${someValue}"). If present, a Label overlay will be created using this value. By default, this overlay will be at location 0.5 (halfway along the path of the edge), but this can be changed by setting a value for labelLocation in the backing data for an edge.
  • labelLocationAttribute Optional. If defined, nominates the name of the attribute within each edge's backing data from which to derive the location of the label overlay. The default value of this parameter is, effectively, labelLocation, as discussed above.

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. they do no have to be - in some cases, you will want to use an Endpoint to represent a Port, and in others you'll want to use a DOM element. If you're using DOM elements and 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 separate template to use to render the Port (as opposed to rendering the Ports inside of each Node's template).

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.

Specifying Endpoints vs DOM elements

When jsPlumb encounters a jtk-port element in a template, it knows that you wish to use an Endpoint to show the Port, as opposed to using a DOM element. If, though, you add a Port programmatically to some Node, and you wish to show that as an Endpoint, you need to tell jsPlumb, in the appropriate Port definition:

view:{
  ...
  ports:{
    "someEndpointPort":{
      isEndpoint:true
      ...
    }
  }
}
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.
  • [isEndpoint=false] Instructs jsPlumb that any Ports of this type that are programmatically added to a Node should result in the addition of an Endpoint to the UI. Defaults to false, meaning that Ports added programmatically configure the entire element as a source or target.

Groups

The Database Visualizer does not make use of the concept of Groups. For a full discussion on Groups, including how to map them inside Views, take a look at this page.

TOP


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.toolkit is the Toolkit instance
          // params.renderer is the Surface widget
          // params.e is the original mouse event 
          // params.node is the Toolkit Node
          // params.el is the element from the DOM representing the Node
        }
      }
    }
  },
  edges:{
    someEdgeType:{
      events:{
        mouseover:function(params) {
          // params.toolkit is the Toolkit instance
          // params.renderer is the Surface widget
          // params.e is the original mouse event
          // 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.toolkit is the Toolkit instance
          // params.renderer is the Surface widget
          // params.e is the original mouse event
          // params.port is the Toolkit Port
          // params.endpoint is a jsPlumb Endpoint object
          console.dir(params.port, params.endpoint);
        }
      }
    }
  }
}

TOP


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"

TOP


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}" {{if rotate}}transform="rotate(${rotate} ${(width / 2) + strokeWidth} ${(height/2) + strokeWidth})"{{/if}}/>
  </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).

TOP


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
});

TOP


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.