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

Commit a5fd2e4

Browse files
committed
feat($interpolate): use custom toString() function if present
Except on Numbers, Dates and Arrays. Thanks to @danielkrainas for the initial implementation of this feature. This behavior is consistent with implementations found in other languages such as Ruby, Python, and CoffeeScript. http://rubymonk.com/learning/books/1-ruby-primer/chapters/5-strings/lessons/31-string-basics https://docs.python.org/2/library/stdtypes.html#string-formatting-operations http://coffeescriptcookbook.com/chapters/strings/interpolation The commit also exposes a private $$stringify method on the angular global, so that ngMessageFormat can use the same logic without duplicating it. Fixes #7317 Closes #8350 Fixes #11406 BREAKING CHANGE: When converting values to strings, interpolation now uses a custom toString() function on objects that are not Number, Array or Date (custom means that the `toString` function is not the same as `Object.prototype.toString`). Otherwise, interpolation uses JSON.stringify() as usual. Should you have a custom toString() function but still want the output of JSON.stringify(), migrate as shown in the following examples: Before: ```html <span>{{myObject}}</span> ``` After - use the `json` filter to stringify the object: ```html <span>{{myObject | json}}</span> ```
1 parent 3cda897 commit a5fd2e4

File tree

9 files changed

+85
-29
lines changed

9 files changed

+85
-29
lines changed

docs/content/guide/interpolation.ngdoc

