-
Notifications
You must be signed in to change notification settings - Fork 105
Home
Spring Flo is a set of Angular JS directives for a diagram editor able to represent a DSL graphically and synchronize graphical and textual representation of that DSL. Graphical representation is done with a Joint JS graph object, textual representation can be either a plain HTML element (such as <textarea>
) or a feature rich editor based on CodeMirror available via the Flo directive.
Spring Flo can be used in any angular application via the flo-editor
directive. The controller for the directive creates/populates flo
(Flo) and definition
(Definition) objects. These objects are how a consuming client interoperates with Flo. The definition
object is a two-way binding to read/write the DSL textual representation. The flo
object is a two-way binding for working with the graphical representation of the DSL (including controlling editor features such as zooming, grid, layout etc.).
There is a sample project in the samples subfolder of the spring-flo repo: https://github.com/spring-projects/spring-flo/tree/master/samples/spring-flo-sample - this sample will be referenced throughout the documentation below to give a pointer to a real use of each feature.
The initial step of embedding Flo into angular application is simply to use the flo-editor
directive on a page or in angular template.
<flo-editor></flo-editor>
The above results in a page with a skeleton for the editor: empty palette and empty canvas. See the sample usage for a more sophisticated configuration. Flo defines UI for the editor and graph interactions but needs domain knowledge from the client to be actually useful. There are actually three services the consuming client can provide to control the behaviour of Flo, only one of which is mandatory:
- Service that defines the types of element being represented by the graph and converts both ways between the domain DSL textual format and the graph format (required)
- Service that defines the visual representation of the nodes and links on the graph
- Service that controls graph editing capabilities
Implementations should be provided by the client as angular services that adhere to a specific contract as detailed in Extension Points. The services are: Metamodel Service (required), Render Service and Editor Service.
Many of the service functions have sensible defaults. In this section we will discuss the important ones, only some of which are mandatory.
In order to populate the palette with content, Flo needs to know about your domain. What are the 'things' you are linking together in your graph? Implementing the Metamodel Service load() function will provide that information to Flo.
app.factory('MyPageMetamodelService', ['$log', function($log) {
return {
load: function() { $log.info('Return descriptions of the domain specific model elements'); }
};
}]);
The sample includes a more complete metamodel service. Once defined, the services can be supplied by name to the parents scope (via parent controller $scope.metamodelServiceName = 'MyPageMetamodelService';
.) or directly to the flo-editor
directive (via metamodel-service-name
attribute on <flo-editor>
element). Lets modify the flo-editor
directive:
<flo-editor metamodel-service-name="MyPageMetamodelService">
</flo-editor>
This is also how the sample index.html does it. Once the Metamodel Service is hooked up to the editor the palette will be populated based on the metamodel object obtained from the Metamodel Service. You should then be able to drop nodes on the canvas, connect nodes with links, move them around. Note that visual representation of nodes and links might be far from what is desired. Implementing a Render Service enables you to customize that look and feel (in the absence of a provided render service, Flo performs some very basic rendering). A basic render service will implement the createNode() and createLink() functions (see also Joint JS API Docs) to get the right look and feel for the shapes on the palette and canvas.
app.factory('MyPageRenderService', ['$log', function($log) {
return {
createNode: function(metadata) { $log.info('Create and return new Joint JS joint.dia.Element for a graph node'); },
createLink: function(source, target, metadata, props) { $log.info('Create and return new Joint JS joint.dia.Link for a link between graph nodes'); }
};
}]);
The sample includes a more complete render service. As with the metamodel service, connect it to the flo-editor
directive
<flo-editor
metamodel-service-name="MyPageMetamodelService"
render-service-name="MyPageRenderService">
</flo-editor>
With basic rendering taken care of, we could further customize:
- Adding "handles" around the shape when it is selected. "handles" are small shapes usually placed around the shape interaction with which would perform some editing actions on the associated shape such as delete, resize or edit properties.
- Provide validation of the graph with validation indicators (errors/warnings) on nodes and links.
- An editor action toolbar might be nice.
Some of these features are supported by implementing more functions on the render-service but some require implementing the final key service, the Editor Service:
app.factory('MyPageEditorService', ['$log', function($log) {
return {
createHandles: function(flo, createHandle, selected) { $log.info('Create handles around selected element'); },
validateLink: function(flo, cellViewS, portS, cellViewT, portT, end, linkView) { $log.info('Validate link');},
validateNode: function(flo, node) { $log.info('Validate Node'); },
preDelete: function(flo, deletedElement) { $log.info('Delete links incident with element being deleted '); },
interactive: true, /* Consult Joint JS Paper #interactive property doc. Make elements interactive */
allowLinkVertexEdit: false /* Disallow creation and editing of 'bend-points' in links */
};
}]);
The sample includes a more complete editor service. Rendering for handles and validation markers (decorations) must be provided by the Render Service since they are required by Editor Service. There is no out-of-the-box rendering defined in Spring-Flo for rendering handles and error marker decoration shapes. Consequently, if you define validateNode()
and createHandles()
in the Editor Service then createDecoration()
and createHandle
must be implemented in the Render Service.
app.factory('MyPageRenderService', ['$log', function($log) {
return {
createHandle: function(kind, parent) { $log.info('Implement handle node creation'); },
createDecoration: function(kind, parent) { $log.info('Implement error decoration node creation'); },
createNode: function(metadata) { $log.info('Create and return new Joint JS joint.dia.Element for a graph node'); },
createLink: function(source, target, metadata, props) { $log.info('Create and return new Joint JS joint.dia.Link for a link between graph nodes'); }
};
}]);
New toolbar actions can be added in HTML in the flo-editor
directive via transclusion, this is how that might look with our editor service also plugged in:
<flo-editor
metamodel-service-name="MyPageMetamodelService"
render-service-name="MyPageRenderService"
editor-service-name="MyPageEditorService">
<div>
<button ng-click="flo.deleteSelectedNode()" ng-disabled="!flo.getSelection()">Delete</button>
<button ng-click="flo.noPalette = !flo.noPalette" ng-class="{on:!flo.noPalette}">Palette</button>
</div>
</flo-editor>
Note the flo
object references in various handler attributes in HTML. The controller for flo-editor
directive will populate flo
object with functions for manipulating the Flo editor content.
The graph editor should be fully functional at this stage, but what about the text DSL?
Functions converting graph to text DSL and text DSL to graph must be provided by the client in order for this to work. These two function (graphToText() and textToGraph()) must be implemented inside the Metamodel Service, let's add them to the definition we had earlier:
app.factory('MyPageMetamodelService', ['$log', function($log) {
return {
load: function() { $log.info('Implement load and return metamodel object!'); },
textToGraph: function(flo, definition) { $log.info('Convert text (definition.text) to graph (flo.getGraph())'); },
graphToText: function(flo, definition) { $log.info('Convert graph (flo.getGraph()) to text (definition.text)'); }
};
}]);
The sample includes basic implementations of graph-to-text and text-to-graph for a 'demo' DSL, those are used by the sample metamodel service.
The Flo editor internally maintains the text DSL in the definition
object it only remains to be exposed in the UI. The text DSL can be displayed either by plain HTML elements such as <textarea>
or advanced CodeMirror based dsl-editor
directive. Lets modify the HTML to have the text DSL UI with help from the dsl-editor
directive.
<flo-editor
metamodel-service-name="MyPageMetamodelService"
render-service-name="MyPageRenderService"
editor-service-name="MyPageEditorService">
<div>
<button ng-click="flo.deleteSelectedNode()" ng-disabled="!flo.getSelection()">Delete</button>
<button ng-click="flo.noPalette = !flo.noPalette" ng-class="{on:!flo.noPalette}">Palette</button>
</div>
<textarea dsl-editor></textarea>
</flo-editor>
The HTML above translates into a page with toolbar for buttons (Layout and Show/Hide Palette), text area for DSL and the Flo editor for graph representation of the DSL.
This service enables the domain in which Flo is being used to specify what kinds of element are being connected together in the graph and also how the graph should be converted to-and-from a textual representation. Sample metamodel service is here.
Sets the graph contents for the flo
object based on the textual representation of the DSL from definition
object. Text is transformed into the corresponding Joint JS graph content. The graph is to be populated via flo
objects functions such as flo.createLink()
and flo.createNode()
and cleared with flo.clearGraph
Convert the current graph available from the flo
object into a textual representation which is then set (as the text
property) on the definition
object.
Returns a promise that resolves to a metamodel
object. The metamodel
object layout is a map of element group
names to a map of elements that belong to this group
. The map of elements that belong to the group
is a mapping between element's name
and element's Metadata Object
Refreshes the meta-model and returns a promise that is resolved to the same result as load(). Refresh should also fire event to metamodel
change listeners.
Encodes DSL element property value text to the DSL required format. Example is converting multiline text into a single line required by the DSL format. Used to display the property value in a human readable format.
Decodes DSL element property value text from DSL format. Example is converting single line text into a multiline text, i.e. replacing escaped line breaks. Used to set a property value for DSL element entered by the user via UI.
Adds a listener to metamodel
events. (See Metamodel Listener)
Removes metamodel
events listener. (See Metamodel Listener)
Check if the the value being specified for the key on the specified element is allowed. For example: if the key takes an integer, don't allow alphabetic characters.
The service is responsible for visual representation of graph elements based on the metadata (coming from Metamodel Service). This service is optional. Sample render service is here.
Creates an instance of Joint JS graph node model object (joint.dia.Element
). Parameters that may affect the kind of node model object are element's metadata and map of properties (if any passed in).
Creates an instance of Joint JS graph link model object (joint.dia.Link
). Parameters that may affect the kind of link model object are element's metadata, map of properties (if any passed in), source and target elements
Creates an instance of Joint JS graph node model object (joint.dia.Element
). An example of a handle is a shape shown next to the parent shape interacting with which results in some editing action over the parent shape. Parameters that may affect the kind of handle model object are kind
of type string
(user defined, i.e. delete
, resize
, etc.) and handle's parent
element. This function is only called by the framework if Editor Service createHandles()
function is implemented.
Creates an instance of Joint JS graph node model object (joint.dia.Element
). An example of decoration is a validation marker displayed over the parent shape. Parameters that may affect the kind of decoration model object are kind
of type string
and decoration's parent
element. Note that kind
parameter is coming from the framework (unlike for createHandle
function). This function is only called by the framework if Editor Service validateNode()
function is implemented. (At the moment decorations are only the validation error markers).
Performs any additional initialization of a newly created graph node
when node
is already added to the Joint JS graph and rendered on the canvas, e.g. element's SVG DOM structure is available. The context
parameter is an object with paper
and graph
properties applicable for the node
. Useful to perform any kind of initialization on a node when it's SVG DOM is appended to the page DOM. Examples: fit string label inside a shape, use angular directive on a shape, add DOM listeners etc.
Performs any additional initialization of a newly created graph link
when link
is already added to the Joint JS graph and rendered on the canvas, e.g. element's SVG DOM structure is available. The context
parameter is an object with paper
and graph
properties applicable for the link
. Useful to perform any kind of initialization on a link when it's SVG DOM is appended to the page DOM. Examples: use angular directive on a shape, add DOM listeners etc.
Performs any additional initialization of a newly created graph handle
when handle
is already added to the Joint JS graph and rendered on the canvas, e.g. element's SVG DOM structure is available. The context
parameter is an object with paper
and graph
properties applicable for the handle
. Useful to perform any kind of initialization on a handle shape when it's SVG DOM is appended to the page DOM. Examples: fit string label inside a shape, use angular directive on a shape, add DOM listeners etc.
Performs any additional initialization of a newly created graph decoration
when decoration
is already added to the Joint JS graph and rendered on the canvas, e.g. element's SVG DOM structure is available. The context
parameter is an object with paper
and graph
properties applicable for the decoration
. Useful to perform any kind of initialization on a decoration shape when it's SVG DOM is appended to the page DOM. Examples: fit string label inside a shape, use angular directive on a shape, add DOM listeners etc.
Returns instance of joint.dia.ElementView
. It can also be a function of the form function(element)
that takes an element model and should return an object responsible for rendering that model onto the screen. Under normal circumstances this function does not need to be implemented and the Joint JS view object created by the framework should be enough. Implement this function if different nodes require different Joint Js views or view has some special rendering (i.e. embedded HTML elements). See Joint JS Paper Options
Returns instance of Joint JS joint.dia.LinkView
. Default is joint.dia.LinkView
. It can also be a function of the form function(link)
that takes a link model and should return an object responsible for rendering that model onto the screen. Under normal circumstances this function does not need to be implemented and the Joint JS view object created by the framework should be enough. Implement this function if different links require different Joint JS views or view has some special rendering (i.e. pattern applied to a line - joint.shapes.flo.PatternLinkView
). See Joint JS Paper Options
Responsible for laying out the Joint JS graph that can be derived from passed in paper
parameter (paper.model
).
Responsible for handling event
that occurred on the link
that belong to passed in Joint JS paper
object. The event
parameter is a string
with possible values: 'add'
, 'remove'
or Joint JS native link change events such as 'change:source'
, 'change:target'
, etc. see Joint JS Link Events
Returns true
for string
property attribute path propertyPath
on an element
if graphs needs to perform some visual update based on propertyPath
value change (Not needed for properties under props
on an element
). Visual update is performed by refreshVisuals(). The property path propertyPath
is relative to Joint JS element attrs
property
Performs some visual update of the graph or, which is more likely, the passed in element
displayed on Joint JS paper
based on the changed property specified by propertyPath
This function allows you to customize what are the anchor points of links. The function must return a point (with x
and y
properties) where the link anchors to the element. The function takes the link view, element view, the port
(SVG element) the link should stick to and a reference point (either the closest vertex or the anchor point on the other side of the link).
The service responsible for providing Flo editor with rich editing capabilities such as handles around selected shapes, custom drag and drop behaviour, live and static validation. This service is optional. Sample editor service is here
Called when node is selected and handles can be displayed. Handles are usually small shapes around the selected
Joint JS node in flo
editor interactions with which modify properties on selected
node, i.e. resize or delete handles. Call createHandle(selected, kind, clickHandlerFunction, coordinate)
function to create a handle. The kind
parameter is a string
kind of a handle, clickHandlerFunction
is performed when handle has been clicked on and coordinate
is the place to put the handle shape. Note that if this function is implemented then Render Service createHandle(...)
function must be implemented as well. The framework will remove handles automatically when needed, hence no need to worry about this on the client side.
Decide whether to create a link if the user clicks a port. The portView
is the DOM element representing the port, view
is the port's parent Joint JS view object show in Joint JS paper
Decide whether to allow or disallow a connection between the source view/port (cellViewS
/portS
) and target view/port (cellViewT
/portT
). The end
is either 'source'
or 'target'
and tells which end of the link is being dragged. This is useful for defining whether, for example, a link starting in a port POut of element A can lead to a port PIn of elmement B.
Called when dragging of a node draggedView
is in progress over targetUnderMouse
Joint JS graph element (node or link) at coordinate
. There are also flo
object parameter and context
object, which currently just has a boolean
property palette
to denote whether drag and drop occurring on the palette or canvas. The function should return a Drag Descriptor Object.
Performs necessary graph manipulations when the node being dragged is dropped. The dragDescriptor
Drag Descriptor should have the mandatory information on what is being dragged and where it's being dropped. The flo
object parameter would help to make necessary graph modifications
Any custom visual feedback when dragging a node over some graph element (node or link) can be drawn by this function. dragDescriptor
parameter has a Drag Descriptor Object that has complete information about dragging in progress and flo
object would help with drawing feedback using Joint JS
Removes any custom visual feedback drawn by showDragFeedback(). Has the same parameters.
Returns a javascript
array of string
error messages that are the result of validating node
Joint JS graph node on the canvas in flo
editor
Called prior to removal of the specified deletedElement
allowing extra tidyup before that happens. For example: removes any dependent Joint JS graph elements related to the element about to be deleted.
If set to false
, interaction with elements and links is disabled. If it is a function, it will be called with the cell view in action and the name of the method it is evaluated in ('pointerdown'
, 'pointermove'
, ...). If the returned value of such a function is false interaction will be disabled for the action. For links, there are special properties of the interaction object that are useful to disable the default behaviour. These properties are: vertexAdd
, vertexMove
, vertexRemove
and arrowheadMove
. By setting any of these properties to false, you can disable the related default action on links.
If set to false
link vertex (or bend point) creation or editing (e.g. movement) is not allowed in the editor.
This object is created by the flo-editor
directive controller and it contains various editor specific properties and functions.
Schedules an asynchronous update of the graph DSL representation based on the text DSL representation.
Asynchronously update the graph DSL representation based on the text DSL representation. A promise is returned which gets resolved when the update completes.
Asynchronously update the text DSL representation (definition
object) based on the graph DSL representation. A promise is returned which gets resolved when the update completes.
Arranges nodes and links of the graph on the canvas.
Clears out canvas of all nodes and links. With syncing on this also causes the text DSL representation to clear.
Returns a reference to joint.dia.Graph
object instance of the canvas contents (The graph model, see Joint JS Graph API
Returns a reference to joint.dia.Paper object instance of the canvas (The graph view object, see Joint JS Paper API
Enables or disables textual and graph DSL representation synchronization mechanism based on the passed boolean
parameter enable
. Useful when textual DSL representation UI is collapsed.
Returns currently selected graph model element (node or link) on the canvas
Angular getter/setter function for the zoom value on the canvas. Sets zoom percent value if the integer number
parameter is supplied. Returns the integer percent value if parameter is missing (getter mode)
Angular getter/setter function for the canvas grid size in pixels. Sets grid width value if the integer number
parameter gridSize
is supplied. Returns the current grid size value if parameter is missing (getter mode). Note that setting grid width to 1
turns the grid off. Invalid values for gridSize
are ignored
Returns integer number
minimum allowed value for the zoom percent. Useful to set the proper range for zoom controls. Needed by the zoom control on the canvas (if it is set to be shown). The value equals 5
by default (5%).
Returns integer number
maximum allowed value for the zoom percent. Useful to set the proper range for zoom controls. Needed by the zoom control on the canvas (if it is set to be shown). The value equals 400
by default (400%).
Returns integer number
zoom percent increment/decrement step. Needed by the zoom control on the canvas (if it is set to be shown). The value equals 5
by default (5% increment/decrement value).
Fits the whole graph into canvas's viewport (i.e. no need to scroll to look for content on the canvas). Adjusts the zoom level and scroll position appropriately
Angular getter/setter function for the canvas "read-only" property. Read-only canvas does not allow for any user editing interaction of any shapes on the canvas. Sets the read-only property based on the passed in newValue
parameter as the result the canvas toggles the behaviour for read-only state right away. Returns the current "read-only" state value if parameter is missing (getter mode).
Creates and returns the newly created Joint JS graph node (instance of joint.dia.Element
) based on the graph node metadata
object (see Element Metadata), properties
key-value pairs map, and location on the canvas (object with x
and y
properties). The new node is also added to the Flo canvas Joint JS graph
and hence to the Joint JS paper
and appears right away on the canvas before this function returns the result.
Creates and returns the newly created Joint JS graph link (instance of joint.dia.Link
) between source
and target
nodes (of type joint.dia.Element
) based on the graph link metadata
object (see Element Metadata), properties
key-value pairs map. The new link is also added to the Flo canvas Joint JS graph
and hence to the Joint JS paper
and appears right away on the canvas before this function returns the result.
This object holds data related to DSL's textual representation. Typically this object should at least have text
property of type string
for the DSL text, but it can also have other properties that might be added by client's Metamodel Service graph-text conversion functions.
Typically Metamodel object is loaded asynchronously via HTTP request. If metadata is cached by the service then it might be useful to register listeners. Flo editor palette would automatically rebuild itself if metamodel has changed
{
metadataError: function(data) {
/* Error loading metadata has occurred */
},
metadataRefresh: function() {
/* Metadata is about to be refreshed */
},
metadataChanged: function(data) {
/* New metadata is available */
}
}
API client is free to add extra properties to this object (i.e. may help drawing visual feedback)
{
context: context, /* String 'palette' or 'canvas' */
source: {
cell: draggedNode, /* Joint JS graph node being dragged */
selector: selector, /* Optional. Joint JS CSS class selector for the subelement of the dragged node*/,
port: portType /* Optional. Involved port DOM element type attribute value == port Joint JS markup 'type' property */
},
target: {
cell: targetNode, /* Joint JS graph node target under mouse element */
selector: selector, /* Optional. Joint JS CSS class selector for the element under mouse within the targetNode */
port: portType /* Optional. Sub-element under mouse is a port. Port DOM element type attribute value == port Joint JS markup 'type' property */
},
};
model: /* Joint JS model object for a module shape */
...
attributes:
...
angle: 0, /* Joint JS property - rotation angle */
id: "02be8001-ea1e-4f30-a94e-9503da5964b5" /* Joint JS property - element model UUID
position: /* Joint JS property - coordinates of the shape's bounding rectangle */
x: 119
y: 46
size: /* Joint JS property - size of the shape's bounding rectangle */
height: 40
width: 120
type: "sinspctr.IntNode" /* Flo property - internal, type (node, link, handle, decoration, etc) */
z: 1 /* Joint JS property - z-index of the shape
ports: /* Joint JS property - internal, ports available on the shape */
input:
id: "input"
output:
id: "output"
tap:
id: "tap"
attrs: /* Joint JS property - user defined rendering constructs and semantic properties */
. /*\ */
.border /* \ */
.box /* \ */
.input-port /* \ */
.label1 /* \___User defined rendering constructs implied by the markup */
.label2 /* / */
.output-port /* / */
.shape /* / */
.stream-label /* / */
.tap-port /*/ */
metadata: /* Flo property. Node metadata supplied by Metamodel Service */
props: /* Flo property. Semantic properties of the element. Name <-> value pair map */
dir: "/Users/x/tmp"
file: "temp.tmp"
debug: true
...
...
...
Graphical element metadata supplied by Metamodel Service
metadata: {
get: function(), /* function taking property key string as a parameter */
/* Returns promise that resolves to the metadata object of the property */
/* See snippet below showing the format of a property metadata */
group: "source", /* Category/Group of an element. Translates into palette groups of elements */
name: "file", /* Name or Type of an element (should be unique within its group) */
metadata: { /* Additional metadata for the element */
titleProperty: 'props/title', /* Property to be displayed at the top of all properties in properties Div */
noEditableProps: false, /* If true then element doesn't have properties to edit and properties Div is not shown */
allow-additional-properties: true, /* Allows user to create new properties for element in the properties Div */
}
}
Element's property metadata is expected to be as follows
properties: {
info: {
defaultValue: null,
description: "General information about the file",
id: "info",
name: "info",
shortDescription: "File Info"
},
language: {
defaultValue: "English"
description: "Language of the file contents",
id: "language",
name: "language",
shortDescription: "Text Language"
},
...