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'}}); + }); });