Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AngularJS and contentEditable two way binding doesn't work as expected

Why in the following example the initial rendered value is {{ person.name }} rather than David? How would you fix this?

Live example here

HTML:

<body ng-controller="MyCtrl">
  <div contenteditable="true" ng-model="person.name">{{ person.name }}</div>
  <pre ng-bind="person.name"></pre>
</body>

JS:

app.controller('MyCtrl', function($scope) {
  $scope.person = {name: 'David'};
});

app.directive('contenteditable', function() {
  return {
    require: 'ngModel',
    link: function(scope, element, attrs, ctrl) {
      // view -> model
      element.bind('blur', function() {
        scope.$apply(function() {
          ctrl.$setViewValue(element.html());
        });
      });

      // model -> view
      ctrl.$render = function() {
        element.html(ctrl.$viewValue);
      };

      // load init value from DOM
      ctrl.$setViewValue(element.html());
    }
  };
});
like image 582
Misha Moroshko Avatar asked Jan 28 '13 12:01

Misha Moroshko


2 Answers

The problem is that you are updating the view value when the interpolation is not finished yet.

So removing

// load init value from DOM
ctrl.$setViewValue(element.html());

or replacing it with

ctrl.$render();

will resolve the issue.

like image 168
asgoth Avatar answered Nov 18 '22 16:11

asgoth


Short answer

You're initializing the model from the DOM using this line:

ctrl.$setViewValue(element.html());

You obviously don't need to initialize it from the DOM, since you're setting the value in the controller. Just remove this initialization line.

Long answer (and probably to the different question)

This is actually a known issue: https://github.com/angular/angular.js/issues/528

See an official docs example here

Html:

<!doctype html>
<html ng-app="customControl">
  <head>
    <script src="http://code.angularjs.org/1.2.0-rc.2/angular.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <form name="myForm">
     <div contenteditable
          name="myWidget" ng-model="userContent"
          strip-br="true"
          required>Change me!</div>
      <span ng-show="myForm.myWidget.$error.required">Required!</span>
     <hr>
     <textarea ng-model="userContent"></textarea>
    </form>
  </body>
</html>

JavaScript:

angular.module('customControl', []).
  directive('contenteditable', function() {
    return {
      restrict: 'A', // only activate on element attribute
      require: '?ngModel', // get a hold of NgModelController
      link: function(scope, element, attrs, ngModel) {
        if(!ngModel) return; // do nothing if no ng-model

        // Specify how UI should be updated
        ngModel.$render = function() {
          element.html(ngModel.$viewValue || '');
        };

        // Listen for change events to enable binding
        element.on('blur keyup change', function() {
          scope.$apply(read);
        });
        read(); // initialize

        // Write data to the model
        function read() {
          var html = element.html();
          // When we clear the content editable the browser leaves a <br> behind
          // If strip-br attribute is provided then we strip this out
          if( attrs.stripBr && html == '<br>' ) {
            html = '';
          }
          ngModel.$setViewValue(html);
        }
      }
    };
  });

Plunkr

like image 14
Vanuan Avatar answered Nov 18 '22 17:11

Vanuan