angularjsngsanitize

Why do ng-bind-html and $sanitize produce different results?


I'm trying to sanitize the content of some text areas, I cannot use ng-bind-html because it breaks two way binding (ng-model does not work at the same time)

Strangely when I apply ng-bind-html to a model it produces a different result to when I use $sanitize or $sce inside of a directive.

Here's a sample I made up

http://plnkr.co/edit/iRvK4med8T9Xqs22BkOe?p=preview

First text area uses ng-bind-html, the second uses $sanitize and the third should be the code for the ng-bind-html directive as I ripped out of the AngularJS source code.

" is only corrected changed to " when using ng-bind-html, in the other two examples it changes to "

How can I replicate the results of ng-bind-html in my directive - while keeping the two way binding?

angular.module('sanitizeExample', ['ngSanitize'])
  .controller('ExampleController', ['$scope', '$sce',
    function($scope, $sce) {

      $scope.value = 'This in "quotes" for testing';
      $scope.model = 'This in "quotes" for testing';

    }
  ]).directive('sanitize', ['$sanitize', '$parse', '$sce',
    function($sanitize, $parse, $sce) {
      return {
        restrict: 'A',
        replace: true,
        scope: true,
        link: function(scope, element, attrs) {

          var process = function(input) {
            return $sanitize(input);
            //return $sce.getTrustedHtml(input);
          };

          var processed = process(scope.model);
          console.log(processed); // Output here = This in "quotes" for testing
          $parse(attrs.ngModel).assign(scope, processed);
          //element.html(processed);
        }
      };
    }
  ])
  .directive('sanitizeBindHtml', ['$parse', '$sce',
    function($parse, $sce) {
      return {
        restrict: 'A',
        replace: true,
        scope: true,
        link: function(scope, element, attrs) {

          var parsed = $parse(attrs.ngModel);

          function getStringValue() {
            var value = parsed(scope);
            getStringValue.$$unwatch = parsed.$$unwatch;
            return (value || '').toString();
          }

          scope.$watch(getStringValue, function ngBindHtmlWatchAction(value) {
            var processed = $sce.getTrustedHtml(parsed(scope)) || '';

            $parse(attrs.ngModel).assign(scope, processed)
          });
        }
      };
    }
  ]);
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.3/angular.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.3/angular-sanitize.js"></script>

<!doctype html>
<html lang="en">


<body ng-app="sanitizeExample">

  <div ng-controller="ExampleController">
    <textarea ng-bind-html="value"></textarea>
    <br/>{{value}}
    <br/>
    <br/>
    <textarea sanitize ng-model="model"></textarea>
    <br/>
    <br/>
    <textarea sanitize-bind-html ng-model="model"></textarea>

  </div>
</body>


Solution

  • It turns out like we would expect, the sanitation service is returning the same result. Placing a breakpoint inside the ngBindHtmlDirective, We can step in and see what is happening. We dive in and examine the values inside the $SanitizeProvider. The value of buf that will be returned back to the ngBindHtmlDirective is:

    This in &#34;quotes&#34; for testing

    The exact same as we get for calling $sanitize, so what's the real difference? The real difference is between a textbox's innerHTML and value. View this example plunker. You can see the difference between calling the two different methods, with the different ways of escaping a double quote. I didn't go digging though the w3 spec or the browser code, but I assume the innerHTML assignment is doing additional work under the hood of creating a documentFragment, grabbing it's textContent, then assigning that to the textbox's value. Obviously value is just grabbing the string and inserting it as is.


    So what's the problem with your directives? I see that element.html(processed) is in a comment, but uncommenting it doesn't have an affect. Well the truth is that it does work for a split second! Stepping though with the debugger, the value of the textbox is correctly set, but then a $digest cycle gets fired and immediate changes it! The truth is the ngModelDirective is getting in the way, specifically it's the $render function of the baseInputType. We can see in the code it is using the element.val method.

    How can we fix this in the directives? Require the ngModelController and override its $render function to use element.html method instead (example plunker).

    // Add require to get the controller
    require: 'ngModel',
    
    // Controller is passed in as the 4th argument
    link: function(scope, element, attrs, ngModelCtrl) {
    
    // modify the $render function to process it as HTML
    ngModelCtrl.$render = function() {
        element.html(ngModelCtrl.$isEmpty(ngModelCtrl.$viewValue) ? '' : ngModelCtrl.$viewValue);
    };