61 |
62 | This gojs-angular sample demonstrates how to use the Diagram, Palette, and Overview
63 | gojs-angular components, made and
64 | maintained by Northwoods Software.
65 |
66 |
67 | Notice how when a Node or Link is removed or added to the Diagram, the list of Nodes and Links
68 | the top-level App Component updates accordingly.
69 |
70 |
71 | For more information on how to use Angular with GoJS, see the
72 | GoJS with Angular intro page.
73 |
74 |
75 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "first-app": {
7 | "projectType": "application",
8 | "schematics": {
9 | "@schematics/angular:component": {
10 | "inlineTemplate": true,
11 | "inlineStyle": true,
12 | "style": "scss",
13 | "skipTests": true
14 | },
15 | "@schematics/angular:class": {
16 | "skipTests": true
17 | },
18 | "@schematics/angular:directive": {
19 | "skipTests": true
20 | },
21 | "@schematics/angular:guard": {
22 | "skipTests": true
23 | },
24 | "@schematics/angular:interceptor": {
25 | "skipTests": true
26 | },
27 | "@schematics/angular:pipe": {
28 | "skipTests": true
29 | },
30 | "@schematics/angular:resolver": {
31 | "skipTests": true
32 | },
33 | "@schematics/angular:service": {
34 | "skipTests": true
35 | }
36 | },
37 | "root": "",
38 | "sourceRoot": "src",
39 | "prefix": "app",
40 | "architect": {
41 | "build": {
42 | "builder": "@angular/build:application",
43 | "options": {
44 | "outputPath": "dist/first-app",
45 | "index": "src/index.html",
46 | "browser": "src/main.ts",
47 | "polyfills": ["zone.js"],
48 | "tsConfig": "tsconfig.app.json",
49 | "inlineStyleLanguage": "scss",
50 | "assets": [
51 | {
52 | "glob": "**/*",
53 | "input": "src/public"
54 | }
55 | ],
56 | "styles": ["src/styles.css"],
57 | "scripts": []
58 | },
59 | "configurations": {
60 | "production": {
61 | "budgets": [
62 | {
63 | "type": "initial",
64 | "maximumWarning": "1.5MB",
65 | "maximumError": "2MB"
66 | },
67 | {
68 | "type": "anyComponentStyle",
69 | "maximumWarning": "100kB",
70 | "maximumError": "200kB"
71 | }
72 | ],
73 | "outputHashing": "all"
74 | },
75 | "development": {
76 | "optimization": false,
77 | "extractLicenses": false,
78 | "sourceMap": true
79 | }
80 | },
81 | "defaultConfiguration": "production"
82 | },
83 | "serve": {
84 | "builder": "@angular/build:dev-server",
85 | "configurations": {
86 | "production": {
87 | "buildTarget": "first-app:build:production"
88 | },
89 | "development": {
90 | "buildTarget": "first-app:build:development"
91 | }
92 | },
93 | "defaultConfiguration": "development"
94 | },
95 | "extract-i18n": {
96 | "builder": "@angular/build:extract-i18n"
97 | }
98 | }
99 | }
100 | },
101 | "schematics": {
102 | "@schematics/angular:component": {
103 | "type": "component"
104 | },
105 | "@schematics/angular:directive": {
106 | "type": "directive"
107 | },
108 | "@schematics/angular:service": {
109 | "type": "service"
110 | },
111 | "@schematics/angular:guard": {
112 | "typeSeparator": "."
113 | },
114 | "@schematics/angular:interceptor": {
115 | "typeSeparator": "."
116 | },
117 | "@schematics/angular:module": {
118 | "typeSeparator": "."
119 | },
120 | "@schematics/angular:pipe": {
121 | "typeSeparator": "."
122 | },
123 | "@schematics/angular:resolver": {
124 | "typeSeparator": "."
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Sample app showcasing gojs-angular components
3 | * For use with gojs-angular version 2.x, assuming immutable data
4 | * This now uses GoJS version 3.0, using some of its new features,
5 | * but your app could use GoJS version 2.3.17, if you don't yet want to upgrade to v3.
6 | */
7 |
8 | import { ChangeDetectorRef, Component, ViewChild, ViewEncapsulation } from '@angular/core';
9 | import * as go from 'gojs';
10 | import {
11 | DataSyncService,
12 | DiagramComponent,
13 | OverviewComponent,
14 | PaletteComponent,
15 | } from 'gojs-angular';
16 | import { produce } from 'immer';
17 | import { InspectorComponent } from './inspector/inspector.component';
18 | import { CommonModule } from '@angular/common';
19 |
20 | @Component({
21 | selector: 'app-root',
22 | templateUrl: './app.component.html',
23 | styleUrls: ['./app.component.css'],
24 | imports: [
25 | DiagramComponent,
26 | PaletteComponent,
27 | InspectorComponent,
28 | OverviewComponent,
29 | CommonModule,
30 | ],
31 | encapsulation: ViewEncapsulation.ShadowDom,
32 | })
33 | export class AppComponent {
34 | @ViewChild('myDiagram', { static: true }) public myDiagramComponent: DiagramComponent;
35 | @ViewChild('myPalette', { static: true }) public myPaletteComponent: PaletteComponent;
36 |
37 | // Big object that holds app-level state data
38 | // As of gojs-angular 2.0, immutability is expected and required of state for ease of change detection.
39 | // Whenever updating state, immutability must be preserved. It is recommended to use immer for this, a small package that makes working with immutable data easy.
40 | public state = {
41 | // Diagram state props
42 | diagramNodeData: [
43 | { key: 'Alpha', text: 'Alpha', color: 'lightblue', loc: '0 0' },
44 | { key: 'Beta', text: 'Beta', color: 'orange', loc: '150 0' },
45 | { key: 'Gamma', text: 'Gamma', color: 'lightgreen', loc: '0 100' },
46 | { key: 'Delta', text: 'Delta', color: 'pink', loc: '100 100' },
47 | ],
48 | diagramLinkData: [
49 | { key: -1, from: 'Alpha', to: 'Beta', fromPort: 'r', toPort: 'l' },
50 | { key: -2, from: 'Alpha', to: 'Gamma', fromPort: 'b', toPort: 't' },
51 | { key: -3, from: 'Beta', to: 'Beta' },
52 | { key: -4, from: 'Gamma', to: 'Delta', fromPort: 'r', toPort: 'l' },
53 | { key: -5, from: 'Delta', to: 'Alpha', fromPort: 't', toPort: 'r' },
54 | ],
55 | diagramModelData: { prop: 'value' },
56 | skipsDiagramUpdate: false,
57 | selectedNodeData: null, // used by InspectorComponent
58 |
59 | // Palette state props
60 | paletteNodeData: [
61 | { key: 'Epsilon', text: 'Epsilon', color: 'moccasin' },
62 | { key: 'Kappa', text: 'Kappa', color: 'lavender' },
63 | ],
64 | paletteModelData: { prop: 'val' },
65 | };
66 |
67 | public diagramDivClassName = 'myDiagramDiv';
68 | public paletteDivClassName = 'myPaletteDiv';
69 |
70 | // initialize diagram / templates
71 | public initDiagram(): go.Diagram {
72 | const diagram = new go.Diagram({
73 | 'commandHandler.archetypeGroupData': { key: 'Group', isGroup: true },
74 | 'clickCreatingTool.archetypeNodeData': { text: 'new node', color: 'lightblue' },
75 | 'undoManager.isEnabled': true,
76 | model: new go.GraphLinksModel({
77 | linkToPortIdProperty: 'toPort', // want to support multiple ports per node
78 | linkFromPortIdProperty: 'fromPort',
79 | linkKeyProperty: 'key', // IMPORTANT! must be defined for merges and data sync when using GraphLinksModel
80 | }),
81 | });
82 |
83 | // a helper function for defining multiple ports in node templates
84 | function makePort(id: string, spot: go.Spot) {
85 | return new go.Shape('Circle', {
86 | desiredSize: new go.Size(8, 8),
87 | opacity: 0.5,
88 | fill: 'gray',
89 | strokeWidth: 0,
90 | portId: id,
91 | alignment: spot,
92 | fromSpot: spot,
93 | toSpot: spot,
94 | fromLinkable: true,
95 | toLinkable: true,
96 | cursor: 'pointer',
97 | });
98 | }
99 |
100 | // define the Node template
101 | diagram.nodeTemplate = new go.Node('Spot', {
102 | contextMenu: (go.GraphObject.build('ContextMenu') as go.Adornment).add(
103 | (go.GraphObject.build('ContextMenuButton') as go.Panel).add(
104 | new go.TextBlock('Group', {
105 | click: (e, obj) => e.diagram.commandHandler.groupSelection(),
106 | })
107 | )
108 | ),
109 | })
110 | .bindTwoWay('location', 'loc', go.Point.parse, go.Point.stringifyFixed(1))
111 | .add(
112 | new go.Panel('Auto').add(
113 | new go.Shape('RoundedRectangle', { strokeWidth: 0.5 }).bind('fill', 'color'),
114 | new go.TextBlock({ margin: 8, editable: true }).bindTwoWay('text')
115 | ),
116 | // Ports
117 | makePort('t', go.Spot.Top),
118 | makePort('l', go.Spot.Left),
119 | makePort('r', go.Spot.Right),
120 | makePort('b', go.Spot.Bottom)
121 | );
122 |
123 | diagram.linkTemplate = new go.Link({
124 | curve: go.Curve.Bezier,
125 | fromEndSegmentLength: 30,
126 | toEndSegmentLength: 30,
127 | }).add(new go.Shape({ strokeWidth: 1.5 }), new go.Shape({ toArrow: 'Standard' }));
128 |
129 | return diagram;
130 | }
131 |
132 | // When the diagram model changes, update app data to reflect those changes. Be sure to use immer's "produce" function to preserve immutability
133 | public diagramModelChange = function (changes: go.IncrementalData) {
134 | if (!changes) return;
135 | const appComp = this;
136 | this.state = produce(this.state, (draft) => {
137 | // set skipsDiagramUpdate: true since GoJS already has this update
138 | // this way, we don't log an unneeded transaction in the Diagram's undoManager history
139 | draft.skipsDiagramUpdate = true;
140 | draft.diagramNodeData = DataSyncService.syncNodeData(
141 | changes,
142 | draft.diagramNodeData,
143 | appComp.observedDiagram.model
144 | );
145 | draft.diagramLinkData = DataSyncService.syncLinkData(
146 | changes,
147 | draft.diagramLinkData,
148 | appComp.observedDiagram.model
149 | );
150 | draft.diagramModelData = DataSyncService.syncModelData(changes, draft.diagramModelData);
151 | // If one of the modified nodes was the selected node used by the inspector, update the inspector selectedNodeData object
152 | const modifiedNodeData = changes.modifiedNodeData;
153 | if (modifiedNodeData && draft.selectedNodeData) {
154 | for (let i = 0; i < modifiedNodeData.length; i++) {
155 | const mn = modifiedNodeData[i];
156 | const nodeKeyProperty = appComp.myDiagramComponent.diagram.model
157 | .nodeKeyProperty as string;
158 | if (mn[nodeKeyProperty] === draft.selectedNodeData[nodeKeyProperty]) {
159 | draft.selectedNodeData = mn;
160 | }
161 | }
162 | }
163 | });
164 | };
165 |
166 | public initPalette(): go.Palette {
167 | const palette = new go.Palette();
168 | // define a simpler Node template than the one used by the main Diagram
169 | palette.nodeTemplate = new go.Node('Auto').add(
170 | new go.Shape('RoundedRectangle', { strokeWidth: 0.5 }).bind('fill', 'color'),
171 | new go.TextBlock({ margin: 8 }).bind('text')
172 | );
173 | return palette;
174 | }
175 |
176 | constructor(private cdr: ChangeDetectorRef) {}
177 |
178 | // Overview Component testing
179 | public oDivClassName = 'myOverviewDiv';
180 | public initOverview(): go.Overview {
181 | return new go.Overview();
182 | }
183 | public observedDiagram = null;
184 |
185 | // currently selected node; for inspector
186 | public selectedNodeData: go.ObjectData = null;
187 |
188 | public ngAfterViewInit() {
189 | if (this.observedDiagram) return;
190 | this.observedDiagram = this.myDiagramComponent.diagram;
191 | this.cdr.detectChanges(); // IMPORTANT: without this, Angular will throw ExpressionChangedAfterItHasBeenCheckedError (dev mode only)
192 |
193 | const appComp: AppComponent = this;
194 | // listener for inspector
195 | this.myDiagramComponent.diagram.addDiagramListener('ChangedSelection', function (e) {
196 | if (e.diagram.selection.count === 0) {
197 | appComp.selectedNodeData = null;
198 | }
199 | const node = e.diagram.selection.first();
200 | appComp.state = produce(appComp.state, (draft) => {
201 | if (node instanceof go.Node) {
202 | var idx = draft.diagramNodeData.findIndex((nd) => nd.key == node.data.key);
203 | var nd = draft.diagramNodeData[idx];
204 | draft.selectedNodeData = nd;
205 | } else {
206 | draft.selectedNodeData = null;
207 | }
208 | });
209 | });
210 | } // end ngAfterViewInit
211 |
212 | /**
213 | * Update a node's data based on some change to an inspector row's input
214 | * @param changedPropAndVal An object with 2 entries: "prop" (the node data prop changed), and "newVal" (the value the user entered in the inspector