Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 1c1c9b2

Browse files
mprobstgkalpak
authored andcommitted
fix(ng-bind-html): watch the unwrapped value using $sce.valueOf() (instead of toString())
Custom `$sce` implementations might not provide a `toString()` method on the wrapped object, or it might be compiled away in non-debug mode. Watching the unwrapped value (retrieved using `$sce.valueOf()`) fixes the problem. The performance of this should be equivalent - `toString()` on objects usually touches all fields, plus we will also avoid the (potentially big) string allocation. Fixes #14526 Closes #14527
1 parent c5e7a5b commit 1c1c9b2

File tree

2 files changed

+55
-6
lines changed

2 files changed

+55
-6
lines changed

src/ng/directive/ngBind.js

+6-5
Original file line numberDiff line numberDiff line change
@@ -188,18 +188,19 @@ var ngBindHtmlDirective = ['$sce', '$parse', '$compile', function($sce, $parse,
188188
restrict: 'A',
189189
compile: function ngBindHtmlCompile(tElement, tAttrs) {
190190
var ngBindHtmlGetter = $parse(tAttrs.ngBindHtml);
191-
var ngBindHtmlWatch = $parse(tAttrs.ngBindHtml, function getStringValue(value) {
192-
return (value || '').toString();
191+
var ngBindHtmlWatch = $parse(tAttrs.ngBindHtml, function sceValueOf(val) {
192+
// Unwrap the value to compare the actual inner safe value, not the wrapper object.
193+
return $sce.valueOf(val);
193194
});
194195
$compile.$$addBindingClass(tElement);
195196

196197
return function ngBindHtmlLink(scope, element, attr) {
197198
$compile.$$addBindingInfo(element, attr.ngBindHtml);
198199

199200
scope.$watch(ngBindHtmlWatch, function ngBindHtmlWatchAction() {
200-
// we re-evaluate the expr because we want a TrustedValueHolderType
201-
// for $sce, not a string
202-
element.html($sce.getTrustedHtml(ngBindHtmlGetter(scope)) || '');
201+
// The watched value is the unwrapped value. To avoid re-escaping, use the direct getter.
202+
var value = ngBindHtmlGetter(scope);
203+
element.html($sce.getTrustedHtml(value) || '');
203204
});
204205
};
205206
}

test/ng/directive/ngBindSpec.js

+49-1
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,16 @@ describe('ngBind*', function() {
142142
expect(angular.lowercase(element.html())).toEqual('<div onclick="">hello</div>');
143143
}));
144144

145+
it('should update html', inject(function($rootScope, $compile, $sce) {
146+
element = $compile('<div ng-bind-html="html"></div>')($rootScope);
147+
$rootScope.html = 'hello';
148+
$rootScope.$digest();
149+
expect(angular.lowercase(element.html())).toEqual('hello');
150+
$rootScope.html = 'goodbye';
151+
$rootScope.$digest();
152+
expect(angular.lowercase(element.html())).toEqual('goodbye');
153+
}));
154+
145155
it('should one-time bind if the expression starts with two colons', inject(function($rootScope, $compile) {
146156
element = $compile('<div ng-bind-html="::html"></div>')($rootScope);
147157
$rootScope.html = '<div onclick="">hello</div>';
@@ -176,7 +186,18 @@ describe('ngBind*', function() {
176186
expect(angular.lowercase(element.html())).toEqual('<div onclick="">hello</div>');
177187
}));
178188

179-
it('should watch the string value to avoid infinite recursion', inject(function($rootScope, $compile, $sce) {
189+
it('should update html', inject(function($rootScope, $compile, $sce) {
190+
element = $compile('<div ng-bind-html="html"></div>')($rootScope);
191+
$rootScope.html = $sce.trustAsHtml('hello');
192+
$rootScope.$digest();
193+
expect(angular.lowercase(element.html())).toEqual('hello');
194+
$rootScope.html = $sce.trustAsHtml('goodbye');
195+
$rootScope.$digest();
196+
expect(angular.lowercase(element.html())).toEqual('goodbye');
197+
}));
198+
199+
it('should not cause infinite recursion for trustAsHtml object watches',
200+
inject(function($rootScope, $compile, $sce) {
180201
// Ref: https://github.com/angular/angular.js/issues/3932
181202
// If the binding is a function that creates a new value on every call via trustAs, we'll
182203
// trigger an infinite digest if we don't take care of it.
@@ -188,6 +209,33 @@ describe('ngBind*', function() {
188209
expect(angular.lowercase(element.html())).toEqual('<div onclick="">hello</div>');
189210
}));
190211

212+
it('should handle custom $sce objects', function() {
213+
function MySafeHtml(val) { this.val = val; }
214+
215+
module(function($provide) {
216+
$provide.decorator('$sce', function($delegate) {
217+
$delegate.trustAsHtml = function(html) { return new MySafeHtml(html); };
218+
$delegate.getTrustedHtml = function(mySafeHtml) { return mySafeHtml.val; };
219+
$delegate.valueOf = function(v) { return v instanceof MySafeHtml ? v.val : v; };
220+
return $delegate;
221+
});
222+
});
223+
224+
inject(function($rootScope, $compile, $sce) {
225+
// Ref: https://github.com/angular/angular.js/issues/14526
226+
// Previous code used toString for change detection, which fails for custom objects
227+
// that don't override toString.
228+
element = $compile('<div ng-bind-html="getHtml()"></div>')($rootScope);
229+
var html = 'hello';
230+
$rootScope.getHtml = function() { return $sce.trustAsHtml(html); };
231+
$rootScope.$digest();
232+
expect(angular.lowercase(element.html())).toEqual('hello');
233+
html = 'goodbye';
234+
$rootScope.$digest();
235+
expect(angular.lowercase(element.html())).toEqual('goodbye');
236+
});
237+
});
238+
191239
describe('when $sanitize is available', function() {
192240
beforeEach(function() { module('ngSanitize'); });
193241

0 commit comments

Comments
 (0)