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

Adding PATCH HTTP method and urlencode request data #1926

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/Angular.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions src/jqLite.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});

Expand Down
31 changes: 31 additions & 0 deletions src/ng/directive/booleanAttrs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
<doc:example>
<doc:source>
Check me check multiple: <input type="checkbox" ng-model="open"><br/>
<details id="details" ng-open="open">
<summary>Show/Hide me</summary>
</details>
</doc:source>
<doc:scenario>
it('should toggle open', function() {
expect(element('#details').prop('open')).toBeFalsy();
input('open').check();
expect(element('#details').prop('open')).toBeTruthy();
});
</doc:scenario>
</doc:example>
*
* @element DETAILS
* @param {string} expression Angular expression that will be evaluated.
*/

var ngAttributeAliasDirectives = {};

Expand Down
20 changes: 16 additions & 4 deletions src/ng/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'}
}
};

Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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()) ?
Expand All @@ -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'];
Expand Down Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions test/AngularSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
});
10 changes: 10 additions & 0 deletions test/ng/directive/booleanAttrsSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('<details ng-open="isOpen"></details>')($rootScope)
$rootScope.isOpen=false;
$rootScope.$digest();
expect(element.attr('open')).toBeFalsy();
$rootScope.isOpen=true;
$rootScope.$digest();
expect(element.attr('open')).toBeTruthy();
}));
});


Expand Down
49 changes: 49 additions & 0 deletions test/ng/httpSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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');
Expand Down Expand Up @@ -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();
}));
Expand Down Expand Up @@ -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'}});
});
});


Expand Down