Using GoJS with Angular
Examples of most of the topics discussed on this page can be found in the gojs-angular-basic project, which serves as a simple starter project.
If you are new to GoJS, it may be helpful to first visit the Getting Started Tutorial.
The easiest way to get a component set up for a GoJS Diagram is to use the gojs-angular package, which exports Angular Components for GoJS Diagrams, Palettes, and Overviews. More information about the package, including the various props it takes, can be found on the NPM page. Our examples will be using a GraphLinksModel, but any model can be used.
You can see a sample project using all GoJS / Angular Components here.
General Information
Installation
To use the published components, make sure you install GoJS and gojs-angular: npm install gojs gojs-angular
.
About Component Styling
Whether you are using the published Diagram, Palette, or Overview Angular / GoJS Components, you will probably want to style them. First, you'll need to style a CSS class for the div of your GoJS Diagram / Palette / Overview such as:
/* app.component.css */ .myDiagramDiv { background: whitesmoke; width: 800px; height: 300px; border: 1px solid black; }
To style the GoJS Diagram / Palette / Overivew div, which will reside in the Angular / GoJS
Component(s) you are using, make sure you set encapsulation: ViewEncapsulation.None
in the @Component
decorator
of the component holding your Angular / GoJS Component(s). Without this, your styling will not effect the
component divs.
Read more about Angular view encapsulation here.
Your @Component
decorator for the component holding the your GoJS / Angular Component(s) should
look something
like:
@Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'], encapsulation: ViewEncapsulation.None })
The DataSyncService
The gojs-angular
package comes with an Angular service
called DataSyncService (demonstrated later), which is used to
easily merge changes (a go.IncrementalData
instance).
This service has three static functions:
syncNodeData(changes, array)
- Merges any node data changes in a go.IncrementalData object with a given array of node data, then returns the new arraysyncLinkData(changes, array)
- Merges any link data changes in a go.IncrementalData object with a given array of link data, then returns the new array. Note: Ensure you set the linkKeyProperty if you are using GraphLinksModel, so data merging is possible.syncModelData(changes, object)
- Merges any modelData changes in a go.IncrementalData object with a given modelData object, then returns the new object
These functions should allow you to keep your data synced up as needed, without needing to write lots of code.
Listening for Model Changes
It is common practice to want to listen for when data changes in a Diagram or Palette, then do something with
those changes on an application-level (such as data syncing). That's why, for both the DiagramComponent and
PaletteComponent, there is a modelChange
@Input property function (more info below).
Note that the UndoManager should always be enabled to allow for transactions to take place, but the UndoManager.maxHistoryLength can be set to 0 to prevent undo and redo.
Using the Diagram Component
Diagram Component accepts several @Input()
Angular properties, some of which
are
optional. They are:
initDiagram
- A function that must return a GoJS Diagram. You may define your Diagram's Node and Link templates here.divClassName
- A class name for your Diagram divnodeDataArray
- An array containing data objects for your nodeslinkDataArray
- An array containing data objects for your linksmodelData
- A data object, containing your diagram's model.modelData. This property is optional.skipsDiagramUpdate
- A boolean flag, specifying whether the component should skip updating, often set when updating state from a GoJS model change.modelChange
- A function, which accepts a go.IncrementalData object. This function will fire when your Diagram's model changes, allowing you to decide what to do with those changes. A common practice is to sync your app-level data to reflect the changes in the diagram model, which is made simple using the DataSyncServicegojs-angular
ships with.
For example, these properties may look something like this:
public initDiagram(): go.Diagram { const $ = go.GraphObject.make; const dia = $(go.Diagram, { 'undoManager.isEnabled': true, // must be set to allow for model change listening // 'undoManager.maxHistoryLength': 0, // uncomment disable undo/redo functionality model: $(go.GraphLinksModel, { linkKeyProperty: 'key' // IMPORTANT! must be defined for merges and data sync when using GraphLinksModel } ) }); // define the Node template dia.nodeTemplate = $(go.Node, 'Auto', { toLinkable: true, fromLinkable: true }, $(go.Shape, 'RoundedRectangle', { stroke: null }, new go.Binding('fill', 'color') ), $(go.TextBlock, { margin: 8 }, new go.Binding('text', 'key')) ); return dia; } public diagramNodeData: Array= [ { key: 'Alpha', color: 'lightblue' }, { key: 'Beta', color: 'orange' }, { key: 'Gamma', color: 'lightgreen' }, { key: 'Delta', color: 'pink' } ]; public diagramLinkData: Array = [ { key: -1, from: 'Alpha', to: 'Beta' }, { key: -2, from: 'Alpha', to: 'Gamma' }, { key: -3, from: 'Beta', to: 'Beta' }, { key: -4, from: 'Gamma', to: 'Delta' }, { key: -5, from: 'Delta', to: 'Alpha' } ]; public diagramDivClassName: string = 'myDiagramDiv'; public diagramModelData = { prop: 'value' }; public skipsDiagramUpdate = false; // When the diagram model changes, update app data to reflect those changes public diagramModelChange = function(changes: go.IncrementalData) { // when setting state here, be sure to set skipsDiagramUpdate: true since GoJS already has this update // (since this is a GoJS model changed listener event function) // this way, we don't log an unneeded transaction in the Diagram's undoManager history this.skipsDiagramUpdate = true; this.diagramNodeData = DataSyncService.syncNodeData(changes, this.diagramNodeData); this.diagramLinkData = DataSyncService.syncLinkData(changes, this.diagramLinkData); this.diagramModelData = DataSyncService.syncModelData(changes, this.diagramModelData); };
Once you've defined your @Input
properties for your DiagramComponent, pass these properties
to your DiagramComponent in your template, like so:
<gojs-diagram [initDiagram]='initDiagram' [nodeDataArray]='diagramNodeData' [linkDataArray]='diagramLinkData' [modelData]='diagramModelData' [skipsDiagramUpdate]='skipsDiagramUpdate' (modelChange)='diagramModelChange($event)' [divClassName]='diagramDivClassName'> </gojs-diagram>
You will now have a GoJS Diagram working in your Angular application.
Using the Palette Component
The Palette Component accepts the following Angular@Input()
properties.
initPalette
- A function that must return a GoJS Palette. You may define your Palette's Node and Link templates here.divClassName
- A class name for the div your Palette divnodeDataArray
- An array containing data objects for your nodeslinkDataArray
- An array containing data objects for your linksmodelData
- A data object, containing your palette's model.modelData. This property is optional.modelChange
- A function, which accepts a go.IncrementalData object. This function will fire when your Palette's model changes, allowing you to decide what to do with those changes. A common practice is to sync your app-level data to reflect the changes in the palette model, which is made simple using the DataSyncServicegojs-angular
ships with.
Define these properties in your component that will hold that Palette Component, such as:
public initPalette(): go.Palette { const $ = go.GraphObject.make; const palette = $(go.Palette); // define the Node template palette.nodeTemplate = $(go.Node, 'Auto', $(go.Shape, 'RoundedRectangle', { stroke: null }, new go.Binding('fill', 'color') ), $(go.TextBlock, { margin: 8 }, new go.Binding('text', 'key')) ); palette.model = $(go.GraphLinksModel, { linkKeyProperty: 'key' // IMPORTANT! must be defined for merges and data sync when using GraphLinksModel }); return palette; } public paletteNodeData: Array= [ { key: 'PaletteNode1', color: 'firebrick' }, { key: 'PaletteNode2', color: 'blueviolet' } ]; public paletteLinkData: Array = [ { from: 'PaletteNode1', to: 'PaletteNode2' } ]; public paletteModelData = { prop: 'val' }; public paletteDivClassName = 'myPaletteDiv'; public paletteModelChange = function(changes: go.IncrementalData) { this.paletteNodeData = DataSyncService.syncNodeData(changes, this.paletteNodeData); this.paletteLinkData = DataSyncService.syncLinkData(changes, this.paletteLinkData); this.paletteModelData = DataSyncService.syncModelData(changes, this.paletteModelData); };
Then pass these properties to your Palette Component in your template, like:
<gojs-palette [initPalette]='initPalette' [nodeDataArray]='paletteNodeData' [linkDataArray]='paletteLinkData' [modelData]='paletteModelData' (modelChange)='paletteModelChange($event)' [divClassName]='paletteDivClassName'> </gojs-palette>
You should now have a GoJS Palette Component working in your Angular application.
Using the Overview Component
The Overview Component accepts the following Angular @Input()
properties.
initOverview
- A function that must return a GoJS Overview.divClassName
- A class name for your Overview divobservedDiagram
- The GoJS Diagram this Overview observes
Define these properties in the component that will hold your Overview Component, like:
public oDivClassName = 'myOverviewDiv'; public initOverview(): go.Overview { const $ = go.GraphObject.make; const overview = $(go.Overview); return overview; } public observedDiagram = null;
Then pass these properties to your Overview Component in your template, like:
<gojs-overview [initOverview]='initOverview' [divClassName]='oDivClassName' [observedDiagram]='observedDiagram'> </gojs-overview>
But, we're not done yet. observedDiagram
is null, so the Overview will observe anything.
To assign your Overview a Diagram to observe, you will have to reassign the observedDiagram
property after initialization. To do so,
reassign the bound observedDiagram
property in your component holding your Overview
Component in the ngAfterViewInit
lifecycle hook.
Note: To avoid a ExpressionChangedAfterItHasBeenCheckedError
, you must inform
Angular
to then detect changes.
This can be done with the ChangeDetectorRef.detectChanges()
method. You can inject a ChangeDetectorRef instance
into your wrapper Component constructor, and use that after you alter observedDiagram
to call
detectChanges(). Like so:
constructor(private cdr: ChangeDetectorRef) { } public ngAfterViewInit() { if (this.observedDiagram) return; // in this snippet, this.myDiagramComponent is a reference to a GoJS/Angular Diagram Component // that has a valid GoJS Diagram this.observedDiagram = this.myDiagramComponent.diagram; // IMPORTANT: without this, Angular will throw ExpressionChangedAfterItHasBeenCheckedError (dev mode only) this.cdr.detectChanges(); }
Now, after initialization, your Overview should display appropriately.
Updating Properties Based on App State
You may have some app-level properties you want to effect the behavior / appearance of your Diagram, Palette,
or Overview. You could subclass their respective components and add @Input
bindings with specific
setter methods, or, more simply, you can have an ngOnChanges
function in your app-level component
that updates various Diagram / Palette / Component properties based on your app state.
For example, say you have an app-level property called showGrid
. When showGrid
is
true, your Diagram's grid should be visible -- when false, it should be invisible. In your AppComponent, you
could do something like:
// myDiagramComponent is a reference to your DiagramComponent @ViewChild('myDiagram', { static: true }) public myDiagramComponent: DiagramComponent; public ngDoCheck() { // whenever showGrid changes, update the diagram.grid.visible in the child DiagramComponent if (this.myDiagramComponent && this.myDiagramComponent.diagram instanceof go.Diagram) { this.myDiagramComponent.diagram.grid.visible = this.showGrid; } }