16

I'm learning AngularJS. I've come across something I can't explain, nor can I find any explanation for (or solution).

I have a simple AngularJS app and I am attempting to bind a <span contenteditable="true"> to a value, but it doesn't work. EG:

<!-- Works as expected -->
<input data-ng-model="chunk.value"></input>

<!-- Shows value, but doesn't bind - changes not reflected in model -->
<span contenteditable="true">{{chunk.value}}</span>

<!-- This is empty -->
<span contenteditable="true" data-ng-model="chunk.value"></span>

How can I make the last span use 2-way binding, such that editing its value updates chunk.value and vice versa?

georgeawg
  • 48,608
  • 13
  • 72
  • 95
Alex McMillan
  • 17,096
  • 12
  • 55
  • 88
  • The span element is read only. You need some kind of input for 2 way binding with a ng-model. How did you plan on changing the content in the second span? – VtoCorleone May 07 '14 at 21:16
  • How can there be something that is read-only in html? I planned on changing it via the model, or via the user clicking it and typing - hence `contenteditable="true"` – Alex McMillan May 07 '14 at 21:19
  • How do you enter data on a span or label? You can change their values but they are reading whatever values you give them. – VtoCorleone May 07 '14 at 21:22
  • 1) Hit F12 to bring up dev console, right here on this page right now. 2) type `$('span').text('potato');` and hit enter. 3) Notice updated content. – Alex McMillan May 07 '14 at 21:32
  • Do you expect your users to use the developer tools to change the text? A user can edit certain DOM elements, span is not one of them. That's what he means by read only. – JonK Dec 24 '15 at 03:55
  • @JonK Go read up about `contentEditable`. – Alex McMillan Feb 09 '16 at 20:07

5 Answers5

23

ng-bind! Use ng-bind for one-way binding in 'span'.

Please refer here for an example: https://docs.angularjs.org/api/ng/directive/ngBind

So your line would be: <span contenteditable="true" ng-bind="chunk.value"></span>

Hope this help

georgeawg
  • 48,608
  • 13
  • 72
  • 95
Siti Farah
  • 270
  • 2
  • 5
  • 1
    ng-bind is one-way binding. Changing the content in the span using contenteditable won't change the underlying model. – Will Nov 03 '16 at 02:18
1

To make ng-model work with contenteditable <span> elements, use a custom directive:

app.directive('contenteditable', ['$sce', function($sce) {
    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($sce.getTrustedHtml(ngModel.$viewValue || ''));
        };

        // Listen for change events to enable binding
        element.on('blur keyup change', function() {
          scope.$evalAsync(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);
        }
      }
    };
}]);

Usage:

<span contenteditable ng-model="userContent">Change me!</span>
<p>{{userContent}}</p>

For more infomation, see


The DEMO

angular.module('customControl', ['ngSanitize'])
.directive('contenteditable', ['$sce', function($sce) {
    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($sce.getTrustedHtml(ngModel.$viewValue || ''));
        };

        // Listen for change events to enable binding
        element.on('blur keyup change', function() {
          scope.$evalAsync(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);
        }
      }
    };
  }]);
[contenteditable] {
  border: 1px solid black;
  background-color: white;
  min-height: 20px;
}
<script src="//unpkg.com/angular/angular.js"></script>
<script src="//unpkg.com/angular-sanitize/angular-sanitize.js"></script>
<body ng-app="customControl">
 <span contenteditable ng-model="userContent">Change me!</span>
 <hr>
 Content={{userContent}}
</body>
Community
  • 1
  • 1
georgeawg
  • 48,608
  • 13
  • 72
  • 95
0

The ngModel won't work as @VtoCorleone pointed out. ngModel docs

The ngModel directive binds an input,select, textarea (or custom form control) to a property on the scope using NgModelController, which is created and exposed by this directive.

You may have a look at the contenteditable directive.

Otherwise, Potential workaround: have a function that gets called. That function then updates the $scope.chunk.value within your controller. And it would take care of the other elements' contents as the binding gets updated.

I'm not sure the exact look or functionality that you're going for, but just put it inside of a <textarea> and style it to look like a <span> (no border or background, etc). Then when it is in focus, you add additional styling to know that it can be edited. This way would allow you to use the ng-model as it is intended to be used. Here is a basic implementation of this approach: Plunker

