Editing Multiple Feature Layers in Esri JS API 4

08-14-2018 09:52 AM
Occasional Contributor III


I'm trying to figure out how to edit multiple feature layers within the Esri JS API 4 ecosystem. I have the editing of a single feature layer nailed down:

<!DOCTYPE html>

  <meta charset="utf-8">
  <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
  <title>Street Closures and Detours Editor</title>

  <link rel="stylesheet" href="https://js.arcgis.com/4.8/esri/css/main.css">
  <script src="https://js.arcgis.com/4.8/"></script>

    #viewDiv {
      padding: 0;
      margin: 0;
      height: 100%;
      width: 100%;

    .editArea-container {
      background: #fff;
      font-family: "Avenir Next W00", "Helvetica Neue", Helvetica, Arial, sans-serif;
      line-height: 1.5em;
      overflow: auto;
      padding: 12px 15px;
      width: 300px;

    .edit-button:focus {
      background-color: #e4e4e4;

    .inputInfo {
      font-size: 12px;
      height: 32px;
      margin-bottom: 6px;
      padding: 0 6px;
      width: 100%;

    .list-heading {
      font-weight: normal;
      margin-top: 20px;
      margin-bottom: 10px;
      color: #323232;

    .edit-button {
      font-size: 14px;
      height: 32px;
      margin-top: 10px;
      width: 100%;
      background-color: transparent;
      border: 1px solid #0079c1;
      color: #0079c1;

    .or-wrap {
      background-color: #e0e0e0;
      height: 1px;
      margin: 2em 0;
      overflow: visible;

    .or-text {
      background: #fff;
      line-height: 0;
      padding: 0 1em;
      position: relative;
      top: -.75em;

    input:invalid {
      border: 1px solid red;

    input:valid {
      border: 1px solid green;

        Map, MapView, Layer, FeatureLayer, Graphic, Expand,
        Home, Search, Extent, Viewpoint, watchUtils, Draw, Polyline, geometryEngine,
        on, dom
      ) {

        var closureFL, editExpand;

        var editArea, attributeEditing, inputDescription,
          inputUserInfo, updateInstructionDiv;

        var map = new Map({
          basemap: "dark-gray"

        var initialExtent = new Extent({
          xmin: -9024777,
          xmax: -8968329,
          ymin: 4179026,
          ymax: 4210862,
          spatialReference: 102100

        var view = new MapView({
          container: "viewDiv",
          map: map,
          extent: initialExtent

        closureLayer = new FeatureLayer({
          url: "https://someserver.domain/arcgis/rest/services/serviceFolder/serviceName/FeatureServer/0",
          outFields: ["*"]
        //detourLayer = new FeatureLayer({
        //  url: "https://someserver.domain/arcgis/rest/services/serviceFolder/serviceName/FeatureServer/1",
        //  outFields: ["*"]


        function addLayer(closureLayer) {
          closureFL = closureLayer;
//          detourFL = detourLayer;
//          map.add(detourLayer);

        function applyEdits(params) {
          var promise = closureFL.applyEdits(params);

        function editResultsHandler(promise) {
            .then(function(editsResult) {
              var extractObjectId = function(result) {
                return result.objectId;

              if (editsResult.addFeatureResults.length > 0) {
                var adds = editsResult.addFeatureResults.map(
                newIncidentId = adds[0];

            .catch(function(error) {
              console.error("[ applyEdits ] FAILURE: ", error.code, error.name,
              console.log("error = ", error);

        view.on("click", function(event) {
          view.hitTest(event).then(function(response) {
            if (response.results.length > 0 && response.results[0].graphic) {

              var feature = response.results[0].graphic;

              closureOID.value = feature.attributes[
              closureBlockName.value = feature.attributes[
              closureLoc.value = feature.attributes[
              closureBlockType.value = feature.attributes[
              closureComm.value = feature.attributes[
              closureClose.value = feature.attributes[
              closureAltRoute.value = feature.attributes[
              closureStartDate.value = feature.attributes[
              closureStartHour.value = feature.attributes[
              closureEndDate.value = feature.attributes[
              closureEndHour.value = feature.attributes[
              closureContact.value = feature.attributes[
              closureDir.value = feature.attributes[
              closureActive.value = feature.attributes[
              closureLink.value = feature.attributes[
              closureCreationDate.value = feature.attributes[
              closureCreator.value = feature.attributes[
              closureEditDate.value = feature.attributes[
              closureEditor.value = feature.attributes[
              closureCATS.value = feature.attributes[
              closureDateComm.value = feature.attributes[
              closureUpdated.value = feature.attributes[
              closureType.value = feature.attributes[
              closureRespDept.value = feature.attributes[
              closureContactComm.value = feature.attributes[
              closureMapURL.value = feature.attributes[
              closureGlobalID.value = feature.attributes[
              closureAuthCreator.value = feature.attributes[
              closureAuthCreationDate.value = feature.attributes[
              closureAuthEditor.value = feature.attributes[
              closureAuthEditDate.value = feature.attributes[
              closureApproval.value = feature.attributes[
              closureID.value = feature.attributes[
              closureProject.value = feature.attributes[
              closureProjectURL.value = feature.attributes[
              attributeEditing.style.display = "block";
              updateInstructionDiv.style.display = "none";

        function selectFeature(objectId) {
          var selectionSymbol = {
            type: "simple-marker", 
            color: [0, 0, 0, 0],
            style: "square",
            size: "40px",
            outline: {
              color: [0, 255, 255, 1],
              width: "3px"
          var query = closureFL.createQuery();
          query.where = closureFL.objectIdField + " = " + objectId;

          closureFL.queryFeatures(query).then(function(results) {
            if (results.features.length > 0) {
              editFeature = results.features[0];
              editFeature.symbol = selectionSymbol;

        function unselectFeature() {
          attributeEditing.style.display = "none";
          updateInstructionDiv.style.display = "block";

          closureOID.value = null;
          closureBlockName.value = null;
          closureLoc.value = null;
          closureBlockType.value = null;
          closureComm.value = null;
          closureClose.value = null;
          closureAltRoute.value = null;
          closureStartDate.value = null;
          closureStartHour.value = null;
          closureEndDate.value = null;
          closureEndHour.value = null;
          closureContact.value = null;
          closureDir.value = null;
          closureActive.value = null;
          closureLink.value = null;
          closureCreationDate.value = null;
          closureCreator.value = null;
          closureEditDate.value = null;
          closureEditor.value = null;
          closureCATS.value = null;
          closureDateComm.value = null;
          closureUpdated.value = null;
          closureType.value = null;
          closureRespDept.value = null;
          closureContactComm.value = null;
          closureMapURL.value = null;
          closureGlobalID.value = null;
          closureAuthCreator.value = null;
          closureAuthCreationDate.value = null;
          closureAuthEditor.value = null;
          closureAuthEditDate.value = null;
          closureApproval.value = null;
          closureID.value = null;
          closureProject.value = null;
          closureProjectURL.value = null;

        function setupView() {
          var homeButton = new Home({
            view: view,
            viewpoint: new Viewpoint({
              targetGeometry: initialExtent
          view.ui.add(homeButton, "top-left");

          editExpand = new Expand({
            expandIconClass: "esri-icon-edit",
            expandTooltip: "Expand Edit",
            expanded: true,
            view: view,
            content: editArea
          view.ui.add(editExpand, "top-right");

        var searchWidget = new Search({
          view: view
        view.ui.add(searchWidget, {
          position: "top-left",
          index: 0

        function setupEditing() {
          editArea = dom.byId("editArea");
          updateInstructionDiv = dom.byId("updateInstructionDiv");
          attributeEditing = dom.byId("featureUpdateDiv");
          closureOID = dom.byId("closureOID");
          closureBlockName = dom.byId("closureBlockName");
          closureLoc= dom.byId("closureLoc");
          closureBlockType = dom.byId("closureBlockType");
          closureComm = dom.byId("closureComm");
          closureClose = dom.byId("closureClose");
          closureAltRoute = dom.byId("closureAltRoute");
          closureStartDate = dom.byId("closureStartDate");
          closureStartHour = dom.byId("closureStartHour");
          closureEndDate = dom.byId("closureEndDate");
          closureEndHour = dom.byId("closureEndHour");
          closureContact = dom.byId("closureContact");
          closureDir = dom.byId("closureDir");
          closureActive = dom.byId("closureActive");
          closureLink = dom.byId("closureLink");
          closureCreationDate = dom.byId("closureCreationDate");
          closureCreator = dom.byId("closureCreator");
          closureEditDate = dom.byId("closureEditDate");
          closureEditor = dom.byId("closureEditor");
          closureCATS = dom.byId("closureCATS");
          closureDateComm = dom.byId("closureDateComm");
          closureUpdated = dom.byId("closureUpdated");
          closureType = dom.byId("closureType");
          closureRespDept = dom.byId("closureRespDept");
          closureContactComm = dom.byId("closureContactComm");
          closureMapURL = dom.byId("closureMapURL");
          closureGlobalID = dom.byId("closureGlobalID");
          closureAuthCreator = dom.byId("closureAuthCreator");
          closureAuthCreationDate = dom.byId("closureAuthCreationDate");
          closureAuthEditor = dom.byId("closureAuthEditor");
          closureAuthEditDate = dom.byId("closureAuthEditDate");
          closureApproval = dom.byId("closureApproval");
          closureID = dom.byId("closureID");
          closureProject = dom.byId("closureProject");
          closureProjectURL = dom.byId("closureProjectURL");

          on(dom.byId("btnUpdate"), "click", function(event) {
            if (editFeature) {
              editFeature.attributes["OBJECTID"] = closureOID.value;
              editFeature.attributes["BLOCKNM"] = closureBlockName.value;
              editFeature.attributes["LOCDESC"] = closureLoc.value;
              editFeature.attributes["BLOCKTYPE"] = closureBlockType.value;
              editFeature.attributes["COMMENT"] = closureComm.value;
              editFeature.attributes["FULLCLOSE"] = closureClose.value;
              editFeature.attributes["ALTROUTE"] = closureAltRoute.value;
              editFeature.attributes["STARTDATE"] = closureStartDate.value;
              editFeature.attributes["StartHour"] = closureStartHour.value;
              editFeature.attributes["ENDDATE"] = closureEndDate.value;
              editFeature.attributes["EndHour"] = closureEndHour.value;
              editFeature.attributes["CONTACT"] = closureContact.value;
              editFeature.attributes["DIRECTION"] = closureDir.value;
              editFeature.attributes["ACTIVE"] = closureActive.value;
              editFeature.attributes["Hyperlink"] = closureLink.value;
              editFeature.attributes["CreationDate"] = closureCreationDate.value;
              editFeature.attributes["Creator"] = closureCreator.value;
              editFeature.attributes["EditDate"] = closureEditDate.value;
              editFeature.attributes["Editor"] = closureEditor.value;
              editFeature.attributes["CATS"] = closureCATS.value;
              editFeature.attributes["DateCommunicated"] = closureDateComm.value;
              editFeature.attributes["Updated"] = closureUpdated.value;
              editFeature.attributes["ClosureType"] = closureType.value;
              editFeature.attributes["ResponsibleDepartment"] = closureRespDept.value;
              editFeature.attributes["ContactCommunications"] = closureContactComm.value;
              editFeature.attributes["ClosureMapURL"] = closureMapURL.value;
              editFeature.attributes["GlobalID"] = closureGlobalID.value;
              editFeature.attributes["created_user"] = closureAuthCreator.value;
              editFeature.attributes["created_date"] = closureAuthCreationDate.value;
              editFeature.attributes["last_edited_user"] = closureAuthEditor.value;
              editFeature.attributes["last_edited_date"] = closureAuthEditDate.value;
              editFeature.attributes["ApproveNotification"] = closureApproval.value;
              editFeature.attributes["ClosureID"] = closureID.value;
              editFeature.attributes["SpecialProject"] = closureProject.value;
              editFeature.attributes["SpecialProjURL"] = closureProjectURL.value;

              var edits = {
                updateFeatures: [editFeature]


      view.when(function(event) {
        var draw = new Draw({
          view: view

        var newClosureButton = document.getElementById("addClosureButton");
        newClosureButton.onclick = function() {
          enableCreateLine(draw, view);

      function enableCreateLine(draw, view) {
        var action = draw.create("polyline", {
          mode: "click"


        action.on("vertex-add", updateVertices);
        action.on("vertex-remove", updateVertices);
        action.on("cursor-update", createGraphic);
        action.on("draw-complete", completeGraphic);


      function updateVertices(event) {
        var result = createGraphic(event);

      function createGraphic(event) {
        var vertices = event.vertices;
        var graphic = new Graphic({
          geometry: new Polyline({
            paths: vertices,
            spatialReference: view.spatialReference
          symbol: {
            type: "simple-line", 
            color: [4, 90, 141],
            width: 3,
            cap: "round",
            join: "round"

      function completeGraphic(event) {
        var vertices = event.vertices;

        var graphic = new Graphic({
          geometry: new Polyline({
            paths: vertices,
            spatialReference: view.spatialReference
        var edits = {
          addFeatures: [graphic]
          on(dom.byId("btnDelete"), "click", function() {
            var edits = {
              deleteFeatures: [editFeature]

          view.when(function() {
            watchUtils.whenTrue(view, "stationary", function() {
              if (editExpand) {
                if (view.zoom <= 8) {
                  editExpand.domNode.style.display = "none";
                } else {
                  editExpand.domNode.style.display = "block";

        function handleLayerLoadError(error) {
          console.log("Layer failed to load: ", error);

  <div id="editArea" class="editArea-container" style="overflow-y:auto;">
    <div id="addFeatureDiv">
      <h3 class="list-heading">Report Road Closures or Detours</h3>
      <ul style="font-size: 13px; padding-left: 1.5em;">
        <li>Click the Appropriate Button</li>
        <li>Draw your Feature on the Map</li>
      <div style="text-align:center;">
        <input type="button" class="edit-button" value="Report Closure" id="addClosureButton" style="width:45%;margin-right:5px">
        <input type="button" class="edit-button" value="Report Detour" id="addDetourButton" style="width:45%;margin-left:5px;">

    <div id="updateInstructionDiv" style="text-align:center">
      <p class="or-wrap">
        <span class="or-text">Or</span>
      <p>Select an incident to edit or delete.</p>

    <div id="featureUpdateDiv" style="display:none; margin-top: 1em;">
      <h3 class="list-heading">Enter the incident information</h3>

      <div id="attributeArea">
        <label for="closureOID">Object ID:</label>
        <input class="inputInfo" readonly type="text" id="closureOID">
        <label for="closureID">Closure ID:</label>
        <input class="inputInfo" readonly type="text" id="closureID">
        <label for="closureGlobalID">Global ID:</label>
        <input class="inputInfo" readonly type="text" id="closureGlobalID">
        <label for="closureBlockName">Closure Name:</label>
        <input class="inputInfo" required type="text" id="closureBlockName" placeHolder="Please enter the name of the closure">
        <label for="closureLoc">Closure Location:</label>
        <input class="inputInfo" required id="closureLoc" placeHolder="Please describe the closure's location">
        <label for="closureBlockType">Closure Type:</label>
        <select class="inputInfo" required type="text" id="closureBlockType" placeHolder="Please choose the appropriate closure type">
          <option value="Construction">Construction</option>          
          <option value="Event">Event</option>
          <option value="Incident">Incident</option>
          <option value="Other">Other</option>
        <label for="closureComm">Comment:</label>
        <input class="inputInfo" required type="text" id="closureComm" placeHolder="Please provide additional information on the closure">
        <label for="closureClose">Closure Type:</label>
        <select class="inputInfo" required type="text" id="closureClose" placeHolder="Please select the appropriate closure type">
          <option value="Yes">Full Closure</option>
          <option value="No">Partial Closure</option>
        <label for="closureAltRoute">Alternate Route:</label>
        <input class="inputInfo" required type="text" id="closureAltRoute" placeHolder="Please provide the alternate route">
        <label for="closureStartDate">Start Date:</label>
        <input class="inputInfo" required type="date" id="closureStartDate" placeHolder="Please indiciate what day the closure will start">
        <label for="closureStartHour">Start Time:</label>
        <input class="inputInfo" required type="time" id="closureStartHour" placeHolder="Please indicate when the closure will start">
        <label for="closureEndDate">End Date:</label>
        <input class="inputInfo" required type="date" id="closureEndDate" placeHolder="Please indiciate what day the closure will end">
        <label for="closureEndHour">End Time:</label>
        <input class="inputInfo" required type="time" id="closureEndHour" placeHolder="Please indicate when the closure will end">
        <label for="closureContact">Contact:</label>
        <input class="inputInfo" required type="text" id="closureContact" placeHolder="Please provide the name and email of the primary contact for this closure">
        <label for="closureDir">Direction:</label>
        <input class="inputInfo" type="text" id="closureDir" placeHolder="Please specify the direction of the closure (if applicable)">
        <label for="closureActive">Active:</label>
        <select class="inputInfo" required type="text" id="closureActive" placeHolder="Please choose whether this closure is active or not">
          <option value="Yes">Yes</option>
          <option value="No">No</option>
        <label for="closureLink">Hyperlink:</label>
        <input class="inputInfo" required type="url" id="closureLink" placeHolder="Please provide the hyperlink that shows this location">
        <label for="closureCreationDate">Creation Date:</label>
        <input class="inputInfo" required type="date" id="closureCreationDate" placeHolder="Please specify the date this record is being created">
        <label for="closureCreator">Creator:</label>
        <input class="inputInfo" type="text" id="closureCreator" placeHolder="Please provide your name">
        <label for="closureEditDate">Edit Date:</label>
        <input class="inputInfo" type="date" id="closureEditDate" placeHolder="Please specify the date this record was updated">
        <label for="closureEditor">Editor:</label>
        <input class="inputInfo" type="text" id="closureEditor" placeHolder="Please specify your name (if updating)">
        <label for="closureCATS">CATS:</label>
        <select class="inputInfo" type="text" id="closureCATS" placeHolder="Please select the appropriate CATS statement">
          <option value="CATS will be affected.">CATS will be affected.</option>
          <option value="CATS will not be affected.">CATS will not be affected.</option>
        <label for="closureDateComm">Date Communicated:</label>
        <input class="inputInfo" type="date" id="closureDateComm" placeHolder="Please specify the date this closure was communicated">
        <label for="closureUpdated">Updated:</label>
        <input class="inputInfo" type="date" id="closureUpdated" placeHolder="Please specify the date this record was updated">
        <label for="closureType">Closure Type:</label>
        <select class="inputInfo" type="text" id="closureType" placeHolder="Please select the appropriate closure type">
          <option value="Lane">Lane</option>
          <option value="Median">Median</option>
          <option value="Resurfacing">Resurfacing</option>
          <option value="Street">Street</option>
        <label for="closureRespDept">Responsible Department:</label>
        <select class="inputInfo" type="text" id="closureRespDept" placeHolder="Please select the department responsible for this closure">
          <option value="Charlotte Water">Charlotte Water</option>
          <option value="Solid Waste Services">Solid Waste Services</option>
          <option value="Stormwater">Stormwater</option>
          <option value="Transportation">Transportation</option>          
        <label for="closureContactComm">Contact Communications:</label>
        <input class="inputInfo" type="text" id="closureContactComm" placeHolder="Enter description" value="Judy Dellert-O'Keef, CDOT Community Engagement Manager at 704-432-0105 or jdellert-okeef@charlottenc.gov.">
        <label for="closureMapURL">Closure Map URL:</label>
        <input class="inputInfo" required type="text" id="closureMapURL" placeHolder="Please input the URL for this closure">
        <input class="inputInfo" type="hidden" id="closureAuthCreator" placeHolder="Enter description">
        <input class="inputInfo" type="hidden" id="closureAuthCreationDate" placeHolder="Enter description">
        <input class="inputInfo" type="hidden" id="closureAuthEditor" placeHolder="Enter description">
        <input class="inputInfo" type="hidden" id="closureAuthEditDate" placeHolder="Enter description">
        <label for="closureApproval">Closure Approval:</label>
        <select class="inputInfo" type="text" id="closureApproval" placeHolder="Please indicate whether this closure has been approved">
          <option value="Yes">Yes</option>
          <option value="No">No</option>
        <label for="closureProject">Special Project:</label>
        <select class="inputInfo" type="text" id="closureProject" placeHolder="Please select the special project causing this closure (if applicable)">
          <option value="Blue Line Extension">Blue Line Extension</option>
          <option value="Cross Charlotte Trail">Cross Charlotte Trail</option>
          <option value="Gold Line Extension">Gold Line Extension</option>
          <option value="Gold Line Extension (phase two)">Gold Line Extension (phase two)</option>
        <label for="closureProjectURL">Special Project URL:</label>
        <input class="inputInfo" type="url"  id="closureProjectURL" placeHolder="Please enter the project URL (if applicable)">
        <input type="button" class="edit-button" value="Update incident info" id="btnUpdate">

      <div id="deleteArea">
        <input type="button" class="edit-button" value="Delete incident" id="btnDelete">
  <div id="viewDiv"></div>
      <div id="line-button" class="esri-widget esri-widget--button esri-interactive" title="Draw polyline">
      <span class="esri-icon-polyline"></span>


 But when I add a second Feature Layer and attempt to put an if/else route in the JS, I'm not getting any luck. Even more simply, when I add a second FeatureLayer the promise that is issued runs into an issue handling multiple layers. The least elegant solution I can think of is to literally just duplicate the functions and promise-strings for the second FeatureLayer, but that seems pretty heavy-handed. 

If anyone has any samples or insight into editing multiple Feature Layers in the Esri JS API 4 environment, I'd be really grateful to see 'em!

Robert Scheitlin, GISP‌ -- any insight from your ginormous brain?

0 Kudos
4 Replies
Occasional Contributor II

Sorry, lots of code there. I do this all the time. I'm assuming you've got a Deferred, Promise with Deferred Resolve and Reject.   You also need an dojo/promise/all.  create an array and push each request:


allRequests.push(this._handleDeletesRequestDef(lyr.url, oids.toString()));

Then, at end, just process all requests: For example.

if (allRequests.length > 0) {


//console.log("SaveEditsHandler.js: _processAllRequests(). about to send all ");


lang.hitch(this, this._onApplyEditsFinishAll, true),

lang.hitch(this, this._onApplyEditsError, true)



I'm starting to muck about with FeatureService ApplyEdits. I think it'll just do everything all at once.

Occasional Contributor III

Hey Karen!

im still getting acquainted with JS (and especially the Esri JS API), so I apologize for potentially rudimentary clarifications that are forthcoming  

1. The allRequests.push component essentially creates a single “entry point” to execution by extracting the Object ID and passing that as a string parameter to the subsequent functions to specify the Object on which to create the edits — is my understanding right on?

2. The if-statement component checks if any events have been created (if so, there length would be > 1 and thus trigger the nested functions). This is conceptually Pythonic so this function makes more sense to me (but the asynchronous callback / promise chains are still a bit challenging   ).

One more point of to make sure we’re speaking the same language; I’m trying to edit one feature at a time from one feature service. However, I want users to choose which feature service they’re editing (either street closures OR street detours). Each editing session will be contained to a single feature of a single feature service. My issue was adding another editable feature service (then converting to a layer) without effectively rewriting all my promise chains. 

Does se that make sense? 

0 Kudos
Occasional Contributor II

The allRequests is an array. So you would push one request at a time into the array. Then, when all done, you would process all requests simultaneously. ie. You can edit Feature from layer 1. Then, edit another feature from layer 2. Then, when ALL of them are done, you would process them all (so you'll end up with an array of responses.).
Im not following your last paragraph.

0 Kudos
Occasional Contributor III

I apologize for the scatterbrainyness of the last paragraph; what I meant to convey way this:

the workflow of editing in this app is as follows: 1. users selects either ButtonA or ButtonB.      1a.ButtonA allows for editing of feature service A.

1b. Button B allows for editing of feature service B. 

2. Once a user clicks a button, the two buttons become disabled from being clicked again until a feature has been added.

3. Once a button has been clicked, the user can draw a single polyline graphic ONLY for the corresponding feature ( ie if a click ButtonA I can only create a single Polyline feature against Feature Service A. 

4. User clicks “done” and the edits are saved and the two buttons become clickable again.

I meant to convey that users will only be editing one feature at a time against a single feature service, but they have the option to choose which feature service to choose.

0 Kudos