Howto use ng-switch inside ng-repeat for editing JSON API data

900 Views Asked by At

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

3

There are 3 best solutions below

2
Patrick Evans On BEST ANSWER

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>

6
Daniel Dawes On

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>

0
Nikos Paraskevopoulos On

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