Hide Table of Contents
View Editor with undo redo sample in sandbox
Editor with undo redo

Description

This sample shows how to use the UndoManager, a utility object, that allows you to incoporate undo/redo functionality into your application. When you create the UndoManager you can specify the number of operations that will be maintained on the undo/redo stack.

undoManager = new UndoManager({maxOperations: 8});

There are several out-of-the-box operations: Add, Delete, Update, Cut and Union. You can also create custom operations by inheriting from the OperationBase class. In this sample we use the Add, Delete and Update operations to maintain a stack of feature edits. This code snippet shows how to add a new operation to the stack. When existing features are updated using the applyEdits method add the update to the stack.

layer.applyEdits(null, [feature], null, function() {
var operation = new Update({

featureLayer: layer,

preUpdatedGraphics: [new Graphic(originalFeature)],

postUpdatedGraphics: [feature]

});
undoManager
.add(operation);
checkUI
();
});
});

If the application end-user wants to undo the update they click the undo button which calls the UndoManager's undo method.
registry.byId("undo").on("click", function(e) {
undoManager.undo();
});
<button id="undo" data-dojo-type="dijit/form/Button" data-dojo-props="disabled:true, iconClass:'undoIcon'" >Undo</button>

Code

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    
    <meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no">
    <title>UndoManager</title>

    <link rel="stylesheet" href="https://js.arcgis.com/3.38/dijit/themes/claro/claro.css">
    <link rel="stylesheet" href="https://js.arcgis.com/3.38/esri/css/esri.css">
    <style>
      html, body {
        height: 100%;
        width: 100%;
        margin: 0;
        padding: 0;
        overflow:hidden;
      }
      .instructions{
        padding-top:20px;
        font-size:12px;
      }
      .undoButtons{
        width:60%;
        margin-left:auto;
        margin-right:auto;
        padding-top:4px;
      }
      #map{
        padding:0px;
        border:solid 2px #1A84AD;
        -moz-border-radius: 4px;
        border-radius: 4px;
      }
      #rightPane{
        border:none;
        width:300px;
      }
      .templatePicker {
        border:solid 2px #1A84AD !important;
      }
      .undoIcon { background-image:url(images/undo.png); width:16px; height:16px; }
      .redoIcon { background-image:url(images/redo.png); width:16px; height:16px;}
    </style>

    <script src="https://js.arcgis.com/3.38/"></script>
    <script>
      var map, undoManager, attInspector;
      require([
        "esri/map",
        "esri/layers/FeatureLayer",
        "esri/undoManager",
        "esri/dijit/AttributeInspector",
        "esri/dijit/editing/TemplatePicker",

        "esri/dijit/editing/Add",
        "esri/dijit/editing/Delete",
        "esri/dijit/editing/Update",
        "esri/dijit/editing/Editor",
        "esri/tasks/query",
        "esri/toolbars/draw",
        "esri/graphic",

        "dojo/parser",
        "dojo/_base/event",
        "dijit/registry",
        "dojo/_base/array",

        "dijit/form/Button",
        "dijit/layout/BorderContainer",
        "dijit/layout/ContentPane",
        "dojo/domReady!"
      ], function(
        Map, FeatureLayer, UndoManager, AttributeInspector, TemplatePicker,
        Add, Delete, Update, Editor, Query, Draw, Graphic,
        parser, event, registry, array
      ) {
        parser.parse();


        // specify the number of undo operations allowed using the maxOperations parameter
        undoManager = new UndoManager({maxOperations: 8});

        // listen for the undo/redo button click events
        registry.byId("undo").on("click", function(e) {
          undoManager.undo();
        });
        registry.byId("redo").on("click", function(e) {
          undoManager.redo();
        });

        map = new Map("map", {
          basemap: "topo-vector",
          center: [-97.367, 37.691],
          zoom: 14
        });

        var landuseLayer = new FeatureLayer("https://sampleserver6.arcgisonline.com/arcgis/rest/services/Military/FeatureServer/6", {
          mode: FeatureLayer.MODE_SNAPSHOT,
          outFields: ["*"]
        });

        map.addLayers([landuseLayer]);
        map.on("layers-add-result", initEditing);

        function initEditing(results) {
          var layer = results.layers[0].layer;
          var layers = array.map(results.layers, function(result) {
            return result.layer;
          });

          var layerInfos = array.map(results.layers, function(result) {
            return {featureLayer: results.layers[0].layer, isEditable: true, showAttachments: false};
          });

          //Ctrl+click to delete features and add this delete operation to undomanager
          layer.on("click", function(evt) {
            event.stop(evt);

            if (evt.ctrlKey === true || evt.metaKey === true) {  //delete feature if ctrl key is depressed
              layer.applyEdits(null, null, [evt.graphic], function() {
                var operation = new Delete({
                  featureLayer: layer,
                  deletedGraphics: [evt.graphic]
                });
                undoManager.add(operation);
                checkUI();
              });
            }
          });

          layer.on("before-apply-edits", function() {
            dijit.byId("undo").set("disabled", true);
            dijit.byId("redo").set("disabled", true);
          });

          layer.on("edits-complete", function(evt) {
            //display attribute inspector for newly created features
            if (evt.adds.length > 0) {
              var query = new Query();
              query.objectIds = [evt.adds[0].objectId];
              layer.selectFeatures(query, FeatureLayer.SELECTION_NEW, function(features
                ) {
                if (features.length > 0) {
                  var screenPoint = map.toScreen(features[0].geometry);
                  //display the attribute window for newly created features
                  map.infoWindow.setTitle("");
                  map.infoWindow.show(screenPoint, map.getInfoWindowAnchor(screenPoint));
                }
                else {
                  map.infoWindow.hide();
                }
              });
            }
            if (evt.deletes.length > 0) {
              //hide the info window if features are deleted.
              map.infoWindow.hide();
            }
            checkUI();
          });

          //Add the attribute inspector and listen for events to update feature layer
          //when attributes are modified.
          attInspector = new AttributeInspector({layerInfos: layerInfos}, "attributesDiv");

          //display the attribute inspector in the info window.
          map.infoWindow.setContent(attInspector.domNode);
          map.infoWindow.resize(300, 190);

          //delete the feature and close the info window if displayed.
          attInspector.on("delete",function(evt){
            var feature = evt.feature;
            var layer = feature.getLayer();
            layer.applyEdits(null, null, [feature], function() {
              var operation = new Delete({
                featureLayer: layer,
                deletedGraphics: [feature]
              });
              undoManager.add(operation);
              checkUI();
            });
            layer.clearSelection();
            map.infoWindow.hide();
          });

          //show the info window for the next selected feature
          attInspector.on("next", function(evt) {
            var feature = evt.feature;
            var screenPoint = map.toScreen(feature.geometry.getExtent().getCenter());
            map.infoWindow.show(screenPoint, map.getInfoWindowAnchor(screenPoint));
          });

          //Update the feature service attributes and add each attribute change to
          //the undo manager for undo/redo capability
          attInspector.on("attribute-change", function(evt) {
            var feature = evt.feature;
            feature.attributes[evt.fieldName] = evt.newFieldValue;

            var layer = feature.getLayer();
            layer.applyEdits(null, [feature], null, function() {
              var operation = new Update({
                featureLayer: layer,
                preUpdatedGraphics: [new Graphic(originalFeature)],
                postUpdatedGraphics: [feature]
              });

              undoManager.add(operation);
              checkUI();
            });
          });

          var templatePicker = new TemplatePicker({
            featureLayers: layers,
            rows: "auto",
            columns: 3,
            grouping: true
          }, "templatePickerDiv");

          templatePicker.startup();

          var drawToolbar = new Draw(map);

          var selectedTemplate;

          //when users select an item from the template picker activate the draw toolbar
          //with the geometry type of the selected template item.
          templatePicker.on("selection-change", function() {
            if (templatePicker.getSelected()) {
              selectedTemplate = templatePicker.getSelected();
            }
            drawToolbar.activate(Draw.POINT);
          });

          //once the geometry is drawn - call applyEdits to update the feature service with the new geometry
          drawToolbar.on("draw-complete", function(evt) {
            drawToolbar.deactivate();
            var newAttributes = dojo.mixin({}, selectedTemplate.template.prototype.attributes);
            var newGraphic = new Graphic(evt.geometry, null, newAttributes);
            //when features are added - add them to the undo manager
            selectedTemplate.featureLayer.applyEdits([newGraphic], null, null, function() {
              var operation = new Add({
                featureLayer: selectedTemplate.featureLayer,
                addedGraphics: [newGraphic]
              });
              undoManager.add(operation);
              checkUI();
            });
          });
        }

        //disable or enable undo/redo buttons depending on current app state
        function checkUI() {
          if (undoManager.canUndo) {
            dijit.byId("undo").set("disabled", false);
          }
          else {
            dijit.byId("undo").set("disabled", true);
          }

          if (undoManager.canRedo) {
            dijit.byId("redo").set("disabled", false);
          }
          else {
            dijit.byId("redo").set("disabled", true);
          }
        }

      });
    </script>
  </head>
  <body class="claro">
    <div data-dojo-type="dijit/layout/BorderContainer" data-dojo-props="gutters:true, design:'sidebar'" style="width:100%;height:100%;">
      <div id="map" data-dojo-type="dijit/layout/ContentPane" data-dojo-props="region:'center'"></div>
      <div id="rightPane" data-dojo-type="dijit/layout/ContentPane" data-dojo-props="region:'right'">
        <div id="templatePickerDiv"></div>
        <div class="undoButtons">
         <button id="undo"  data-dojo-type="dijit/form/Button" data-dojo-props="disabled:true, iconClass:'undoIcon'" >Undo</button>
         <button id="redo"  data-dojo-type="dijit/form/Button" data-dojo-props="disabled:true, iconClass:'redoIcon'" >Redo</button>
        </div>
        <div class='instructions'>
          <ul style="list-style:none;padding-left:4px;">
            <li><b>Create Features:</b> Select template then click on map.</li>
            <li><b>Delete Features:</b> Ctrl or Cmd + Click feature.</li>
          </ul>
          The undo/redo buttons will become enabled after editing the feature attributes or geometry.
        </div>
      </div>
    </div>
  </body>
</html>
 
          
Show Modal