55

When using ng-repeat what is the best way to be able to edit content?

In my ideal situation the added birthday would be a hyperlink, when this is tapped it will show an edit form - just the same as the current add form with an update button.

Live Preview (Plunker)

HTML:

<!DOCTYPE html>
<html>
  <head lang="en">
    <meta charset="utf-8">
    <title>Custom Plunker</title>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.3/angular.min.js"></script>
    <script>
      document.write('<base href="' + document.location + '" />');
    </script>
    <script src="app.js"></script>
    <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.2.0/css/bootstrap-combined.min.css"
    rel="stylesheet">
  </head>
<body ng-app="birthdayToDo" ng-controller="main">
    <div id="wrap">

      <!-- Begin page content -->
      <div class="container">
        <div class="page-header">
          <h1>Birthday Reminders</h1>
        </div>
            <ul ng-repeat="bday in bdays">
                <li>{{bday.name}} | {{bday.date}}</li>
            </ul>

           <form ng-show="visible" ng-submit="newBirthday()">
            <label>Name:</label>
            <input type="text" ng-model="bdayname" placeholder="Name" ng-required/>
            <label>Date:</label>
            <input type="date" ng-model="bdaydate" placeholder="Date" ng-required/>
            <br/>
            <button class="btn" type="submit">Save</button>
        </form>
      </div>

      <div id="push"></div>
    </div>

    <div id="footer">
      <div class="container">
        <a class="btn" ng-click="visible = true"><i class="icon-plus"></i>Add</a>
      </div>
    </div>
    </body>

App.js:

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

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

    // Start as not visible but when button is tapped it will show as true 

        $scope.visible = false;

    // Create the array to hold the list of Birthdays

        $scope.bdays = [];

    // Create the function to push the data into the "bdays" array

    $scope.newBirthday = function(){

        $scope.bdays.push({name:$scope.bdayname, date:$scope.bdaydate});

        $scope.bdayname = '';
        $scope.bdaydate = '';

    };
});
isherwood
  • 58,414
  • 16
  • 114
  • 157
Jess McKenzie
  • 8,345
  • 27
  • 100
  • 170
  • Does your question refer to the actual implementation or the interface design? – F Lekschas Mar 16 '13 at 19:07
  • @Flek It was meant to be the implementation the interface design is easy – Jess McKenzie Mar 18 '13 at 18:41
  • I've arrived here looking for what you wanna do, but have you noticed that you are repeating the ´ul´ element instead of the ´li´? The result is that you have as many `ul`'s as elements, what is not correct. – adripanico Apr 08 '16 at 11:10

4 Answers4

71

You should put the form inside each node and use ng-show and ng-hide to enable and disable editing, respectively. Something like this:

<li>
  <span ng-hide="editing" ng-click="editing = true">{{bday.name}} | {{bday.date}}</span>
  <form ng-show="editing" ng-submit="editing = false">
    <label>Name:</label>
    <input type="text" ng-model="bday.name" placeholder="Name" ng-required/>
    <label>Date:</label>
    <input type="date" ng-model="bday.date" placeholder="Date" ng-required/>
    <br/>
    <button class="btn" type="submit">Save</button>
   </form>
 </li>

The key points here are:

  • I've changed controls ng-model to the local scope
  • Added ng-show to form so we can show it while editing
  • Added a span with a ng-hide to hide the content while editing
  • Added a ng-click, that could be in any other element, that toggles editing to true
  • Changed ng-submit to toggle editing to false

Here is your updated Plunker.

malana
  • 5,045
  • 3
  • 28
  • 41
