}` –
133 | * transform function or an array of such functions. The transform function takes the http
134 | * response body and headers and returns its transformed (typically deserialized) version.
135 | * - **`cache`** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the
136 | * GET request, otherwise if a cache instance built with
137 | * {@link ng.$cacheFactory $cacheFactory}, this cache will be used for
138 | * caching.
139 | * - **`timeout`** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise} that
140 | * should abort the request when resolved.
141 | * - **`withCredentials`** - `{boolean}` - whether to set the `withCredentials` flag on the
142 | * XHR object. See {@link https://developer.mozilla.org/en/http_access_control#section_5
143 | * requests with credentials} for more information.
144 | * - **`responseType`** - `{string}` - see {@link
145 | * https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType requestType}.
146 | * - **`interceptor`** - `{Object=}` - The interceptor object has two optional methods -
147 | * `response` and `responseError`. Both `response` and `responseError` interceptors get called
148 | * with `http response` object. See {@link ng.$http $http interceptors}.
149 | *
150 | * @returns {Object} A resource "class" object with methods for the default set of resource actions
151 | * optionally extended with custom `actions`. The default set contains these actions:
152 | *
153 | * { 'get': {method:'GET'},
154 | * 'save': {method:'POST'},
155 | * 'query': {method:'GET', isArray:true},
156 | * 'remove': {method:'DELETE'},
157 | * 'delete': {method:'DELETE'} };
158 | *
159 | * Calling these methods invoke an {@link ng.$http} with the specified http method,
160 | * destination and parameters. When the data is returned from the server then the object is an
161 | * instance of the resource class. The actions `save`, `remove` and `delete` are available on it
162 | * as methods with the `$` prefix. This allows you to easily perform CRUD operations (create,
163 | * read, update, delete) on server-side data like this:
164 | *
165 | var User = $resource('/user/:userId', {userId:'@id'});
166 | var user = User.get({userId:123}, function() {
167 | user.abc = true;
168 | user.$save();
169 | });
170 |
171 | *
172 | * It is important to realize that invoking a $resource object method immediately returns an
173 | * empty reference (object or array depending on `isArray`). Once the data is returned from the
174 | * server the existing reference is populated with the actual data. This is a useful trick since
175 | * usually the resource is assigned to a model which is then rendered by the view. Having an empty
176 | * object results in no rendering, once the data arrives from the server then the object is
177 | * populated with the data and the view automatically re-renders itself showing the new data. This
178 | * means that in most cases one never has to write a callback function for the action methods.
179 | *
180 | * The action methods on the class object or instance object can be invoked with the following
181 | * parameters:
182 | *
183 | * - HTTP GET "class" actions: `Resource.action([parameters], [success], [error])`
184 | * - non-GET "class" actions: `Resource.action([parameters], postData, [success], [error])`
185 | * - non-GET instance actions: `instance.$action([parameters], [success], [error])`
186 | *
187 | * Success callback is called with (value, responseHeaders) arguments. Error callback is called
188 | * with (httpResponse) argument.
189 | *
190 | * Class actions return empty instance (with additional properties below).
191 | * Instance actions return promise of the action.
192 | *
193 | * The Resource instances and collection have these additional properties:
194 | *
195 | * - `$promise`: the {@link ng.$q promise} of the original server interaction that created this
196 | * instance or collection.
197 | *
198 | * On success, the promise is resolved with the same resource instance or collection object,
199 | * updated with data from server. This makes it easy to use in
200 | * {@link ngRoute.$routeProvider resolve section of $routeProvider.when()} to defer view
201 | * rendering until the resource(s) are loaded.
202 | *
203 | * On failure, the promise is resolved with the {@link ng.$http http response} object, without
204 | * the `resource` property.
205 | *
206 | * - `$resolved`: `true` after first server interaction is completed (either with success or
207 | * rejection), `false` before that. Knowing if the Resource has been resolved is useful in
208 | * data-binding.
209 | *
210 | * @example
211 | *
212 | * # Credit card resource
213 | *
214 | *
215 | // Define CreditCard class
216 | var CreditCard = $resource('/user/:userId/card/:cardId',
217 | {userId:123, cardId:'@id'}, {
218 | charge: {method:'POST', params:{charge:true}}
219 | });
220 |
221 | // We can retrieve a collection from the server
222 | var cards = CreditCard.query(function() {
223 | // GET: /user/123/card
224 | // server returns: [ {id:456, number:'1234', name:'Smith'} ];
225 |
226 | var card = cards[0];
227 | // each item is an instance of CreditCard
228 | expect(card instanceof CreditCard).toEqual(true);
229 | card.name = "J. Smith";
230 | // non GET methods are mapped onto the instances
231 | card.$save();
232 | // POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'}
233 | // server returns: {id:456, number:'1234', name: 'J. Smith'};
234 |
235 | // our custom method is mapped as well.
236 | card.$charge({amount:9.99});
237 | // POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'}
238 | });
239 |
240 | // we can create an instance as well
241 | var newCard = new CreditCard({number:'0123'});
242 | newCard.name = "Mike Smith";
243 | newCard.$save();
244 | // POST: /user/123/card {number:'0123', name:'Mike Smith'}
245 | // server returns: {id:789, number:'0123', name: 'Mike Smith'};
246 | expect(newCard.id).toEqual(789);
247 | *
248 | *
249 | * The object returned from this function execution is a resource "class" which has "static" method
250 | * for each action in the definition.
251 | *
252 | * Calling these methods invoke `$http` on the `url` template with the given `method`, `params` and
253 | * `headers`.
254 | * When the data is returned from the server then the object is an instance of the resource type and
255 | * all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD
256 | * operations (create, read, update, delete) on server-side data.
257 |
258 |
259 | var User = $resource('/user/:userId', {userId:'@id'});
260 | var user = User.get({userId:123}, function() {
261 | user.abc = true;
262 | user.$save();
263 | });
264 |
265 | *
266 | * It's worth noting that the success callback for `get`, `query` and other methods gets passed
267 | * in the response that came from the server as well as $http header getter function, so one
268 | * could rewrite the above example and get access to http headers as:
269 | *
270 |
271 | var User = $resource('/user/:userId', {userId:'@id'});
272 | User.get({userId:123}, function(u, getResponseHeaders){
273 | u.abc = true;
274 | u.$save(function(u, putResponseHeaders) {
275 | //u => saved user object
276 | //putResponseHeaders => $http header getter
277 | });
278 | });
279 |
280 |
281 | * # Creating a custom 'PUT' request
282 | * In this example we create a custom method on our resource to make a PUT request
283 | *
284 | * var app = angular.module('app', ['ngResource', 'ngRoute']);
285 | *
286 | * // Some APIs expect a PUT request in the format URL/object/ID
287 | * // Here we are creating an 'update' method
288 | * app.factory('Notes', ['$resource', function($resource) {
289 | * return $resource('/notes/:id', null,
290 | * {
291 | * 'update': { method:'PUT' }
292 | * });
293 | * }]);
294 | *
295 | * // In our controller we get the ID from the URL using ngRoute and $routeParams
296 | * // We pass in $routeParams and our Notes factory along with $scope
297 | * app.controller('NotesCtrl', ['$scope', '$routeParams', 'Notes',
298 | function($scope, $routeParams, Notes) {
299 | * // First get a note object from the factory
300 | * var note = Notes.get({ id:$routeParams.id });
301 | * $id = note.id;
302 | *
303 | * // Now call update passing in the ID first then the object you are updating
304 | * Notes.update({ id:$id }, note);
305 | *
306 | * // This will PUT /notes/ID with the note object in the request payload
307 | * }]);
308 | *
309 | */
310 | angular.module('ngResource', ['ng']).
311 | factory('$resource', ['$http', '$q', function($http, $q) {
312 |
313 | var DEFAULT_ACTIONS = {
314 | 'get': {method:'GET'},
315 | 'save': {method:'POST'},
316 | 'query': {method:'GET', isArray:true},
317 | 'remove': {method:'DELETE'},
318 | 'delete': {method:'DELETE'}
319 | };
320 | var noop = angular.noop,
321 | forEach = angular.forEach,
322 | extend = angular.extend,
323 | copy = angular.copy,
324 | isFunction = angular.isFunction;
325 |
326 | /**
327 | * We need our custom method because encodeURIComponent is too aggressive and doesn't follow
328 | * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set (pchar) allowed in path
329 | * segments:
330 | * segment = *pchar
331 | * pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
332 | * pct-encoded = "%" HEXDIG HEXDIG
333 | * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
334 | * sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
335 | * / "*" / "+" / "," / ";" / "="
336 | */
337 | function encodeUriSegment(val) {
338 | return encodeUriQuery(val, true).
339 | replace(/%26/gi, '&').
340 | replace(/%3D/gi, '=').
341 | replace(/%2B/gi, '+');
342 | }
343 |
344 |
345 | /**
346 | * This method is intended for encoding *key* or *value* parts of query component. We need a
347 | * custom method because encodeURIComponent is too aggressive and encodes stuff that doesn't
348 | * have to be encoded per http://tools.ietf.org/html/rfc3986:
349 | * query = *( pchar / "/" / "?" )
350 | * pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
351 | * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
352 | * pct-encoded = "%" HEXDIG HEXDIG
353 | * sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
354 | * / "*" / "+" / "," / ";" / "="
355 | */
356 | function encodeUriQuery(val, pctEncodeSpaces) {
357 | return encodeURIComponent(val).
358 | replace(/%40/gi, '@').
359 | replace(/%3A/gi, ':').
360 | replace(/%24/g, '$').
361 | replace(/%2C/gi, ',').
362 | replace(/%20/g, (pctEncodeSpaces ? '%20' : '+'));
363 | }
364 |
365 | function Route(template, defaults) {
366 | this.template = template;
367 | this.defaults = defaults || {};
368 | this.urlParams = {};
369 | }
370 |
371 | Route.prototype = {
372 | setUrlParams: function(config, params, actionUrl) {
373 | var self = this,
374 | url = actionUrl || self.template,
375 | val,
376 | encodedVal;
377 |
378 | var urlParams = self.urlParams = {};
379 | forEach(url.split(/\W/), function(param){
380 | if (param === 'hasOwnProperty') {
381 | throw $resourceMinErr('badname', "hasOwnProperty is not a valid parameter name.");
382 | }
383 | if (!(new RegExp("^\\d+$").test(param)) && param &&
384 | (new RegExp("(^|[^\\\\]):" + param + "(\\W|$)").test(url))) {
385 | urlParams[param] = true;
386 | }
387 | });
388 | url = url.replace(/\\:/g, ':');
389 |
390 | params = params || {};
391 | forEach(self.urlParams, function(_, urlParam){
392 | val = params.hasOwnProperty(urlParam) ? params[urlParam] : self.defaults[urlParam];
393 | if (angular.isDefined(val) && val !== null) {
394 | encodedVal = encodeUriSegment(val);
395 | url = url.replace(new RegExp(":" + urlParam + "(\\W|$)", "g"), function(match, p1) {
396 | return encodedVal + p1;
397 | });
398 | } else {
399 | url = url.replace(new RegExp("(\/?):" + urlParam + "(\\W|$)", "g"), function(match,
400 | leadingSlashes, tail) {
401 | if (tail.charAt(0) == '/') {
402 | return tail;
403 | } else {
404 | return leadingSlashes + tail;
405 | }
406 | });
407 | }
408 | });
409 |
410 | // strip trailing slashes and set the url
411 | url = url.replace(/\/+$/, '') || '/';
412 | // then replace collapse `/.` if found in the last URL path segment before the query
413 | // E.g. `http://url.com/id./format?q=x` becomes `http://url.com/id.format?q=x`
414 | url = url.replace(/\/\.(?=\w+($|\?))/, '.');
415 | // replace escaped `/\.` with `/.`
416 | config.url = url.replace(/\/\\\./, '/.');
417 |
418 |
419 | // set params - delegate param encoding to $http
420 | forEach(params, function(value, key){
421 | if (!self.urlParams[key]) {
422 | config.params = config.params || {};
423 | config.params[key] = value;
424 | }
425 | });
426 | }
427 | };
428 |
429 |
430 | function resourceFactory(url, paramDefaults, actions) {
431 | var route = new Route(url);
432 |
433 | actions = extend({}, DEFAULT_ACTIONS, actions);
434 |
435 | function extractParams(data, actionParams){
436 | var ids = {};
437 | actionParams = extend({}, paramDefaults, actionParams);
438 | forEach(actionParams, function(value, key){
439 | if (isFunction(value)) { value = value(); }
440 | ids[key] = value && value.charAt && value.charAt(0) == '@' ?
441 | lookupDottedPath(data, value.substr(1)) : value;
442 | });
443 | return ids;
444 | }
445 |
446 | function defaultResponseInterceptor(response) {
447 | return response.resource;
448 | }
449 |
450 | function Resource(value){
451 | shallowClearAndCopy(value || {}, this);
452 | }
453 |
454 | forEach(actions, function(action, name) {
455 | var hasBody = /^(POST|PUT|PATCH)$/i.test(action.method);
456 |
457 | Resource[name] = function(a1, a2, a3, a4) {
458 | var params = {}, data, success, error;
459 |
460 | /* jshint -W086 */ /* (purposefully fall through case statements) */
461 | switch(arguments.length) {
462 | case 4:
463 | error = a4;
464 | success = a3;
465 | //fallthrough
466 | case 3:
467 | case 2:
468 | if (isFunction(a2)) {
469 | if (isFunction(a1)) {
470 | success = a1;
471 | error = a2;
472 | break;
473 | }
474 |
475 | success = a2;
476 | error = a3;
477 | //fallthrough
478 | } else {
479 | params = a1;
480 | data = a2;
481 | success = a3;
482 | break;
483 | }
484 | case 1:
485 | if (isFunction(a1)) success = a1;
486 | else if (hasBody) data = a1;
487 | else params = a1;
488 | break;
489 | case 0: break;
490 | default:
491 | throw $resourceMinErr('badargs',
492 | "Expected up to 4 arguments [params, data, success, error], got {0} arguments",
493 | arguments.length);
494 | }
495 | /* jshint +W086 */ /* (purposefully fall through case statements) */
496 |
497 | var isInstanceCall = this instanceof Resource;
498 | var value = isInstanceCall ? data : (action.isArray ? [] : new Resource(data));
499 | var httpConfig = {};
500 | var responseInterceptor = action.interceptor && action.interceptor.response ||
501 | defaultResponseInterceptor;
502 | var responseErrorInterceptor = action.interceptor && action.interceptor.responseError ||
503 | undefined;
504 |
505 | forEach(action, function(value, key) {
506 | if (key != 'params' && key != 'isArray' && key != 'interceptor') {
507 | httpConfig[key] = copy(value);
508 | }
509 | });
510 |
511 | if (hasBody) httpConfig.data = data;
512 | route.setUrlParams(httpConfig,
513 | extend({}, extractParams(data, action.params || {}), params),
514 | action.url);
515 |
516 | var promise = $http(httpConfig).then(function(response) {
517 | var data = response.data,
518 | promise = value.$promise;
519 |
520 | if (data) {
521 | // Need to convert action.isArray to boolean in case it is undefined
522 | // jshint -W018
523 | if (angular.isArray(data) !== (!!action.isArray)) {
524 | throw $resourceMinErr('badcfg', 'Error in resource configuration. Expected ' +
525 | 'response to contain an {0} but got an {1}',
526 | action.isArray?'array':'object', angular.isArray(data)?'array':'object');
527 | }
528 | // jshint +W018
529 | if (action.isArray) {
530 | value.length = 0;
531 | forEach(data, function(item) {
532 | value.push(new Resource(item));
533 | });
534 | } else {
535 | shallowClearAndCopy(data, value);
536 | value.$promise = promise;
537 | }
538 | }
539 |
540 | value.$resolved = true;
541 |
542 | response.resource = value;
543 |
544 | return response;
545 | }, function(response) {
546 | value.$resolved = true;
547 |
548 | (error||noop)(response);
549 |
550 | return $q.reject(response);
551 | });
552 |
553 | promise = promise.then(
554 | function(response) {
555 | var value = responseInterceptor(response);
556 | (success||noop)(value, response.headers);
557 | return value;
558 | },
559 | responseErrorInterceptor);
560 |
561 | if (!isInstanceCall) {
562 | // we are creating instance / collection
563 | // - set the initial promise
564 | // - return the instance / collection
565 | value.$promise = promise;
566 | value.$resolved = false;
567 |
568 | return value;
569 | }
570 |
571 | // instance call
572 | return promise;
573 | };
574 |
575 |
576 | Resource.prototype['$' + name] = function(params, success, error) {
577 | if (isFunction(params)) {
578 | error = success; success = params; params = {};
579 | }
580 | var result = Resource[name].call(this, params, this, success, error);
581 | return result.$promise || result;
582 | };
583 | });
584 |
585 | Resource.bind = function(additionalParamDefaults){
586 | return resourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions);
587 | };
588 |
589 | return Resource;
590 | }
591 |
592 | return resourceFactory;
593 | }]);
594 |
595 |
596 | })(window, window.angular);
597 |
--------------------------------------------------------------------------------