We are considering purchasing GoJS so we have started evaluating the product. Because we are using Angular, we want to use the gojs-angular package and see if we can implement some of our requirements.
We are trying to reproduce the code demonstrated here Simple Block Editor but using the gojs-angular wrapper.
So we downloaded the gojs-angular-basic sample and added the relevant code to show a context menu when clicking on a shape.
We can display the menu with a list of figure buttons to change the shape of the selected node.
ClickFunction gets called and we see that m.set(obj.part.adornedPart.data, propname, value)
sets the correct values.
However, the shape in the diagram does not get updated. We believe this happens because angular change detection does not get fired. What is the correct way to do this ?
Also, we have added a TextBlock menu item ‘edit task’. In the click event, we are just logging the click event, which works fine. But how would we call a method located in my actual component ?
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
encapsulation: ViewEncapsulation.None
})
export class AppComponent {
@ViewChild('myDiagram', { static: true }) public myDiagramComponent: DiagramComponent;
@ViewChild('myPalette', { static: true }) public myPaletteComponent: PaletteComponent;
// initialize diagram / templates
public initDiagram(): go.Diagram {
const $ = go.GraphObject.make;
const dia = $(go.Diagram, {
'undoManager.isEnabled': true,
model: $(go.GraphLinksModel,
{
linkToPortIdProperty: 'toPort',
linkFromPortIdProperty: 'fromPort',
linkKeyProperty: 'key' // IMPORTANT! must be defined for merges and data sync when using GraphLinksModel
}
)
});
dia.commandHandler.archetypeGroupData = { key: 'Group', isGroup: true };
const makePort = (id: string, spot: go.Spot) => {
return $(go.Shape, 'Circle',
{
opacity: .5,
fill: 'gray', strokeWidth: 0, desiredSize: new go.Size(8, 8),
portId: id, alignment: spot,
fromLinkable: true, toLinkable: true
}
);
}
// define the Node template
dia.nodeTemplate =
$(go.Node, 'Spot',
{
contextMenu:
$('ContextMenu',
$('ContextMenuButton',
$(go.TextBlock, 'Group'),
{ click: (e, obj) => { e.diagram.commandHandler.groupSelection(); } },
new go.Binding('visible', '', (o) => {
return o.diagram.selection.count > 1;
}).ofObject())
)
},
$(go.Panel, 'Auto',
$(go.Shape, 'RoundedRectangle', { stroke: null },
new go.Binding('fill', 'color')
),
$(go.TextBlock, { margin: 8 },
new go.Binding('text', 'key'))
),
// Ports
makePort('t', go.Spot.TopCenter),
makePort('l', go.Spot.Left),
makePort('r', go.Spot.Right),
makePort('b', go.Spot.BottomCenter)
);
dia.nodeTemplate.contextMenu =
$("ContextMenu",
$("ContextMenuButton",
$(go.Panel, "Horizontal",
FigureButton("Rectangle"), FigureButton("RoundedRectangle"), FigureButton("Ellipse"), FigureButton("Diamond")
)
),
$("ContextMenuButton", $(go.Panel, "Horizontal",
$(go.TextBlock, 'edit task', { click: e => console.log('clicked') }))
)
);
function FigureButton(fig, propname = 'figure') {
return $(go.Shape,
{
width: 32, height: 32, scale: 0.5, fill: "lightgray", figure: fig,
margin: 1, background: "transparent",
mouseEnter: (e, shape) => { shape.fill = "dodgerblue"; },
mouseLeave: (e, shape) => { shape.fill = "lightgray"; },
click: ClickFunction(propname, fig), contextClick: ClickFunction(propname, fig)
});
}
// PROPNAME is the name of the data property that should be set to the given VALUE.
function ClickFunction(propname, value) {
return (e, obj) => {
e.handled = true; // don't let the click bubble up
e.diagram.skipsDiagramUpdate = false;
e.diagram.model.commit((m) => {
m.set(obj.part.adornedPart.data, propname, value);
});
};
}
return dia;
}
public diagramNodeData: Array<go.ObjectData> = [
{ key: 'Alpha', color: 'lightblue', arr: [1, 2] },
{ key: 'Beta', color: 'orange' },
{ key: 'Gamma', color: 'lightgreen' },
{ key: 'Delta', color: 'pink' }
];
public diagramLinkData: Array<go.ObjectData> = [
{ key: -1, from: 'Alpha', to: 'Beta', fromPort: 'r', toPort: '1' },
{ key: -2, from: 'Alpha', to: 'Gamma', fromPort: 'b', toPort: 't' },
{ key: -3, from: 'Beta', to: 'Beta' },
{ key: -4, from: 'Gamma', to: 'Delta', fromPort: 'r', toPort: 'l' },
{ key: -5, from: 'Delta', to: 'Alpha', fromPort: 't', toPort: 'r' }
];
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);
};
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<go.ObjectData> = [
{ key: 'PaletteNode1', color: 'firebrick' },
{ key: 'PaletteNode2', color: 'blueviolet' }
];
public paletteLinkData: Array<go.ObjectData> = [
{ from: 'PaletteNode1', to: 'PaletteNode2' }
];
public paletteModelData = { prop: 'val' };
public paletteDivClassName = 'myPaletteDiv';
public skipsPaletteUpdate = false;
public paletteModelChange = function (changes: go.IncrementalData) {
// when setting state here, be sure to set skipsPaletteUpdate: 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 Palette's undoManager history
this.skipsPaletteUpdate = true;
this.paletteNodeData = DataSyncService.syncNodeData(changes, this.paletteNodeData);
this.paletteLinkData = DataSyncService.syncLinkData(changes, this.paletteLinkData);
this.paletteModelData = DataSyncService.syncModelData(changes, this.paletteModelData);
};
constructor(private cdr: ChangeDetectorRef) { }
// Overview Component testing
public oDivClassName = 'myOverviewDiv';
public initOverview(): go.Overview {
const $ = go.GraphObject.make;
const overview = $(go.Overview);
return overview;
}
public observedDiagram = null;
// currently selected node; for inspector
public selectedNode: go.Node | null = null;
public ngAfterViewInit() {
if (this.observedDiagram) return;
this.observedDiagram = this.myDiagramComponent.diagram;
this.cdr.detectChanges(); // IMPORTANT: without this, Angular will throw ExpressionChangedAfterItHasBeenCheckedError (dev mode only)
const appComp: AppComponent = this;
// listener for inspector
this.myDiagramComponent.diagram.addDiagramListener('ChangedSelection', e => {
if (e.diagram.selection.count === 0) {
appComp.selectedNode = null;
}
const node = e.diagram.selection.first();
if (node instanceof go.Node) {
appComp.selectedNode = node;
} else {
appComp.selectedNode = null;
}
});
} // end ngAfterViewInit
public handleInspectorChange(newNodeData) {
const key = newNodeData.key;
// find the entry in nodeDataArray with this key, replace it with newNodeData
let index = null;
for (let i = 0; i < this.diagramNodeData.length; i++) {
const entry = this.diagramNodeData[i];
if (entry.key && entry.key === key) {
index = i;
}
}
if (index >= 0) {
// here, we set skipsDiagramUpdate to false, since GoJS does not yet have this update
this.skipsDiagramUpdate = false;
this.diagramNodeData[index] = _.cloneDeep(newNodeData);
}
}
}