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

27,451

Solution 1

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.

Solution 2

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

Solution 3

Here is my understanding of Custom directives.

The code below is basic overview of two way binding.

you can see it working here as well.

http://plnkr.co/edit/8dhZw5W1JyPFUiY7sXjo

<!doctype html>
<html ng-app="customCtrl">
  <head>
    <script src="http://code.angularjs.org/1.2.0-rc.2/angular.min.js"></script>
    <script>

  angular.module("customCtrl", []) //[] for setter
  .directive("contenteditable", function () {

    return {
      restrict: "A",  //A for Attribute, E for Element, C for Class & M for comment
      require: "ngModel",  //requiring ngModel to bind 2 ways.
      link: linkFunc
    }
    //----------------------------------------------------------------------//
    function linkFunc(scope, element, attributes,ngModelController) {
        // From Html to View Model
        // Attaching an event handler to trigger the View Model Update.
        // Using scope.$apply to update View Model with a function as an
        // argument that takes Value from the Html Page and update it on View Model
        element.on("keyup blur change", function () {
          scope.$apply(updateViewModel)
        })

        function updateViewModel() {
          var htmlValue = element.text()
          ngModelController.$setViewValue(htmlValue)
        }
              // from View Model to Html
              // render method of Model Controller takes a function defining how
              // to update the Html. Function gets the current value in the View Model
              // with $viewValue property of Model Controller and I used element text method
              // to update the Html just as we do in normal jQuery.
              ngModelController.$render = updateHtml

              function updateHtml() {
                var viewModelValue = ngModelController.$viewValue
                // if viewModelValue is change internally, and if it is
                // undefined, it won't update the html. That's why "" is used.
                viewModelValue = viewModelValue ? viewModelValue : ""
                element.text(viewModelValue)
              }
    // General Notes:- ngModelController is a connection between backend View Model and the 
    // front end Html. So we can use $viewValue and $setViewValue property to view backend
    // value and set backend value. For taking and setting Frontend Html Value, Element would suffice.

    }
  })

    </script>
  </head>
  <body>
    <form name="myForm">
    <label>Enter some text!!</label>
     <div contenteditable
          name="myWidget" ng-model="userContent"
          style="border: 1px solid lightgrey"></div>
     <hr>
     <textarea placeholder="Enter some text!!" ng-model="userContent"></textarea>
    </form>
  </body>
</html>

Hope, it helps someone out there.!!

Share:
27,451

Related videos on Youtube

Misha Moroshko
Author by

Misha Moroshko

I build products that make humans happier. Previously Front End engineer at Facebook. Now, reimagining live experiences at https://muso.live

Updated on July 09, 2022

Comments

  • Misha Moroshko
    Misha Moroshko almost 2 years

    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());
        }
      };
    });
    
    • Kevin Hoffman
      Kevin Hoffman about 11 years
      I just borrowed code similar to yours to get my page working (I am using contenteditable divs for edit mode as well). One problem I have is that I have a section that I'm using ng-bind-html-unsafe instead of ng-model ... How would I adapt your code to work in either situation?
    • Vanuan
      Vanuan almost 11 years
      @KevinHoffman, you don't need ng-bind-html-unsafe with this code, just use ng-model.
  • Alex Walker
    Alex Walker almost 9 years
    Try including some example code - link-only answers are discouraged.
  • Thomas.Benz
    Thomas.Benz over 8 years
    This saves me a lot of time. Thank you.
  • SANGEETH KUMAR S G
    SANGEETH KUMAR S G over 7 years
    Your answer helpful for me also. Can you suggest any method if I want to add place holder to the div along with ng-model, as the text Change me! is automatically binding to ng-model
  • Anze
    Anze over 6 years
    How to keep html formatting from document.execCommand in model? Like <font size="1">content</font>.
  • Vikas Gautam
    Vikas Gautam over 6 years
    Don't know what are you asking for. I have left angular1 since angular2+ is out.
  • Anze
    Anze over 6 years
    Sorry for poor explanation. If i manipulate textarea/div/etc.. with execCommand, it produces some html code. This html is then stipped. Model is sanitized of html code. Any way to workaround this? Like ngBindHtml?
  • Anze
    Anze over 6 years
    Solved it: instead of element.text() -> element.html(). Feeling dumb.