upgrade to AngularJS 1.0.3 bouncy-thunder
[angular-drzb] / app / lib / angular / angular-resource.js
1 /**
2  * @license AngularJS v1.0.3
3  * (c) 2010-2012 Google, Inc. http://angularjs.org
4  * License: MIT
5  */
6 (function(window, angular, undefined) {
7 'use strict';
8
9 /**
10  * @ngdoc overview
11  * @name ngResource
12  * @description
13  */
14
15  /**
16  * @ngdoc object
17  * @name ngResource.$resource
18  * @requires $http
19  *
20  * @description
21  * A factory which creates a resource object that lets you interact with
22  * [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer) server-side data sources.
23  *
24  * The returned resource object has action methods which provide high-level behaviors without
25  * the need to interact with the low level {@link ng.$http $http} service.
26  *
27  * @param {string} url A parameterized URL template with parameters prefixed by `:` as in
28  *   `/user/:username`.
29  *
30  * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in
31  *   `actions` methods.
32  *
33  *   Each key value in the parameter object is first bound to url template if present and then any
34  *   excess keys are appended to the url search query after the `?`.
35  *
36  *   Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in
37  *   URL `/path/greet?salutation=Hello`.
38  *
39  *   If the parameter value is prefixed with `@` then the value of that parameter is extracted from
40  *   the data object (useful for non-GET operations).
41  *
42  * @param {Object.<Object>=} actions Hash with declaration of custom action that should extend the
43  *   default set of resource actions. The declaration should be created in the following format:
44  *
45  *       {action1: {method:?, params:?, isArray:?},
46  *        action2: {method:?, params:?, isArray:?},
47  *        ...}
48  *
49  *   Where:
50  *
51  *   - `action` – {string} – The name of action. This name becomes the name of the method on your
52  *     resource object.
53  *   - `method` – {string} – HTTP request method. Valid methods are: `GET`, `POST`, `PUT`, `DELETE`,
54  *     and `JSONP`
55  *   - `params` – {object=} – Optional set of pre-bound parameters for this action.
56  *   - isArray – {boolean=} – If true then the returned object for this action is an array, see
57  *     `returns` section.
58  *
59  * @returns {Object} A resource "class" object with methods for the default set of resource actions
60  *   optionally extended with custom `actions`. The default set contains these actions:
61  *
62  *       { 'get':    {method:'GET'},
63  *         'save':   {method:'POST'},
64  *         'query':  {method:'GET', isArray:true},
65  *         'remove': {method:'DELETE'},
66  *         'delete': {method:'DELETE'} };
67  *
68  *   Calling these methods invoke an {@link ng.$http} with the specified http method,
69  *   destination and parameters. When the data is returned from the server then the object is an
70  *   instance of the resource class `save`, `remove` and `delete` actions are available on it as
71  *   methods with the `$` prefix. This allows you to easily perform CRUD operations (create, read,
72  *   update, delete) on server-side data like this:
73  *   <pre>
74         var User = $resource('/user/:userId', {userId:'@id'});
75         var user = User.get({userId:123}, function() {
76           user.abc = true;
77           user.$save();
78         });
79      </pre>
80  *
81  *   It is important to realize that invoking a $resource object method immediately returns an
82  *   empty reference (object or array depending on `isArray`). Once the data is returned from the
83  *   server the existing reference is populated with the actual data. This is a useful trick since
84  *   usually the resource is assigned to a model which is then rendered by the view. Having an empty
85  *   object results in no rendering, once the data arrives from the server then the object is
86  *   populated with the data and the view automatically re-renders itself showing the new data. This
87  *   means that in most case one never has to write a callback function for the action methods.
88  *
89  *   The action methods on the class object or instance object can be invoked with the following
90  *   parameters:
91  *
92  *   - HTTP GET "class" actions: `Resource.action([parameters], [success], [error])`
93  *   - non-GET "class" actions: `Resource.action([parameters], postData, [success], [error])`
94  *   - non-GET instance actions:  `instance.$action([parameters], [success], [error])`
95  *
96  *
97  * @example
98  *
99  * # Credit card resource
100  *
101  * <pre>
102      // Define CreditCard class
103      var CreditCard = $resource('/user/:userId/card/:cardId',
104       {userId:123, cardId:'@id'}, {
105        charge: {method:'POST', params:{charge:true}}
106       });
107
108      // We can retrieve a collection from the server
109      var cards = CreditCard.query(function() {
110        // GET: /user/123/card
111        // server returns: [ {id:456, number:'1234', name:'Smith'} ];
112
113        var card = cards[0];
114        // each item is an instance of CreditCard
115        expect(card instanceof CreditCard).toEqual(true);
116        card.name = "J. Smith";
117        // non GET methods are mapped onto the instances
118        card.$save();
119        // POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'}
120        // server returns: {id:456, number:'1234', name: 'J. Smith'};
121
122        // our custom method is mapped as well.
123        card.$charge({amount:9.99});
124        // POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'}
125      });
126
127      // we can create an instance as well
128      var newCard = new CreditCard({number:'0123'});
129      newCard.name = "Mike Smith";
130      newCard.$save();
131      // POST: /user/123/card {number:'0123', name:'Mike Smith'}
132      // server returns: {id:789, number:'01234', name: 'Mike Smith'};
133      expect(newCard.id).toEqual(789);
134  * </pre>
135  *
136  * The object returned from this function execution is a resource "class" which has "static" method
137  * for each action in the definition.
138  *
139  * Calling these methods invoke `$http` on the `url` template with the given `method` and `params`.
140  * When the data is returned from the server then the object is an instance of the resource type and
141  * all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD
142  * operations (create, read, update, delete) on server-side data.
143
144    <pre>
145      var User = $resource('/user/:userId', {userId:'@id'});
146      var user = User.get({userId:123}, function() {
147        user.abc = true;
148        user.$save();
149      });
150    </pre>
151  *
152  *     It's worth noting that the success callback for `get`, `query` and other method gets passed
153  *     in the response that came from the server as well as $http header getter function, so one
154  *     could rewrite the above example and get access to http headers as:
155  *
156    <pre>
157      var User = $resource('/user/:userId', {userId:'@id'});
158      User.get({userId:123}, function(u, getResponseHeaders){
159        u.abc = true;
160        u.$save(function(u, putResponseHeaders) {
161          //u => saved user object
162          //putResponseHeaders => $http header getter
163        });
164      });
165    </pre>
166
167  * # Buzz client
168
169    Let's look at what a buzz client created with the `$resource` service looks like:
170     <doc:example>
171       <doc:source jsfiddle="false">
172        <script>
173          function BuzzController($resource) {
174            this.userId = 'googlebuzz';
175            this.Activity = $resource(
176              'https://www.googleapis.com/buzz/v1/activities/:userId/:visibility/:activityId/:comments',
177              {alt:'json', callback:'JSON_CALLBACK'},
178              {get:{method:'JSONP', params:{visibility:'@self'}}, replies: {method:'JSONP', params:{visibility:'@self', comments:'@comments'}}}
179            );
180          }
181
182          BuzzController.prototype = {
183            fetch: function() {
184              this.activities = this.Activity.get({userId:this.userId});
185            },
186            expandReplies: function(activity) {
187              activity.replies = this.Activity.replies({userId:this.userId, activityId:activity.id});
188            }
189          };
190          BuzzController.$inject = ['$resource'];
191        </script>
192
193        <div ng-controller="BuzzController">
194          <input ng-model="userId"/>
195          <button ng-click="fetch()">fetch</button>
196          <hr/>
197          <div ng-repeat="item in activities.data.items">
198            <h1 style="font-size: 15px;">
199              <img src="{{item.actor.thumbnailUrl}}" style="max-height:30px;max-width:30px;"/>
200              <a href="{{item.actor.profileUrl}}">{{item.actor.name}}</a>
201              <a href ng-click="expandReplies(item)" style="float: right;">Expand replies: {{item.links.replies[0].count}}</a>
202            </h1>
203            {{item.object.content | html}}
204            <div ng-repeat="reply in item.replies.data.items" style="margin-left: 20px;">
205              <img src="{{reply.actor.thumbnailUrl}}" style="max-height:30px;max-width:30px;"/>
206              <a href="{{reply.actor.profileUrl}}">{{reply.actor.name}}</a>: {{reply.content | html}}
207            </div>
208          </div>
209        </div>
210       </doc:source>
211       <doc:scenario>
212       </doc:scenario>
213     </doc:example>
214  */
215 angular.module('ngResource', ['ng']).
216   factory('$resource', ['$http', '$parse', function($http, $parse) {
217     var DEFAULT_ACTIONS = {
218       'get':    {method:'GET'},
219       'save':   {method:'POST'},
220       'query':  {method:'GET', isArray:true},
221       'remove': {method:'DELETE'},
222       'delete': {method:'DELETE'}
223     };
224     var noop = angular.noop,
225         forEach = angular.forEach,
226         extend = angular.extend,
227         copy = angular.copy,
228         isFunction = angular.isFunction,
229         getter = function(obj, path) {
230           return $parse(path)(obj);
231         };
232
233   /**
234    * We need our custom mehtod because encodeURIComponent is too aggressive and doesn't follow
235    * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set (pchar) allowed in path
236    * segments:
237    *    segment       = *pchar
238    *    pchar         = unreserved / pct-encoded / sub-delims / ":" / "@"
239    *    pct-encoded   = "%" HEXDIG HEXDIG
240    *    unreserved    = ALPHA / DIGIT / "-" / "." / "_" / "~"
241    *    sub-delims    = "!" / "$" / "&" / "'" / "(" / ")"
242    *                     / "*" / "+" / "," / ";" / "="
243    */
244   function encodeUriSegment(val) {
245     return encodeUriQuery(val, true).
246       replace(/%26/gi, '&').
247       replace(/%3D/gi, '=').
248       replace(/%2B/gi, '+');
249   }
250
251
252   /**
253    * This method is intended for encoding *key* or *value* parts of query component. We need a custom
254    * method becuase encodeURIComponent is too agressive and encodes stuff that doesn't have to be
255    * encoded per http://tools.ietf.org/html/rfc3986:
256    *    query       = *( pchar / "/" / "?" )
257    *    pchar         = unreserved / pct-encoded / sub-delims / ":" / "@"
258    *    unreserved    = ALPHA / DIGIT / "-" / "." / "_" / "~"
259    *    pct-encoded   = "%" HEXDIG HEXDIG
260    *    sub-delims    = "!" / "$" / "&" / "'" / "(" / ")"
261    *                     / "*" / "+" / "," / ";" / "="
262    */
263   function encodeUriQuery(val, pctEncodeSpaces) {
264     return encodeURIComponent(val).
265       replace(/%40/gi, '@').
266       replace(/%3A/gi, ':').
267       replace(/%24/g, '$').
268       replace(/%2C/gi, ',').
269       replace((pctEncodeSpaces ? null : /%20/g), '+');
270   }
271
272   function Route(template, defaults) {
273       this.template = template = template + '#';
274       this.defaults = defaults || {};
275       var urlParams = this.urlParams = {};
276       forEach(template.split(/\W/), function(param){
277         if (param && template.match(new RegExp("[^\\\\]:" + param + "\\W"))) {
278           urlParams[param] = true;
279         }
280       });
281       this.template = template.replace(/\\:/g, ':');
282     }
283
284     Route.prototype = {
285       url: function(params) {
286         var self = this,
287             url = this.template,
288             val,
289             encodedVal;
290
291         params = params || {};
292         forEach(this.urlParams, function(_, urlParam){
293           val = params.hasOwnProperty(urlParam) ? params[urlParam] : self.defaults[urlParam];
294           if (angular.isDefined(val) && val !== null) {
295             encodedVal = encodeUriSegment(val);
296             url = url.replace(new RegExp(":" + urlParam + "(\\W)", "g"), encodedVal + "$1");
297           } else {
298             url = url.replace(new RegExp("/?:" + urlParam + "(\\W)", "g"), '$1');
299           }
300         });
301         url = url.replace(/\/?#$/, '');
302         var query = [];
303         forEach(params, function(value, key){
304           if (!self.urlParams[key]) {
305             query.push(encodeUriQuery(key) + '=' + encodeUriQuery(value));
306           }
307         });
308         query.sort();
309         url = url.replace(/\/*$/, '');
310         return url + (query.length ? '?' + query.join('&') : '');
311       }
312     };
313
314
315     function ResourceFactory(url, paramDefaults, actions) {
316       var route = new Route(url);
317
318       actions = extend({}, DEFAULT_ACTIONS, actions);
319
320       function extractParams(data, actionParams){
321         var ids = {};
322         actionParams = extend({}, paramDefaults, actionParams);
323         forEach(actionParams, function(value, key){
324           ids[key] = value.charAt && value.charAt(0) == '@' ? getter(data, value.substr(1)) : value;
325         });
326         return ids;
327       }
328
329       function Resource(value){
330         copy(value || {}, this);
331       }
332
333       forEach(actions, function(action, name) {
334         var hasBody = action.method == 'POST' || action.method == 'PUT' || action.method == 'PATCH';
335         Resource[name] = function(a1, a2, a3, a4) {
336           var params = {};
337           var data;
338           var success = noop;
339           var error = null;
340           switch(arguments.length) {
341           case 4:
342             error = a4;
343             success = a3;
344             //fallthrough
345           case 3:
346           case 2:
347             if (isFunction(a2)) {
348               if (isFunction(a1)) {
349                 success = a1;
350                 error = a2;
351                 break;
352               }
353
354               success = a2;
355               error = a3;
356               //fallthrough
357             } else {
358               params = a1;
359               data = a2;
360               success = a3;
361               break;
362             }
363           case 1:
364             if (isFunction(a1)) success = a1;
365             else if (hasBody) data = a1;
366             else params = a1;
367             break;
368           case 0: break;
369           default:
370             throw "Expected between 0-4 arguments [params, data, success, error], got " +
371               arguments.length + " arguments.";
372           }
373
374           var value = this instanceof Resource ? this : (action.isArray ? [] : new Resource(data));
375           $http({
376             method: action.method,
377             url: route.url(extend({}, extractParams(data, action.params || {}), params)),
378             data: data
379           }).then(function(response) {
380               var data = response.data;
381
382               if (data) {
383                 if (action.isArray) {
384                   value.length = 0;
385                   forEach(data, function(item) {
386                     value.push(new Resource(item));
387                   });
388                 } else {
389                   copy(data, value);
390                 }
391               }
392               (success||noop)(value, response.headers);
393             }, error);
394
395           return value;
396         };
397
398
399         Resource.bind = function(additionalParamDefaults){
400           return ResourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions);
401         };
402
403
404         Resource.prototype['$' + name] = function(a1, a2, a3) {
405           var params = extractParams(this),
406               success = noop,
407               error;
408
409           switch(arguments.length) {
410           case 3: params = a1; success = a2; error = a3; break;
411           case 2:
412           case 1:
413             if (isFunction(a1)) {
414               success = a1;
415               error = a2;
416             } else {
417               params = a1;
418               success = a2 || noop;
419             }
420           case 0: break;
421           default:
422             throw "Expected between 1-3 arguments [params, success, error], got " +
423               arguments.length + " arguments.";
424           }
425           var data = hasBody ? this : undefined;
426           Resource[name].call(this, params, data, success, error);
427         };
428       });
429       return Resource;
430     }
431
432     return ResourceFactory;
433   }]);
434
435 })(window, window.angular);