Thomas Teilmann Thomas Teilmann - 5 months ago 32
Javascript Question

Textarea ng-bind, where value has a prefix and suffix

I'm trying to complete something that should be fairly simple, in my opinion. And it also is, when using something like a text input. I'm trying to create model binding, on a textarea, where the value, when the user is typing, is shown with a prefix and a suffix. The prefix and suffix being quotation marks:

“My awesome quote”


The problem is, that i'm currently using ng-model, which i ofcourse cannot use for this. I was thinking about binding to a variable, holding the value without the prefix and suffix, and then watching that variable. When the variable, with the original value, then changes, i would write the value with a pre and suffix to another variable, on the scope. That variable would then be shown in the textarea, as the user types. The only problem is, that a textarea, unlike an input field, doesn't have a value property.

Is this even possible?

EDIT

If i where to achieve this with an input text field, i would create a variable called A, to hold the raw value that changes when the user is typing.
When A changes, i would then take the raw value, put quotes around it and store that new value in another variable, also on the scope. That new variable is called B

The input field would then use ng-bind on the A variable, and show the content from the B variable, using the input fields value attribute. Something like below:

<input type="text" ng-bind="A" value="{{B}}">


I don't have time to create a fiddle right now, but i will try to do it later this week. The description above is all in theory, as i have not tested it yet.

Answer

Your requirements are quite interesting and can be split into two main functionalities:

1. Add a prefix and suffix to a view value without affecting the model value

This has been achieved using NgModelController. I've created a watcher on the ngModelController.$viewValue and whenever it changes, if it doesn't contain the prefix or suffix, I add those values.

2. Don't allow the user to interfere with the prefix or suffix

This is a bit tricky, but I found a clever way on SO of getting and setting the caret position of an input or textarea element.

Here's a working example:

angular
  .module('myApp', [])
  .directive('beautifyText', function() {
    return {
      link: function(scope, iElement, iAttrs, ngModelController) {
        // Adds the prefix and suffix
        (function() {
          var prefix = iAttrs.beautifyTextWithPrefix;
          var suffix = iAttrs.beautifyTextWithSuffix;

          ngModelController.$parsers.push(function(value) {
            if (angular.isString(value)) {
              return value.replace(new RegExp('^' + prefix), '').replace(new RegExp(suffix + '$'), '');
            }

            return '';
          });

          scope.$watch(function() {
            return ngModelController.$viewValue;
          }, function(newValue) {
            if (angular.isString(newValue) && newValue.length > 0) {
              if (angular.isString(ngModelController.$modelValue) && ngModelController.$modelValue.length === 0 && newValue.length === 1) {
                ngModelController.$viewValue = '';
                ngModelController.$render();
                return;
              }

              if (!isBeautifiedWithPrefix(newValue)) {
                ngModelController.$viewValue = prefix + newValue;
              }

              if (!isBeautifiedWithSuffix(newValue)) {
                ngModelController.$viewValue = newValue + suffix;
              }

              ngModelController.$render();
            } else {
              ngModelController.$viewValue = '';
              ngModelController.$render();
            }
          });

          function isBeautifiedWithPrefix(value) {
            return value.match(new RegExp('^' + prefix)) !== null;
          }

          function isBeautifiedWithSuffix(value) {
            return value.match(new RegExp(suffix + '$')) !== null;
          }
        })();

        // Changes the caret position
        (function() {
          var element = iElement[0];

          // http://stackoverflow.com/questions/7745867/how-do-you-get-the-cursor-position-in-a-textarea#answer-7745998
          function getCursorPos() {
            if ("selectionStart" in element && document.activeElement == element) {
              return {
                start: element.selectionStart,
                end: element.selectionEnd
              };
            } else if (element.createTextRange) {
              var sel = document.selection.createRange();
              if (sel.parentElement() === element) {
                var rng = element.createTextRange();
                rng.moveToBookmark(sel.getBookmark());
                for (var len = 0; rng.compareEndPoints("EndToStart", rng) > 0; rng.moveEnd("character", -1)) {
                  len++;
                }
                rng.setEndPoint("StartToStart", element.createTextRange());
                for (var pos = {
                  start: 0,
                  end: len
                }; rng.compareEndPoints("EndToStart", rng) > 0; rng.moveEnd("character", -1)) {
                  pos.start++;
                  pos.end++;
                }
                return pos;
              }
            }
            return -1;
          }

          // http://stackoverflow.com/questions/7745867/how-do-you-get-the-cursor-position-in-a-textarea#answer-7745998
          function setCursorPos(start, end) {
            if (arguments.length < 2) {
              end = start;
            }

            if ("selectionStart" in element) {
              element.selectionStart = start;
              element.selectionEnd = end;
            } else if (element.createTextRange) {
              var rng = element.createTextRange();
              rng.moveStart("character", start);
              rng.collapse();
              rng.moveEnd("character", end - start);
              rng.select();
            }
          }

          iElement.bind('mousedown mouseup keydown keyup', function() {
            if (ngModelController.$viewValue.length > 0) {
              var caretPosition = getCursorPos();

              if (caretPosition.start === 0) {
                setCursorPos(1, caretPosition.end < 1 ? 1 : caretPosition.end);
              }

              if (caretPosition.end === ngModelController.$viewValue.length) {
                setCursorPos(caretPosition.start, ngModelController.$viewValue.length - 1);
              }
            }
          });
        })();
      },
      restrict: 'A',
      require: 'ngModel'
    }
  });
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.7/angular.min.js"></script>

<div ng-app="myApp">
  <textarea ng-model="myVariable" beautify-text beautify-text-with-prefix="“" beautify-text-with-suffix="”"></textarea>
  <p>myVariable: {{ myVariable }}</p>
</div>