Caio Cunha
  • 23,326
  • 6
  • 78
  • 74
  • Thanks for your excellent answer :) I have an issue and it seems when I edit it is showing the add form too - because its set to true – Jess McKenzie Mar 16 '13 at 19:24
  • If I got your question right, just set `$scope.visible = false` on `newBirthday()`. This will make the Add form to disappear when you add an item. – Caio Cunha Mar 16 '13 at 19:29
  • To limit processing, I would put this in a directive, so the DOM manipulation and compilation won't occur unless it's needed. – Josh David Miller Mar 16 '13 at 21:19
  • @JoshDavidMiller Can I have an explanation/example? – Jess McKenzie Mar 18 '13 at 18:39
  • 7
    My point was that most entries won't need to be edited, yet we're adding processing on all of them to add the capability. `ngShow` and `ngHide` don't stop functionality - they just add `display: none` to the CSS. So all of the bindings are still wired and the DOM is still manipulated. If we did this as a directive, we can call the form creation on demand. Here's a simple edit-in-place directive I created a while back to illustrate: http://jsfiddle.net/joshdmiller/NDFHg/. And a more complicated one: http://plnkr.co/edit/LVUIQD. – Josh David Miller Mar 18 '13 at 20:58
  • You could also use [ui-if](http://angular-ui.github.com) to control element compilation and destruction. It work pretty much like `ng-show`/`ng-hide`, except it will remove the element from DOM and destroy it instead of 'display-noning' it. – Caio Cunha Mar 18 '13 at 21:50
  • 1
    Since [unstable Angular 1.1.5](https://github.com/angular/angular.js/blob/master/CHANGELOG.md#115-triangle-squarification-2013-05-22), Angular now supports [ngIf](https://github.com/angular/angular.js/commit/2f96fbd17577685bc013a4f7ced06664af253944) out of the box. Just like to reinforce that 1.1.5 is a unstable release. – Caio Cunha Jun 04 '13 at 13:01
  • How would I push these changes back to my web service? I've tried a call to my controller on the button click and on the form submit but neither pass the form data as a parameter. Should I be watching my collection of bdays for changes instead of handling the form submit? – BenCr Sep 12 '13 at 13:24
  • thanks for this man, didnt knew Angularjs can do this too :D didnt needed any other UI plugins :D – STEEL Nov 07 '13 at 06:55
  • I got this to work, and the changes are made "locally", but how do I make these changes back to the server? I'm using this with AngularFire and I have a 3-way data binding. – nodebase Feb 25 '15 at 04:42
  • My current perf issue got me here. For those who potentially have large list of items(700 in my case), you might want to avoid this approach. Otherwise you will run into "script error" issue. Should have thought of paging in the first place! – Cuong Nguyen Jul 14 '16 at 10:31
  • Oh yeah! For sure. If you have too many items in your list, you better go a different approach, maybe even create a custom directive that is able to act exactly when and on what you need. Anyway, try reducing non-bind-once when you know there will be too many items. – Caio Cunha Jul 15 '16 at 16:45
26

I was looking for a inline editing solution and I found a plunker that seemed promising, but it didn't work for me out of the box. After some tinkering with the code I got it working. Kudos to the person who made the initial effort to code this piece.

The example is available here http://plnkr.co/edit/EsW7mV?p=preview

Here goes the code:

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

  $scope.updateTodo = function(indx) {
    console.log(indx);
  };

  $scope.cancelEdit = function(value) {
    console.log('Canceled editing', value);
  };

  $scope.todos = [
    {id:123, title: 'Lord of the things'},
    {id:321, title: 'Hoovering heights'},
    {id:231, title: 'Watership brown'}
  ];
});

// On esc event
app.directive('onEsc', function() {
  return function(scope, elm, attr) {
    elm.bind('keydown', function(e) {
      if (e.keyCode === 27) {
        scope.$apply(attr.onEsc);
      }
    });
  };
});

// On enter event
app.directive('onEnter', function() {
  return function(scope, elm, attr) {
    elm.bind('keypress', function(e) {
      if (e.keyCode === 13) {
        scope.$apply(attr.onEnter);
      }
    });
  };
});

// Inline edit directive
app.directive('inlineEdit', function($timeout) {
  return {
    scope: {
      model: '=inlineEdit',
      handleSave: '&onSave',
      handleCancel: '&onCancel'
    },
    link: function(scope, elm, attr) {
      var previousValue;

      scope.edit = function() {
        scope.editMode = true;
        previousValue = scope.model;

        $timeout(function() {
          elm.find('input')[0].focus();
        }, 0, false);
      };
      scope.save = function() {
        scope.editMode = false;
        scope.handleSave({value: scope.model});
      };
      scope.cancel = function() {
        scope.editMode = false;
        scope.model = previousValue;
        scope.handleCancel({value: scope.model});
      };
    },
    templateUrl: 'inline-edit.html'
  };
});

Directive template:

<div>
  <input type="text" on-enter="save()" on-esc="cancel()" ng-model="model" ng-show="editMode">
  <button ng-click="cancel()" ng-show="editMode">cancel</button>
  <button ng-click="save()" ng-show="editMode">save</button>
  <span ng-mouseenter="showEdit = true" ng-mouseleave="showEdit = false">
    <span ng-hide="editMode" ng-click="edit()">{{model}}</span>
    <a ng-show="showEdit" ng-click="edit()">edit</a>
  </span>
</div>

To use it just add water:

<div ng-repeat="todo in todos" 
     inline-edit="todo.title" 
     on-save="updateTodo($index)" 
     on-cancel="cancelEdit(todo.title)"></div>

UPDATE:

Another option is to use the readymade Xeditable for AngularJS:

http://vitalets.github.io/angular-xeditable/

John P
  • 15,035
  • 4
  • 48
  • 56
7

I've modified your plunker to get it working via angular-xeditable:

http://plnkr.co/edit/xUDrOS?p=preview

It is common solution for inline editing - you creale hyperlinks with editable-text directive that toggles into <input type="text"> tag:

<a href="#" editable-text="bday.name" ng-click="myform.$show()" e-placeholder="Name">
    {{bday.name || 'empty'}}
</a>

For date I used editable-date directive that toggles into html5 <input type="date">.

vitalets
  • 4,675
  • 1
  • 35
  • 35
4

Since this is a common piece of functionality it's a good idea to write a directive for this. In fact, someone already did that and open sourced it. I used editablespan library in one of my projects and it worked perfectly, highly recommended.

Michał Kwiatkowski
  • 9,196
  • 2
  • 25
  • 20