3

I am implementing a web interface for a legacy system, so data from server is fixed. This server data specifies various controls to be shown to the user, e.g. combo-boxes, buttons etc. I have parsed the server data and added the HTML for the controls via $sce.trustAsHtml().

The problem is that the controls are not bound to the model. And if I put in an ng-change event handler, it is not called with user edits.

I read this helpful post: call function inside $sce.trustAsHtml() string in Angular js that indicates:

ng-bind-html will simply insert plain old html and not bother compiling it (so any directives in the html will not be processed by angular.

So this seems to be my problem. But I don't understand Angular well enough to understand the solution offered by the above post.

I am displaying the dynamic text in the following HTML

<span ng-repeat="i in range(0, item.Data.DisplayText.JSArray.length-1)">
  <span ng-bind-html="item.Data.DisplayText.JSArray[i] | trusted_html"></span>
  <span ng-show="!$last"><br></span>
</span>

Here is the definition for range():

$scope.range = function(min, max){
  var input = [];
  for (var i=min; i<=max; i++) input.push(i);
  return input;
};

And here is the definition for the filter:

app.filter('trusted_html', ['$sce', function($sce){
    return function(text) {
        return $sce.trustAsHtml(text);
    };
}]);

I have had help setting parts of this up, and have gleaned solutions from here on stack-overflow. I apologize that can't give links to these original solutions.

What I think I need to do is to extend my trusted_html filter such that it $compiles the text. But I don't know if that would compile the entire DOM (potentially slow) or just the text parameter.

Any help will be appreciated.


ADDENDUM:

I appreciate @Simeon Cheeseman's response. He indicated that a directive would be better than a filter, as I initially wanted. I have been studying the documentation and reading posts, and it seems he is 100% correct. DOM manipulation should occur in directives. However, I don't like the idea of having one directive (e.g. 'compile-template' from the example in the link above) fix the short-comings of another directive (ng-bind-html). It seems poor form to me. How do I know that the ng-bind-html directive will be executed before the compile-template directive is? And what happens if the execution order is reversed?

So I am going to consider if they could be combined into one function. I will think out loud here below. If anyone sees a problem, please let me know.

First, I looked at the ng-bind-html directive from angular.js found here: https://code.angularjs.org/1.2.9/angular.js and in this file, I searched for 'ngBindHTML'.

var ngBindHtmlDirective = ['$sce', '$parse', function($sce, $parse) {
  return function(scope, element, attr) {
    element.addClass('ng-binding').data('$binding', attr.ngBindHtml);

    var parsed = $parse(attr.ngBindHtml);
    function getStringValue() { return (parsed(scope) || '').toString(); }

    scope.$watch(getStringValue, function ngBindHtmlWatchAction(value) {
      element.html($sce.getTrustedHtml(parsed(scope)) || '');
    });
 };

}];

The above is not in the form of app.directive(), and it doesn't return a {link: fun()} object. So I am not exactly sure if this directly applicable to compare to the compile-template directive below. However, I did find, on line 2025, the following. So I think I am on the right path:

ngBindHtml: ngBindHtmlDirective,

So this ngBindHtmlDirective is somehow acting as ngBindHtml.

I am going to pick apart the code above, and reform it. And FYI, I am relatively new to javascript, and anonymous functions and closures are still a bit new to me. So I am trying to remove anonymous functions for my clarity. I'll add some comments from info I have picked up on Angular's website etc.

var DirectiveFnFactory = function($sce, $parse) {

  var DirectiveHandlerFn = function(scope, element, attr) {
    //Input:  scope = an Angular scope object 
    //        element = the jqLite-wrapped element that this directive matches
    //        attr = a hash object with key-value pairs of normalized attribute 
    //               names and their corresponding attribute values
    //closure scope input: $compile object
    //                     $parse object
    //Result: none

    element.addClass('ng-binding');
    // .addClass is jQuery:  http://api.jquery.com/addclass/
    //  adds class to element
    element.data('$binding', attr.ngBindHtml);
    //  adds value to key of '$binding';

    var parsed = $parse(attr.ngBindHtml); 
    //Input:  attr.ngBindHtml should be the HTML to be bound.
    //Result:  $parse() returns a function which represents the 
    //         compiled input expression.
    //  This function will have this signature: 
    //     function(context, locals)    
    //            context – {object} – an object against which any expressions 
    //                                 embedded in the strings are evaluated against 
    //                                 (typically a scope object).
    //            locals – {object=} – local variables context object, useful for 
    //                                 overriding values in context.
    //     The returned function also has the following properties:
    //       literal – {boolean} – whether the expression's top-level node is a 
    //                             JavaScript literal.
    //                           constant – {boolean} – whether the expression is made 
    //                             entirely of JavaScript constant literals.
    //        assign – {?function(context, value)} – if the expression is assignable, 
    //                             this will be set to a function to change its value 
    //                             on the given context.

    function getStringValue() { return (parsed(scope) || '').toString(); }
    //First, this is executing the function that was returned by parsed(), 
    //    passing scope that was given to DirectiveHandlerFn()
    //Next converting Fn() output to string. 
    //In the case of binding HTML. I would think that the output of this would be HTML

    var watchListener = function ngBindHtmlWatchAction(value) {
      element.html($sce.getTrustedHtml(parsed(scope)) || '');
      //.html is jquery: http://api.jquery.com/html/
      //  Get the HTML contents of the first element in the set of matched elements
      //    or set the HTML contents of every matched element.
    }

    scope.$watch(getStringValue, watchListener);
    //$watch signature: 
    //  $watch(watchExpression, listener, [objectEquality]);
    //  This registers a listener callback to be executed whenever the 
    //    watchExpression()  changes
    //  The listener() has this signature (determined by example, not API documents!):
    //     listener(newValue, oldValue);
    //  The listener is called automagically by Angular when the value changes.
  }

  return DirectiveHandlerFn;
}

var ngBindHtmlDirective = ['$sce', '$parse', DirectiveFnFactory];

OK, now I will first quote the compileTemplate directive, (from here: call function inside $sce.trustAsHtml() string in Angular js):

.directive('compileTemplate', function($compile, $parse){
    return {
        link: function(scope, element, attar){
            var parsed = $parse(attr.ngBindHtml);
            function getStringValue() { return (parsed(scope) || '').toString(); }

            //Recompile if the template changes
            scope.$watch(getStringValue, function() {
                $compile(element, null, -9999)(scope);  
               //The -9999 makes it skip directives so that we 
               //do not recompile ourselves
            });
        }         
    }
});

And now pick it apart to allow commenting:

var DirectiveObjFactory = function($compile, $parse){
    //input: $compile object
    //       $parse object

    var DirectiveHandlerFn = function(scope, element, attr) {
        //Input:  scope = an Angular scope object 
        //        element = the jqLite-wrapped element that this directive matches
        //        attr = a hash object with key-value pairs of normalized attribute 
        //               names and their corresponding attribute values
        //closure scope vars: $compile object
        //                    $parse object
        //Result: none

        var parsed = $parse(attr.ngBindHtml);
        //Input:  attr.ngBindHtml should be the HTML to be bound.
        //Result:  $parse() returns a function which represents the 
        //         compiled input expression.
        //  This resulted function will have this signature: 
        //     function(context, locals)    
        //            context – {object} – an object against which any expressions 
        //                             embedded in the strings are evaluated against 
        //                             (typically a scope object).
        //            locals – {object=} – local variables context object, useful for 
        //                             overriding values in context.
        //     The returned function also has the following properties:
        //       literal – {boolean} – whether the expression's top-level node is a 
        //                         JavaScript literal.
        //       constant – {boolean} – whether the expression is made 
        //                             entirely of JavaScript constant literals.
        //        assign – {?function(context, value)} – if the expr is assignable, 
        //                          this will be set to a function to change its value 
        //                             on the given context.

       function getStringValue() { return (parsed(scope) || '').toString(); }
        //First, this is executing the function that was returned by parsed(), 
        //    passing scope that was given to DirectiveHandlerFn()
        //Next converting Fn() output to string. 
        //In the case of binding HTML. I would think that the output of this 
        //  would be HTML

        var watchListener = function ngBindHtmlWatchAction(value) {
          //Input: value -- actual the newValue. (oldValue not accepted here)
          //Locally scoped vars used -- element, scope

          // -- Adding Below is from ngbindHtml ------------
          element.html($sce.getTrustedHtml(parsed(scope)) || '');
          //.html is jquery: http://api.jquery.com/html/
          //Gets the HTML contents of the first element in the set of matched 
          //    elements or set the HTML contents of every matched element.
          // -- End addition  ------------

          var compFn = $compile(element, null, -9999);
          //NOTE: I can't find formal documentation for the parameters for $compile()
          //      below is from examples found...
          //Input: element -- the HTML element to compile
          //       transcludeFunction -- null here.
          //       maxPriority -- "The -9999 makes it skip directives so that we 
          //                       do not recompile ourselves"
          //$compile() compiles an HTML string or DOM into a template and 
          //    produces a template function, which can then be used to link scope 
          //    and the template together.
          //  The returned function accepts a scope variable, against which the code
          //    is evaluated. 
          compFn(scope); // execute the returned function, passing scope  
        } // end watchListener

        scope.$watch(getStringValue, watchListener);
        //$watch() function signature: 
        //  $watch(watchExpression, listener, [objectEquality]);
        //  This registers a listener callback to be executed whenever the 
        //    watchExpression changes
        //  The supplied listener() should have this signature:
        //     listener(newValue, oldValue);
        //  The listener is called automagically by Angular when the value changes.

    } // end DirectiveHandlerFn

    return {link: DirectiveHandlerFn}         

} // end DirectiveObjFactory

app.directive('compileTemplate', DirectiveObjFactory);

I think I am almost there. Let me try to bring this all back together...

.directive('bindAndWatchHtml', ['$sce', function($compile, $parse){
    return {
        link: function(scope, element, attr){
            var parsed = $parse(attr.ngBindHtml);
            function getStringValue() { return (parsed(scope) || '').toString(); }

            //Recompile if the template changes
            scope.$watch(getStringValue, function() {
                element.html($sce.getTrustedHtml(parsed(scope)) || '');
                $compile(element, null, -9999)(scope);  
                //The -9999 makes it skip directives so that we do not recompile ourselves
            });
        }         
    }
}]);

Hopefully this will bind AND compile the html, and trust the HTML at the same time.

Now for testing...

ADDENDUM:

Doesn't work.. On the line:

element.html($sce.getTrustedHtml(parsed(scope)) || '');

it complains the $sce is not defined.

...

I changed the following line, and that allows $sce to be defined.

app.directive('bindAndWatchTrustedHtml0', ['$compile', '$parse', '$sce',
                                       function($compile, $parse, $sce){ ...

Next I get an error about trying to use a secure text in an insecure location....

....

This is just taking too long. I'm giving up on this. I used this, as per the original link at the very top, and it works.

<span ng-bind-html="item.Data.DisplayText.JSArray[i] | trusted_html" compile-template>
</span>
Community
  • 1
  • 1
kdtop
  • 541
  • 1
  • 7
  • 22
  • what error you are getting in console? – Pankaj Parkar Aug 05 '14 at 02:30
  • @pankajparkar -- no error in the console. Just when I edit the data, the hg-change event handler doesn't fire, and the edits are not stored in the model. Thanks for the reply. – kdtop Aug 05 '14 at 10:47

1 Answers1

1

See https://code.angularjs.org/1.2.19/docs/api/ng/service/$compile for how to compile.

Basically you'd call compiledHtml = $compile('<div>' + text + '</div>')($scope).html().

I think you're going to be better off creating a directive than using a filter though as you'll need the $scope variable to compile.

Maybe you could make use of ngBindTemplate or ngInclude

Simeon Cheeseman
  • 1,748
  • 1
  • 13
  • 24