+9
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ normal {@link ng.$rootScope.Scope#$digest digest} cycle.
2626

2727
Note that the interpolateDirective has a priority of 100 and sets up the watch in the preLink function.
2828

29+
### How the string representation is computed
30+
31+
If the interpolated value is not a `String`, it is computed as follows:
32+
- `undefined` and `null` are converted to `''`
33+
- if the value is an object that is not a `Number`, `Date` or `Array`, $interpolate looks for
34+
a custom `toString()` function on the object, and uses that. Custom means that
35+
`myObject.toString !== `Object.prototype.toString`.
36+
- if the above doesn't apply, `JSON.stringify` is used.
37+
2938
### Binding to boolean attributes
3039

3140
Attributes such as `disabled` are called `boolean` attributes, because their presence means `true` and

src/.jshintrc

+1
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@
9696
"createMap": false,
9797
"VALIDITY_STATE_PROPERTY": false,
9898
"reloadWithDebugInfo": false,
99+
"stringify": false,
99100

100101
"NODE_TYPE_ELEMENT": false,
101102
"NODE_TYPE_ATTRIBUTE": false,

src/Angular.js

+22
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
getBlockNodes: true,
8787
hasOwnProperty: true,
8888
createMap: true,
89+
stringify: true,
8990
9091
NODE_TYPE_ELEMENT: true,
9192
NODE_TYPE_ATTRIBUTE: true,
@@ -1903,6 +1904,27 @@ function createMap() {
19031904
return Object.create(null);
19041905
}
19051906

1907+
function stringify(value) {
1908+
if (value == null) { // null || undefined
1909+
return '';
1910+
}
1911+
switch (typeof value) {
1912+
case 'string':
1913+
break;
1914+
case 'number':
1915+
value = '' + value;
1916+
break;
1917+
default:
1918+
if (hasCustomToString(value) && !isArray(value) && !isDate(value)) {
1919+
value = value.toString();
1920+
} else {
1921+
value = toJson(value);
1922+
}
1923+
}
1924+
1925+
return value;
1926+
}
1927+
19061928
var NODE_TYPE_ELEMENT = 1;
19071929
var NODE_TYPE_ATTRIBUTE = 2;
19081930
var NODE_TYPE_TEXT = 3;

src/AngularPublic.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,8 @@ function publishExternalAPI(angular) {
156156
'$$minErr': minErr,
157157
'$$csp': csp,
158158
'$$encodeUriSegment': encodeUriSegment,
159-
'$$encodeUriQuery': encodeUriQuery
159+
'$$encodeUriQuery': encodeUriQuery,
160+
'$$stringify': stringify
160161
});
161162

162163
angularModule = setupModuleLoader(window);

src/ng/interpolate.js

-17
Original file line numberDiff line numberDiff line change
@@ -111,23 +111,6 @@ function $InterpolateProvider() {
111111
replace(escapedEndRegexp, endSymbol);
112112
}
113113

114-
function stringify(value) {
115-
if (value == null) { // null || undefined
116-
return '';
117-
}
118-
switch (typeof value) {
119-
case 'string':
120-
break;
121-
case 'number':
122-
value = '' + value;
123-
break;
124-
default:
125-
value = toJson(value);
126-
}
127-
128-
return value;
129-
}
130-
131114
//TODO: this is the same as the constantWatchDelegate in parse.js
132115
function constantWatchDelegate(scope, listener, objectEquality, constantInterp) {
133116
var unwatch;

src/ngMessageFormat/messageFormatCommon.js

+1-9
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,7 @@
88
/* global isFunction: false */
99
/* global noop: false */
1010
/* global toJson: false */
11-
12-
function stringify(value) {
13-
if (value == null /* null/undefined */) { return ''; }
14-
switch (typeof value) {
15-
case 'string': return value;
16-
case 'number': return '' + value;
17-
default: return toJson(value);
18-
}
19-
}
11+
/* global $$stringify: false */
2012

2113
// Convert an index into the string into line/column for use in error messages
2214
// As such, this doesn't have to be efficient.

src/ngMessageFormat/messageFormatService.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
/* global noop: true */
1111
/* global toJson: true */
1212
/* global MessageFormatParser: false */
13-
/* global stringify: false */
1413

1514
/**
1615
* @ngdoc module
@@ -180,7 +179,7 @@ var $$MessageFormatFactory = ['$parse', '$locale', '$sce', '$exceptionHandler',
180179
return function stringifier(value) {
181180
try {
182181
value = trustedContext ? $sce['getTrusted'](trustedContext, value) : $sce['valueOf'](value);
183-
return allOrNothing && (value === void 0) ? value : stringify(value);
182+
return allOrNothing && (value === void 0) ? value : $$stringify(value);
184183
} catch (err) {
185184
$exceptionHandler($interpolateMinErr['interr'](text, err));
186185
}
@@ -214,6 +213,7 @@ var $interpolateMinErr;
214213
var isFunction;
215214
var noop;
216215
var toJson;
216+
var $$stringify;
217217

218218
var module = window['angular']['module']('ngMessageFormat', ['ng']);
219219
module['factory']('$$messageFormat', $$MessageFormatFactory);
@@ -222,6 +222,7 @@ module['config'](['$provide', function($provide) {
222222
isFunction = window['angular']['isFunction'];
223223
noop = window['angular']['noop'];
224224
toJson = window['angular']['toJson'];
225+
$$stringify = window['angular']['$$stringify'];
225226

226227
$provide['decorator']('$interpolate', $$interpolateDecorator);
227228
}]);

test/ng/interpolateSpec.js

+23
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,29 @@ describe('$interpolate', function() {
3535
expect($interpolate('{{ false }}')({})).toEqual('false');
3636
}));
3737

38+
it('should use custom toString when present', inject(function($interpolate, $rootScope) {
39+
var context = {
40+
a: {
41+
toString: function() {
42+
return 'foo';
43+
}
44+
}
45+
};
46+
47+
expect($interpolate('{{ a }}')(context)).toEqual('foo');
48+
}));
49+
50+
it('should NOT use toString on array objects', inject(function($interpolate) {
51+
expect($interpolate('{{a}}')({ a: [] })).toEqual('[]');
52+
}));
53+
54+
55+
it('should NOT use toString on Date objects', inject(function($interpolate) {
56+
var date = new Date(2014, 10, 10);
57+
expect($interpolate('{{a}}')({ a: date })).toBe(JSON.stringify(date));
58+
expect($interpolate('{{a}}')({ a: date })).not.toEqual(date.toString());
59+
}));
60+
3861

3962
it('should return interpolation function', inject(function($interpolate, $rootScope) {
4063
var interpolateFn = $interpolate('Hello {{name}}!');

test/ngMessageFormat/messageFormatSpec.js

+24
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,30 @@ describe('$$ngMessageFormat', function() {
311311
}));
312312

313313

314+
it('should use custom toString when present', inject(function($interpolate, $rootScope) {
315+
var context = {
316+
a: {
317+
toString: function() {
318+
return 'foo';
319+
}
320+
}
321+
};
322+
323+
expect($interpolate('{{ a }}')(context)).toEqual('foo');
324+
}));
325+
326+
it('should NOT use toString on array objects', inject(function($interpolate) {
327+
expect($interpolate('{{a}}')({ a: [] })).toEqual('[]');
328+
}));
329+
330+
331+
it('should NOT use toString on Date objects', inject(function($interpolate) {
332+
var date = new Date(2014, 10, 10);
333+
expect($interpolate('{{a}}')({ a: date })).toBe(JSON.stringify(date));
334+
expect($interpolate('{{a}}')({ a: date })).not.toEqual(date.toString());
335+
}));
336+
337+
314338
it('should return interpolation function', inject(function($interpolate, $rootScope) {
315339
var interpolateFn = $interpolate('Hello {{name}}!');
316340

0 commit comments

Comments
 (0)