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.

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.setDataProperty and by Model property setters.

For property changes, that information includes the ChangedEvent.object that was modified, the ChangedEvent.propertyName, and the ChangedEvent.oldValue and ChangedEvent.newValue values for that property. Property changes are identified by the ChangedEvent.change property value being ChangeType.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:

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 property name by having set GraphLinksModel.linkFromKeyProperty to some other data property name.

Any property can be changed on a node data or link data object, by calling Model.setDataProperty. 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.

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:

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 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:

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:


  // notice whenever a transaction or undo/redo has occurred
  diagram.addModelChangedListener(evt => {
    if (evt.isTransactionFinished) saveModel(evt.model);
  });

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 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.


  diagram.addModelChangedListener(evt => {
    // ignore unimportant Transaction events
    if (!evt.isTransactionFinished) return;
    var txn = evt.object;  // a Transaction
    if (txn === null) return;
    // iterate over all of the actual ChangedEvents of the Transaction
    txn.changes.each(e => {
      // ignore any kind of change other than adding/removing a node
      if (e.modelChange !== "nodeDataArray") return;
      // record node insertions and removals
      if (e.change === go.ChangeType.Insert) {
        console.log(evt.propertyName + " added node with key: " + e.newValue.key);
      } else if (e.change === go.ChangeType.Remove) {
        console.log(evt.propertyName + " removed node with key: " + e.oldValue.key);
      }
    });
  });

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.


  diagram.addModelChangedListener(evt => {
    // ignore unimportant Transaction events
    if (!evt.isTransactionFinished) return;
    var txn = evt.object;  // a Transaction
    if (txn === null) return;
    // iterate over all of the actual ChangedEvents of the Transaction
    txn.changes.each(e => {
      // record node insertions and removals
      if (e.change === go.ChangeType.Property) {
        if (e.modelChange === "linkFromKey") {
          console.log(evt.propertyName + " changed From key of link: " +
                      e.object + " from: " + e.oldValue + " to: " + e.newValue);
        } else if (e.modelChange === "linkToKey") {
          console.log(evt.propertyName + " changed To key of link: " +
                      e.object + " from: " + e.oldValue + " to: " + e.newValue);
        }
      } else if (e.change === go.ChangeType.Insert && e.modelChange === "linkDataArray") {
        console.log(evt.propertyName + " added link: " + e.newValue);
      } else if (e.change === go.ChangeType.Remove && e.modelChange === "linkDataArray") {
        console.log(evt.propertyName + " removed link: " + e.oldValue);
      }
    });
  });

Note: the above code only works for a GraphLinksModel, where the link data are separate JavaScript objects.

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.


  diagram.addModelChangedListener(e => {
    // ignore unimportant Transaction events
    if (!evt.isTransactionFinished) return;
    var json = e.model.toIncrementalJson(e);
    var data = e.model.toIncrementalData(e);
    ... send to server/database ...
  });

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 is placed on objects.

Diagram and GraphObject changes

Diagram ChangedEvents record state changes to GraphObjects or RowColumnDefinitions in a diagram, or to a Layer 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.