diff --git a/src/ng/directive/ngBind.js b/src/ng/directive/ngBind.js index 3c7c65d4cb56..807f6ab7b3c5 100644 --- a/src/ng/directive/ngBind.js +++ b/src/ng/directive/ngBind.js @@ -188,8 +188,9 @@ var ngBindHtmlDirective = ['$sce', '$parse', '$compile', function($sce, $parse, restrict: 'A', compile: function ngBindHtmlCompile(tElement, tAttrs) { var ngBindHtmlGetter = $parse(tAttrs.ngBindHtml); - var ngBindHtmlWatch = $parse(tAttrs.ngBindHtml, function getStringValue(value) { - return (value || '').toString(); + var ngBindHtmlWatch = $parse(tAttrs.ngBindHtml, function sceValueOf(val) { + // Unwrap the value to compare the actual inner safe value, not the wrapper object. + return $sce.valueOf(val); }); $compile.$$addBindingClass(tElement); @@ -197,9 +198,9 @@ var ngBindHtmlDirective = ['$sce', '$parse', '$compile', function($sce, $parse, $compile.$$addBindingInfo(element, attr.ngBindHtml); scope.$watch(ngBindHtmlWatch, function ngBindHtmlWatchAction() { - // we re-evaluate the expr because we want a TrustedValueHolderType - // for $sce, not a string - element.html($sce.getTrustedHtml(ngBindHtmlGetter(scope)) || ''); + // The watched value is the unwrapped value. To avoid re-escaping, use the direct getter. + var value = ngBindHtmlGetter(scope); + element.html($sce.getTrustedHtml(value) || ''); }); }; } diff --git a/test/ng/directive/ngBindSpec.js b/test/ng/directive/ngBindSpec.js index c03afa9123bb..39a35c15723a 100644 --- a/test/ng/directive/ngBindSpec.js +++ b/test/ng/directive/ngBindSpec.js @@ -142,6 +142,16 @@ describe('ngBind*', function() { expect(angular.lowercase(element.html())).toEqual('
hello
'); })); + it('should update html', inject(function($rootScope, $compile, $sce) { + element = $compile('
')($rootScope); + $rootScope.html = 'hello'; + $rootScope.$digest(); + expect(angular.lowercase(element.html())).toEqual('hello'); + $rootScope.html = 'goodbye'; + $rootScope.$digest(); + expect(angular.lowercase(element.html())).toEqual('goodbye'); + })); + it('should one-time bind if the expression starts with two colons', inject(function($rootScope, $compile) { element = $compile('
')($rootScope); $rootScope.html = '
hello
'; @@ -176,7 +186,18 @@ describe('ngBind*', function() { expect(angular.lowercase(element.html())).toEqual('
hello
'); })); - it('should watch the string value to avoid infinite recursion', inject(function($rootScope, $compile, $sce) { + it('should update html', inject(function($rootScope, $compile, $sce) { + element = $compile('
')($rootScope); + $rootScope.html = $sce.trustAsHtml('hello'); + $rootScope.$digest(); + expect(angular.lowercase(element.html())).toEqual('hello'); + $rootScope.html = $sce.trustAsHtml('goodbye'); + $rootScope.$digest(); + expect(angular.lowercase(element.html())).toEqual('goodbye'); + })); + + it('should not cause infinite recursion for trustAsHtml object watches', + inject(function($rootScope, $compile, $sce) { // Ref: https://github.com/angular/angular.js/issues/3932 // If the binding is a function that creates a new value on every call via trustAs, we'll // trigger an infinite digest if we don't take care of it. @@ -188,6 +209,33 @@ describe('ngBind*', function() { expect(angular.lowercase(element.html())).toEqual('
hello
'); })); + it('should handle custom $sce objects', function() { + function MySafeHtml(val) { this.val = val; } + + module(function($provide) { + $provide.decorator('$sce', function($delegate) { + $delegate.trustAsHtml = function(html) { return new MySafeHtml(html); }; + $delegate.getTrustedHtml = function(mySafeHtml) { return mySafeHtml.val; }; + $delegate.valueOf = function(v) { return v instanceof MySafeHtml ? v.val : v; }; + return $delegate; + }); + }); + + inject(function($rootScope, $compile, $sce) { + // Ref: https://github.com/angular/angular.js/issues/14526 + // Previous code used toString for change detection, which fails for custom objects + // that don't override toString. + element = $compile('
')($rootScope); + var html = 'hello'; + $rootScope.getHtml = function() { return $sce.trustAsHtml(html); }; + $rootScope.$digest(); + expect(angular.lowercase(element.html())).toEqual('hello'); + html = 'goodbye'; + $rootScope.$digest(); + expect(angular.lowercase(element.html())).toEqual('goodbye'); + }); + }); + describe('when $sanitize is available', function() { beforeEach(function() { module('ngSanitize'); });