Sunday, October 6, 2013

How make a two-way binding to your contenteditable DIV or SPAN using AngularJS directives

Consider you are using contenteditable attribute to make a <DIV> or <SPAN> editable by user. So once user clicks on it, she will be able to change the existing text. This is easier than imagine:

<div contenteditable>Your default text</div>

Now you want to use angularJS to bind the text of this div into a property in your AngularJS model.
For example consider the AngularJS scope of the div is a user information and you want to bind this div to "Address" property. The resulting div should be something like this:

<div contenteditable name="anyName" ng-model="Address" strip-br="true"></div>

Now you need to add the following AngularJS directive to your java script code:

angular.module('myDirectives', []).
  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(readViewText);
              });

              // No need to initialize, AngularJS will initialize the text based on ng-model attribute

              // Write data to the model
              function readViewText() {
                  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);
              }
          }
      };
  });

Also please don't forget to add the module name (in this case myDirectives) to the dependency list of your main application module. Like this pattern:

var app = angular.module('angularjs-starter', ['module1', 'module2', ..., 'myDirectives']);

Now just add this class to your CSS files to show editable areas in a proper way:
[contenteditable]:focus {
    border-style: dashed;
    border-color: black;
    border-width: 3px;
}

The rest is to run your code and see how it works. :)

Note 1: the above directive is a slightly modified version of AngularJS sample directive for contenteditable.
Note 2: In the directive I have removed the code that gets the first value from HTML because I needed to initialized the visible text of my div only based on the value of my model.

Sample:
Your HTML before making editable:
<span>{{Address.City}}</span>

Your HTML after making editable:

<span contenteditable ng-model="Address.City" strip-br="true" required/>


Hope it helps you save time.

12 comments:

  1. Thanks that indeed saved my time! :-)

    ReplyDelete
  2. Thank you. You saved a lot of my time!!!

    ReplyDelete
  3. Thanks. It saved me a lot of time.

    ReplyDelete
  4. Great, thanks a lot. I wasted lot of time trying too many different things.

    ReplyDelete
  5. Thanks so much. That was really helpful.

    ReplyDelete
  6. Great work :)
    +1 for overriding the render method

    ReplyDelete
  7. I am getting this every time I make a return /div

    Hello there - here is a test
    sdsdsdsd'sd
    ssdsdsdsd

    Shows like this in a div:
    Hello there - here is a testdivsdsdsdsd'sd/divdivssdsdsdsd/div

    Any way to fix? (it actually shows the whole div tag- I cannot put it in as this form will reject it.

    ReplyDelete
  8. Hi Mehran,

    Thanks for the AngularJS Tutorial here, It is very useful for me.It is real easy to understand man.

    ReplyDelete
  9. how to disable the text entering in the span tag and enabling it upon a button click
    I used ng-disable = "false". It didnt work.

    ReplyDelete
  10. Thanks http://arudhrainnovations.com

    ReplyDelete