1

I know this is a recurring question but unfortunately I couldn't find a proper answer to my case.

Basically I'm getting data from an JSON API endpoint which gets displayed in a table using ng-repeat. I now want to ng-switch the view to input fields for amending the data (and sending it later back to the server).

Atm, my solutions depends on having a property in the data which I don't really like. I'm sure there's a smarter way than injecting this property after having retrieved the data - any suggestions?

HTML:

<tbody>
  <tr ng-repeat="item in data" ng-switch on="item.edit" >
    <td ng-switch-default ng-bind="item.color"></td>
    <td ng-switch-when='true'>
      <input type="text" ng-model="item.color" />
    </td>
    <td ng-switch-default><button ng-click="switch(item)">edit</button></td>
    <td ng-switch-when='true'><button ng-click="send(item)">send</button></td>
  </tr>
</tbody>

JS:

var app = angular.module('myApp', []);

app.controller('MyCtrl', function($scope) {

  $scope.switch = function (item) {
    if (item.edit) {
      item.edit = false;
    } else {
      item.edit = true;
    }
  };

  $scope.send = function (item) {
    if (item.edit) {
      // data is sent...
      item.edit = false;
    } else {
      item.edit = true;
    }
  };

  $scope.data = [
      {color: 'blue', edit: false},
      {color: 'green', edit: false},
      {color: 'orange', edit: false}];
});

thanks in advance!

here's a plunker: http://plnkr.co/edit/h8ar4S43JUvjHurzLgT0?p=preview

skriems
  • 23
  • 7

3 Answers3

1

If you do not want to put your flags on your data objects than you will need to use a separate object to store them. With WeakMaps you can easily associate the data object, or the element itself, with a flags object. If you are targeting older browsers you will need to find a similar way to associate the data object / or element to the flags object

JS

let map = new WeakMap();
$scope.editing = function(item){
    return map.get(item).edit;
}
$scope.switch = function (item) {
    let flags = map.get(item);
    if (flags.edit) {
        flags.edit = false;
    } else {
        flags.edit = true;
    }
};
//Note you could combine switch and send into a single toggle function
$scope.send = function (item) {
    let flags = map.get(item);
    if (flags.edit) {
        flags.edit = false;
    } else {
        flags.edit = true;
    }
};
$scope.data = [
  {color: 'blue'},
  {color: 'green'},
  {color: 'orange'}
];
//Create an empty flags object for each data item
for(let item of $scope.data){
   map.set(item,{});
}

HTML

<tr ng-repeat="item in data" ng-switch on="editing(item)" >
    <td ng-switch-default ng-bind="item.color"></td>
    <td ng-switch-when='true'>
        <input type="text" ng-model="item.color" />
    </td>
    <td ng-switch-default><button ng-click="switch(item)">edit</button></td>
    <td ng-switch-when='true'><button ng-click="send(item)">send</button></td>
</tr>

Demo

// Code goes here
var app = angular.module('myApp', []);

