Changed Events
There are three basic kinds of events that GoJS generates: DiagramEvents, InputEvents, and ChangedEvents. This page talks about the latter, which are generated as Diagrams, GraphObjects, Models, or Model data objects are modified. See the page Events for the former two kinds of events.
ChangedEvents in GoJS are notifications of state changes, mostly object property changes. The ChangedEvent records the kind of change that occurred and enough information to be able to undo and redo them.
Changed events are produced by both Model and Diagram. They are multicast events, so you can call Model.addChangedListener and Diagram.addChangedListener, as well as the corresponding removeChangedListener methods. For convenience you can also specify a Model change listener on a Diagram: Diagram.addModelChangedListener. ChangedEvents received by a Model change listener will have a non-null value for ChangedEvent.model. Similarly, ChangedEvents received by a Diagram change listener will have non-null value for ChangedEvent.diagram.
In the following example, a Model change listener is added to the diagram and used to display the changed property on a simple draggable node that has a two-way binding on its location. Be careful with triggering changes to the model within a model change listener, as this can cause the listener to reenter itself and loop infinitely.
A Diagram always registers itself as a listener on its Model, so that it can automatically notice changes to the model and update its Parts accordingly. Furthermore the UndoManager, if enabled, automatically listens to changes to both the model and the diagram, so that it can record the change history and perform undo and redo.
Model and data changes
Model property changes
Model ChangedEvents record state changes either to data in a model or to the Model itself. ChangedEvents for models are generated by calls to Model.set and by Model property setters. Property changes are identified by the ChangedEvent.change property value being ChangeType.Property.
For property changes, the information includes the ChangedEvent.object that was modified, the ChangedEvent.propertyName, and the ChangedEvent.oldValue and ChangedEvent.newValue values for that property.
Some changes represent structural changes to the model, not just simple model data changes. "Structural" changes are the insertion, modification, or removal of relationships that the model is responsible for maintaining. In such cases the ChangedEvent.modelChange property will be a non-empty string naming the kind of change. The following names for Property ChangedEvents correspond to structural model data changes:
| Name | Cause |
|---|---|
| "nodeDataArray" | when the Model.nodeDataArray Array has been replaced |
| "nodeCategory" | due to a call to Model.setCategoryForNodeData |
| "nodeGroupKey" | due to a call to GraphLinksModel.setGroupKeyForNodeData |
| "linkDataArray" | when the GraphLinksModel.linkDataArray Array has been replaced |
| "linkFromKey" | due to a call to GraphLinksModel.setFromKeyForLinkData |
| "linkToKey" | due to a call to GraphLinksModel.setToKeyForLinkData |
| "linkFromPortId" | due to a call to GraphLinksModel.setFromPortIdForLinkData |
| "linkToPortId" | due to a call to GraphLinksModel.setToPortIdForLinkData |
| "linkLabelKeys" | due to a call to GraphLinksModel.setLabelKeysForLinkData |
| "linkCategory" | due to a call to GraphLinksModel.setCategoryForLinkData |
| "nodeParentKey" | due to a call to TreeModel.setParentKeyForNodeData |
| "parentLinkCategory" | due to a call to TreeModel.setParentLinkCategoryForNodeData |
The value of ChangedEvent.modelChange will be one of these strings. The value of ChangedEvent.propertyName depends on the name of the actual data property that was modified. For example, for the model property change "linkFromKey", the actual property name defaults to "from". But you might be using a different data property name by having set GraphLinksModel.linkFromKeyProperty to some other name.
The following sample demonstrates the above structural Property ChangedEvents for a
GraphLinksModel. It steps through a sequence of calls to the various set...ForNodeData
and set...ForLinkData methods, plus a trivial replacement of Model.nodeDataArray and
GraphLinksModel.linkDataArray. Each event is captured by a model change listener that displays
the ChangedEvent.modelChange name and the actual ChangedEvent.propertyName in a
panel below.
The two TreeModel-only changes, "nodeParentKey" and "parentLinkCategory" behave identically but require a TreeModel rather than the GraphLinksModel.
Any property can be changed on a node data or link data object, by calling Model.set. Such a call will result in the property name to be recorded as the ChangedEvent.propertyName. These cases are treated as normal property changes, not structural model changes, so ChangedEvent.modelChange will be the empty string. The value of ChangedEvent.object will of course be the JavaScript object that was modified.
Some changes may happen temporarily because some code, such as in a Tool, might want to use temporary objects for their own purposes. However your change listener might not be interested in such ChangedEvents. If that is the case, you may want to ignore the ChangedEvent if Model.skipsUndoManager (or Diagram.skipsUndoManager) is true, as this generally signifies temporary events.
Finally, there are property changes on the model itself. For a listing of such properties, see the documentation for Model, GraphLinksModel, and TreeModel. These cases are also treated as normal property changes, so ChangedEvent.modelChange will be the empty string. Both ChangedEvent.model and ChangedEvent.object will be the model itself.
Model collection changes
Other kinds of changed events include ChangeType.Insert and ChangeType.Remove. In addition to all of the previously mentioned ChangedEvent properties used to record a property change, the ChangedEvent.oldParam and ChangedEvent.newParam provide the "index" information needed to be able to properly undo and redo the change.
The following names for Insert and Remove ChangedEvents correspond to model changes to collections, as the value of ChangedEvent.modelChange:
- "nodeDataArray", due to a call to Model.addNodeData or Model.removeNodeData
- "linkDataArray", due to a call to GraphLinksModel.addLinkData or GraphLinksModel.removeLinkData
- "linkLabelKeys", due to a call to GraphLinksModel.addLabelKeyForLinkData or GraphLinksModel.removeLabelKeyForLinkData
Transactions
The final kind of model changed event is ChangeType.Transaction. These are not strictly object changes in the normal sense, but they do notify about state changes in the model or the UndoManager, when a transaction starts or finishes or when an undo or redo starts or finishes.
The following values of ChangedEvent.propertyName describe the kind of transaction-related event that just occurred:
- "StartingFirstTransaction"
- "StartedTransaction"
- "CommittingTransaction"
- "CommittedTransaction"
- "RolledBackTransaction"
- "StartingUndo"
- "FinishedUndo"
- "StartingRedo"
- "FinishedRedo"
In each case the ChangedEvent.object is the Transaction holding a sequence of ChangedEvents. The ChangedEvent.oldValue is the name of the transaction -- the string passed to UndoManager.startTransaction or UndoManager.commitTransaction. The various standard commands and tools that perform transactions document the transaction name(s) that they employ. But your code can employ as many transaction names as you like.
As a general rule, you should not make any changes to the model or any of its data in a listener for any Transaction ChangedEvent.
Saving the Model when Transactions complete
It is commonplace to want to update a server database when a transaction has finished. Use the ChangedEvent.isTransactionFinished read-only property to detect that case. You'll want to implement a Changed listener as follows:
The value of Transaction.changes will be a List of ChangedEvents, in the order that they were recorded.
Those ChangedEvents represent changes both to the Model or its data and to the Diagram or its GraphObjects.
Model changes will have e.model !== null; diagram changes will have e.diagram !== null.
Incrementally saving changes to the Model
If you do not want to save the whole model at the end of each transaction, but only certain changes to the model, you can iterate over the list of changes to pick out the ones that you care about. For example, here is a listener that logs a message only when node data is added to or removed from the Model.nodeDataArray.
The above listener will put out messages as the user adds nodes (including by copying) and deletes nodes. The ChangedEvent.propertyName of the Transaction event (i.e. evt in the code above) will be either "CommittedTransaction", "FinishedUndo", or "FinishedRedo". Note that a "FinishedUndo" of the removal of a node is really adding the node, just as the undo of the insertion of a node actually removes it.
Similarly, here is an example of noticing when links are connected, reconnected, or disconnected. This not only checks for insertions to and removals from GraphLinksModel.linkDataArray, but also changes to the "from" and the "to" properties of the link data.
Note: the above code only works for a GraphLinksModel, where the link data are separate JavaScript objects.
For an example of these principles in action, the following sample lets the user add and delete
employees to an org chart and tracks who is being hired and who is leaving via listening for nodeDataArray
changes:
Look at the Update Demo for a demonstration of how you can keep track of changes to a model when a transaction is committed or when an undo or redo is finished. The common pattern is to iterate over the ChangedEvents of the current Transaction in order to decide what to record in a database.
It is also possible to send incremental updates to a database using Model.toIncrementalJson or Model.toIncrementalData, which iterate over the changes in a transaction and group them into a JSON-formatted string or an object representing any updates.
Caution: don't call JSON.stringify on the result of Model.toIncrementalData,
because that will not properly handle any instances of JavaScript classes that are referenced by the object's properties.
Instead call Model.toIncrementalJson, which will produce a more compact textual serialization.
If you have to do your own serialization, please ignore the internal hash id property that GoJS may place on objects.
Diagram and GraphObject changes
Diagram ChangedEvents record state changes to GraphObjects, RowColumnDefinitions, or Layers in a diagram, or to the Diagram itself. For such events, ChangedEvent.diagram will be non-null.
Most ChangedEvents for diagrams record property changes, such as when some code sets the TextBlock.text property or the Part.location property. There are a few places which generate ChangedEvents recording insertions into or removals from collections, such as Panel.insertAt. There are never any ChangedEvents for diagrams that are ChangeType.Transaction.
Although ChangedEvents for diagrams are important for undo/redo in order to retain visual fidelity, one normally ignores them when saving models. Only ChangedEvents for models record state changes to model data. So for saving to a database, you will want to consider only those ChangedEvents for which ChangedEvent.model is non-null.