angular-resource.js 17 KB

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