app.controller('MyCtrl', function($scope) {
  var map = new WeakMap();
  
  //Using fat arrow less code to write
  $scope.editing = item=>map.get(item).edit;
  
  
  //Since "switch" and "send" had similar 
  //toggling code just combined them
  //Also no need to use if statement, just use the NOT operator
  //to toggle the edit flag
  $scope.toggle = item=>{
    let flags = map.get(item);
    flags.edit = !flags.edit;
  };

  $scope.switch = item=>{
    $scope.toggle(item);
    //Do some switching? 
    //if not doing anything else just 
    //call toggle in the ng-click
  };
  $scope.send = item=>{
    $scope.toggle(item);
    //Do some sending
  };

  $scope.data = [
      {color: 'blue'},
      {color: 'green'},
      {color: 'orange'}];
      
  for(let item of $scope.data){
    map.set(item,{});
  }
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myApp" ng-controller="MyCtrl">
  <table>
    <thead>
      <tr>
        <th width="180">Column</th>
        <th>Edit</th>
      </tr>
    </thead>
    <tbody>
      <tr ng-repeat="item in data" ng-switch on="editing(item)" >
        <td ng-switch-default ng-bind="item.color"></td>
        <td ng-switch-when='true'>
          <input type="text" ng-model="item.color" />
        </td>
        <td ng-switch-default><button ng-click="switch(item)">edit</button></td>
        <td ng-switch-when='true'><button ng-click="send(item)">send</button></td>
      </tr>
    </tbody>
  </table><br>
  "$scope.data" should never change after hitting edit/send since the flag is no longer on the data item object:
  <code><pre>{{data}}</pre></code>
</div>
Patrick Evans
  • 41,991
  • 6
  • 74
  • 87
  • Hey Patrick, thanks for this solution. This definately answers best the initial question hence this is accepted, though I'm considering storing the flag on the data b/c it's not that crucial. I will have a deeper look at WeakMap later b/c I don't get your last point with the flag not beeing on the data item anymore. My thaughts atm: after $http.put I would have to resync and rebuild the map... – skriems May 17 '16 at 16:57
  • @conrel, If your data isn't changed, eg you don't do another `$scope.data = [...]`, in the `$http.put` callback, than the map retains the association, you do not need to rebuild the map. _"don't get your last point with the flag not beeing on the data item"_ it means that `edit` isn't on the `item` object anymore, but instead is on the `flags` object so you do not have to "clean" your data before sending it back to the server – Patrick Evans May 17 '16 at 17:05
0

I am using ng-show instead, but hopefully this demonstrates a better approach:

http://plnkr.co/edit/63Io7k1mJcfppxBQUVef?p=preview

I am using ng-show instead and I just append the "edit" to the object implicitly when it's needed, given you are not immediately needing to set it to true. The lack of the property will mean it returns false.

Markup:

<tbody> <tr ng-repeat="item in data"> <td ng-show="!item.edit" ng-bind="item.color"></td> <td ng-show='item.edit'> <input type="text" ng-model="item.color" /> </td> <td><button ng-click="edit(item)">{{item.edit ? "Send" : "Edit"}}</button></td> </tr> </tbody>

Daniel Dawes
  • 975
  • 5
  • 16
  • This would still put the `edit` flag on the data object, OP wants to not have to rely on putting the flag on the data object. – Patrick Evans May 17 '16 at 16:22
  • So just strip off the property before sending it? If OP is that desperate never to put that on, then create a separate array that maps to this one and use a boolean to indicate if you are editing, , e.g. [1,0,0]. Map it just using the same index as the data, for simplicity. – Daniel Dawes May 17 '16 at 16:29
  • Hey Daniel, thanks for your suggestion. I'm new to angular so correct me if I'm wrong but I'm a bit concerned about performance in this approach. Using ng-show/-hide leads to alot of bindings which gets updated in each digest, right? Therefore I wanted to use ng-if/-switch edit: I have alot of data :) Pagination is on the list... – skriems May 17 '16 at 16:34
  • You are sort of half way there, ng-show & ng-hide toggle the visibility of an element that’s already in the DOM, while ng-switch and ng-If detach and reattach the elements from the DOM. So switch is probably preferred if you are optimizing a page. It's not very well documented actually. The digest cycle will still check the status of them either way though. – Daniel Dawes May 17 '16 at 16:36
  • exactly, but afaik the hidden elements are evaluated costing performance – skriems May 17 '16 at 16:38
  • I did edit the comment to illustrate, either way the digest loop will evaluate whether or not it needs to adjust the DOM based on the data. The saving is around what's actually rendered, page size being a factor... – Daniel Dawes May 17 '16 at 16:40
0

For cases like this I always encapsulate the view state in a directive. Here it means to create a directive for each row and move the item.edit flag in that directive.

A very naive implementation follows:

HTML:

<tbody>
    <tr ng-repeat="item in data" inplace-edit="item" send-callback="send(item)"></tr>
</tbody>

JS:

app.directive('inplaceEdit', function() {
  return {
    restrict: 'A',
    template:
      '<td ng-if="!inEditMode" ng-bind="item.color"></td>' +
      '<td ng-if="inEditMode">' +
        '<input type="text" ng-model="item.color" />' +
      '</td>' +
      '<td ng-if="!inEditMode"><button ng-click="toEditMode()">edit</button></td>' +
      '<td ng-if="inEditMode"><button ng-click="send()">send</button></td>',
    scope: {
      item: '=inplaceEdit',
      sendCallback: '&'
    },
    link: function(scope) {
      scope.inEditMode = false;

      scope.toEditMode = function() {
        scope.inEditMode = true;
      };

      scope.send = function() {
        scope.sendCallback({item: scope.item});
        scope.inEditMode = false;
      };
    }
  };
});

See forked plunk: http://plnkr.co/edit/BS6a866aiy3BA9MX0Flx?p=preview

What I would add to modernize this:

  1. controllerAs, bindToController
  2. Some code to rollback/undo the changes (i.e. a "cancel" button in edit mode)
  3. Use Angular 1.5.x and the one-way binding: item: '>inplaceEdit' or integrate the inplace-edit directive with ng-model
Nikos Paraskevopoulos
  • 39,514
  • 12
  • 85
  • 90