diff --git a/src/Angular.js b/src/Angular.js
index 21b3ef070eef..37beb5026a60 100644
--- a/src/Angular.js
+++ b/src/Angular.js
@@ -740,6 +740,33 @@ function fromJson(json) {
: json;
}
+function serialize(obj, prefix) {
+ var str = [];
+ for(var p in obj) {
+ var k = prefix ? prefix + "[" + p + "]" : p, v = obj[p];
+ str.push(typeof v == "object" ?
+ serialize(v, k) :
+ encodeURIComponent(k) + "=" + encodeURIComponent(v));
+ }
+ return str.join("&");
+}
+
+/**
+ * @ngdoc function
+ * @name angular.toUrlEncodedString
+ * @function
+ *
+ * @description
+ * URL encodes a string, following jQuery's param function,
+ * but using a pure JavaScript solution from
+ * http://stackoverflow.com/questions/1714786/querystring-encoding-of-a-javascript-object
+ *
+ * @param {Object} object Object to serialize into a url encoded string
+ * @returns {string} A url encoded string
+ */
+function toUrlEncodedString(object) {
+ return (isString(object) && object) || serialize(object);
+}
function toBoolean(value) {
if (value && value.length !== 0) {
diff --git a/src/jqLite.js b/src/jqLite.js
index 46e0a73c3c54..f84e7d1fe896 100644
--- a/src/jqLite.js
+++ b/src/jqLite.js
@@ -353,11 +353,11 @@ var JQLitePrototype = JQLite.prototype = {
// value on get.
//////////////////////////////////////////
var BOOLEAN_ATTR = {};
-forEach('multiple,selected,checked,disabled,readOnly,required'.split(','), function(value) {
+forEach('multiple,selected,checked,disabled,readOnly,required,open'.split(','), function(value) {
BOOLEAN_ATTR[lowercase(value)] = value;
});
var BOOLEAN_ELEMENTS = {};
-forEach('input,select,option,textarea,button,form'.split(','), function(value) {
+forEach('input,select,option,textarea,button,form,details'.split(','), function(value) {
BOOLEAN_ELEMENTS[uppercase(value)] = true;
});
diff --git a/src/ng/directive/booleanAttrs.js b/src/ng/directive/booleanAttrs.js
index 8902c35d2d4b..864c12c118d9 100644
--- a/src/ng/directive/booleanAttrs.js
+++ b/src/ng/directive/booleanAttrs.js
@@ -272,6 +272,37 @@
* @param {string} expression Angular expression that will be evaluated.
*/
+/**
+ * @ngdoc directive
+ * @name ng.directive:ngOpen
+ * @restrict A
+ *
+ * @description
+ * The HTML specs do not require browsers to preserve the special attributes such as open.
+ * (The presence of them means true and absence means false)
+ * This prevents the angular compiler from correctly retrieving the binding expression.
+ * To solve this problem, we introduce the `ngMultiple` directive.
+ *
+ * @example
+
+
+ Check me check multiple:
+
+ Show/Hide me
+
+
+
+ it('should toggle open', function() {
+ expect(element('#details').prop('open')).toBeFalsy();
+ input('open').check();
+ expect(element('#details').prop('open')).toBeTruthy();
+ });
+
+
+ *
+ * @element DETAILS
+ * @param {string} expression Angular expression that will be evaluated.
+ */
var ngAttributeAliasDirectives = {};
diff --git a/src/ng/http.js b/src/ng/http.js
index ed9e6712c89b..2778835d132c 100644
--- a/src/ng/http.js
+++ b/src/ng/http.js
@@ -142,13 +142,19 @@ function $HttpProvider() {
return isObject(d) && !isFile(d) ? toJson(d) : d;
}],
+ // transform outgoing request data by Url Encoding
+ transformRequestByUrlEncode: [function(d) {
+ return isObject(d) && !isFile(d) ? toUrlEncodedString(d) : d;
+ }],
+
// default headers
headers: {
common: {
'Accept': 'application/json, text/plain, */*'
},
post: {'Content-Type': 'application/json;charset=utf-8'},
- put: {'Content-Type': 'application/json;charset=utf-8'}
+ put: {'Content-Type': 'application/json;charset=utf-8'},
+ patch: {'Content-Type': 'application/json;charset=utf-8'}
}
};
@@ -416,11 +422,13 @@ function $HttpProvider() {
* {@link ng.$cacheFactory $cacheFactory}, this cache will be used for
* caching.
* - **timeout** – `{number}` – timeout in milliseconds.
- * - **withCredentials** - `{boolean}` - whether to to set the `withCredentials` flag on the
+ * - **withCredentials** - `{boolean}` - whether or not to set the `withCredentials` flag on the
* XHR object. See {@link https://developer.mozilla.org/en/http_access_control#section_5
* requests with credentials} for more information.
* - **responseType** - `{string}` - see {@link
* https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType requestType}.
+ * - **urlEncodeRequestData** - `{boolean}` - whether or not to URL encode request data. Default
+ * behavior is to JSON stringify the request data
*
* @returns {HttpPromise} Returns a {@link ng.$q promise} object with the
* standard `then` method and two http specific methods: `success` and `error`. The `then`
@@ -513,7 +521,7 @@ function $HttpProvider() {
function $http(config) {
config.method = uppercase(config.method);
- var reqTransformFn = config.transformRequest || defaults.transformRequest,
+ var reqTransformFn = config.transformRequest || ((config.urlEncodeRequestData || defaults.urlEncodeRequestData) && defaults.transformRequestByUrlEncode) || defaults.transformRequest,
respTransformFn = config.transformResponse || defaults.transformResponse,
defHeaders = defaults.headers,
xsrfToken = isSameDomain(config.url, $browser.url()) ?
@@ -523,6 +531,10 @@ function $HttpProvider() {
reqData = transformData(config.data, headersGetter(reqHeaders), reqTransformFn),
promise;
+ if(config.urlEncodeRequestData || defaults.urlEncodeRequestData) {
+ reqHeaders['Content-Type'] = 'application/x-www-form-urlencoded;charset=utf-8';
+ }
+
// strip content-type if data is undefined
if (isUndefined(config.data)) {
delete reqHeaders['Content-Type'];
@@ -654,7 +666,7 @@ function $HttpProvider() {
* @param {Object=} config Optional configuration object
* @returns {HttpPromise} Future object
*/
- createShortMethodsWithData('post', 'put');
+ createShortMethodsWithData('post', 'put', 'patch');
/**
* @ngdoc property
diff --git a/test/AngularSpec.js b/test/AngularSpec.js
index f5638b9c5288..9ce726a3a56a 100644
--- a/test/AngularSpec.js
+++ b/test/AngularSpec.js
@@ -640,4 +640,14 @@ describe('angular', function() {
expect(toJson({key: $rootScope})).toEqual('{"key":"$SCOPE"}');
}));
});
+
+ describe('toUrlEncodedString', function() {
+
+ it('should encode objects properly', function() {
+ expect(toUrlEncodedString({ })).toEqual('');
+ expect(toUrlEncodedString({ one: "one", two: 2 })).toEqual('one=one&two=2');
+ expect(toUrlEncodedString({ a:1, b:{ c:3, d:2 } })).toEqual('a=1&b%5Bc%5D=3&b%5Bd%5D=2');
+ expect(toUrlEncodedString({ a:1, b:{ c:3, d:[1,2,3] } })).toEqual('a=1&b%5Bc%5D=3&b%5Bd%5D%5B0%5D=1&b%5Bd%5D%5B1%5D=2&b%5Bd%5D%5B2%5D=3');
+ });
+ });
});
diff --git a/test/ng/directive/booleanAttrsSpec.js b/test/ng/directive/booleanAttrsSpec.js
index 435ffcb9727f..0ce6b555108e 100644
--- a/test/ng/directive/booleanAttrsSpec.js
+++ b/test/ng/directive/booleanAttrsSpec.js
@@ -74,6 +74,16 @@ describe('boolean attr directives', function() {
$rootScope.$digest();
expect(element.attr('multiple')).toBeTruthy();
}));
+
+ it('should bind open', inject(function($rootScope, $compile) {
+ element = $compile(' ')($rootScope)
+ $rootScope.isOpen=false;
+ $rootScope.$digest();
+ expect(element.attr('open')).toBeFalsy();
+ $rootScope.isOpen=true;
+ $rootScope.$digest();
+ expect(element.attr('open')).toBeTruthy();
+ }));
});
diff --git a/test/ng/httpSpec.js b/test/ng/httpSpec.js
index 1473ab1ccc3f..e42cc208e675 100644
--- a/test/ng/httpSpec.js
+++ b/test/ng/httpSpec.js
@@ -406,6 +406,15 @@ describe('$http', function() {
$httpBackend.flush();
});
+ it('should set default headers for PATCH request', function() {
+ $httpBackend.expect('PATCH', '/url', 'messageBody', function(headers) {
+ return headers['Accept'] == 'application/json, text/plain, */*' &&
+ headers['Content-Type'] == 'application/json;charset=utf-8';
+ }).respond('');
+
+ $http({url: '/url', method: 'PATCH', headers: {}, data: 'messageBody'});
+ $httpBackend.flush();
+ });
it('should set default headers for custom HTTP method', function() {
$httpBackend.expect('FOO', '/url', undefined, function(headers) {
@@ -430,6 +439,33 @@ describe('$http', function() {
$httpBackend.flush();
});
+ it('should change content-type header to urlencoded if specified in config', function() {
+ $httpBackend.expect('POST', '/url', 'messageBody', function(headers) {
+ return headers['Content-Type'] == 'application/x-www-form-urlencoded;charset=utf-8';
+ }).respond('');
+
+ $http({url: '/url', method: 'POST', data: 'messageBody', urlEncodeRequestData: true });
+ $httpBackend.flush();
+ });
+
+ it('should automatically JSON encode request data if not specified in config', function() {
+ $httpBackend.expect('POST', '/url', '{"one":"one","two":2}', function(headers) {
+ return headers['Content-Type'] == 'application/json;charset=utf-8';
+ }).respond('');
+
+ $http({url: '/url', method: 'POST', data: { one: 'one', two: 2 } });
+ $httpBackend.flush();
+ });
+
+ it('should URL encode request data if specified in config', function() {
+ $httpBackend.expect('POST', '/url', 'one=one&two=2', function(headers) {
+ return headers['Content-Type'] == 'application/x-www-form-urlencoded;charset=utf-8';
+ }).respond('');
+
+ $http({url: '/url', method: 'POST', data: { one: 'one', two: 2 }, urlEncodeRequestData: true });
+ $httpBackend.flush();
+ });
+
it('should not set XSRF cookie for cross-domain requests', inject(function($browser) {
$browser.cookies('XSRF-TOKEN', 'secret');
$browser.url('http://host.com/base');
@@ -464,11 +500,13 @@ describe('$http', function() {
$httpBackend.expect('POST', '/url', undefined, checkXSRF('secret')).respond('');
$httpBackend.expect('PUT', '/url', undefined, checkXSRF('secret')).respond('');
$httpBackend.expect('DELETE', '/url', undefined, checkXSRF('secret')).respond('');
+ $httpBackend.expect('PATCH', '/url', undefined, checkXSRF('secret')).respond('');
$http({url: '/url', method: 'GET'});
$http({url: '/url', method: 'POST', headers: {'S-ome': 'Header'}});
$http({url: '/url', method: 'PUT', headers: {'Another': 'Header'}});
$http({url: '/url', method: 'DELETE', headers: {}});
+ $http({url: '/url', method: 'PATCH'});
$httpBackend.flush();
}));
@@ -553,6 +591,17 @@ describe('$http', function() {
$httpBackend.expect('JSONP', '/url', undefined, checkHeader('Custom', 'Header')).respond('');
$http.jsonp('/url', {headers: {'Custom': 'Header'}});
});
+
+ it('should have patch()', function() {
+ $httpBackend.expect('PATCH', '/url').respond('');
+ $http.patch('/url');
+ });
+
+
+ it('patch() should allow config param', function() {
+ $httpBackend.expect('PATCH', '/url', 'some-data', checkHeader('Custom', 'Header')).respond('');
+ $http.patch('/url', 'some-data', {headers: {'Custom': 'Header'}});
+ });
});