EnigmaRM
  • 7,523
  • 11
  • 46
  • 72
  • Sorry I don't follow - how would this function get called? Should I use jQuery `.on('blur', ...)` or something? – Alex McMillan May 07 '14 at 21:34
  • My aim is for there to be no styling whatsoever - that's why I was trying to use a span. I'm looking for paragraphs of text that are editable, something that looks like Notepad for example. A "chunk" is simple a piece of text within a paragraph. – Alex McMillan May 07 '14 at 21:36
  • I'd probably do it as a click event that is attached to some sort of save or update button – EnigmaRM May 07 '14 at 21:36
  • I thought this is what AngularJS was *good* at? I can't force my users to perform a click for every update - thats ludicrous! – Alex McMillan May 07 '14 at 21:37
  • If you want something like a notepad, why aren't you using a textarea instead of a span? Either I am completely misunderstanding what you're trying to do or you need to understand HTML and JS basics more before jumping into Angular. – VtoCorleone May 07 '14 at 21:40
  • It is good at this. But you're trying to use Angular in a way that it was not intended to be used. – EnigmaRM May 07 '14 at 21:40
  • @AlexMcMillan You can still achieve the look you want. You just need to use a proper element to do so i.e. `textarea`. Yes, you have to use some styling. But that's to be expected. And it's actually very minimal styling. – EnigmaRM May 07 '14 at 21:41
  • I'm building a legal-document editor. I'm trying to enable fine-grained control over both presentation and structure. I have a structure of `` for each paragraph, for several reasons. I want to bind the content of the spans to chunks within my database. I don't see how this is using Angular in a way it was not intended. I cannot use a textarea for the entire thing, as it doesn't provide the control I need. – Alex McMillan May 07 '14 at 21:42
  • Can't you simply replace your `span` with `textarea`? – EnigmaRM May 07 '14 at 21:48
  • And Angular intends to use `ngModel` with `input`, `select`, and `textarea` (or custom form control). That's how you are using it in a way that is not intended. – EnigmaRM May 07 '14 at 21:49
  • @AlexMcMillan, I added a basic plunker of what I'm suggesting. http://plnkr.co/edit/EsZfXaRqNMOVLh7WToVi?p=preview – EnigmaRM May 07 '14 at 21:55
  • There will be MANY of these on the page, I thought `span`s were considered more "lightweight"? Or does 500 `span`s on a page decrease performance exactly as much as 500 `textarea`s? – Alex McMillan May 07 '14 at 21:56
  • I'm not sure it makes a big difference either way. And if there will be 500 of them, make sure you set this up to use a `ng-repeat` – EnigmaRM May 07 '14 at 21:58
  • There will be potentially 500 Nodes, each with 1-10 Chunks. Thanks for the Plunkr, but the "Chunks" need to flow as one (think sentences) – Alex McMillan May 07 '14 at 22:00
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/52240/discussion-between-enigmarm-and-alex-mcmillan) – EnigmaRM May 07 '14 at 22:00
0

ng-model is not meant to be used with span. If you absolutely need that you can write a custom directive for this. This directive will set up a keydown,keyup listener on the contentEditable span and update the scope model (within $apply()). This will bind span content to model.

I quickly created a plunker for you. Check it out. It syncs the <span> content to scope model. Open up the browser console to see the scope model update whenever you type something.

Sandeep Panda
  • 723
  • 6
  • 9
  • Why would you do all of this? The span is reading from the scope's model. Why would have another scope property listening for the span's model property? – VtoCorleone May 07 '14 at 21:29
  • @VtoCorleone we certainly don't need binding from model to span (it has already been taken care of). Just updating the scope model when the span content changes will solve our problem. – Sandeep Panda May 07 '14 at 21:33
0

By adding ng-model-options="{ getterSetter: true }" behavior to an element that has ng-model attached to it. You can also add ng-model-options="{ getterSetter: true }" to a <form>, which will enable this behavior for all <input>s within it.

Example shows how to use ngModel with a getter/setter: demo page

Prabhakaran S
  • 391
  • 2
  • 5