The page you've requested doesn't exist anymore: {{ currentUrl }}. Try visiting the home index to find our more about what this site is, and how you can find what you're looking for.
4 |
5 | By the end of this tutorial you should grasp a few key things about working with Angular.js inside of WordPress. Using a self-installed WordPress install as a backend API service for serving HTTP requests to a front-end Angular.js client is a powerful combination. If you're an eager beaver you can dive right into the [sample plugin on GitHub](https://github.com/Kevinlearynet/wordpress-angular-plugin) which provides the source for the working demo. This example app should demonstrate the following concepts I commonly come across when building Angular apps in WordPress:
6 |
7 | 1. How to work with HTML5 pushState routing
8 | 1. Creating custom API endpoints that are consumed by Angular's `$resource` service
9 | 1. Work with gulp to compile your front-end's LESS, JS and more with a build process
10 | 1. Auto-version your CSS and JS includes
11 | 1. Setup browser cache rules for `/api/**` routes to reduce requests per second (when it makes sense to)
12 | 1. How to handle it all inside of an isolated WordPress plugin
13 |
14 | **If there's a common scenario I didn't mention that you would like to recommend please let me know in the [blog post comments](https://www.kevinleary.net/angularjs-wordpress-tutorial/#respond).**
15 |
16 | ## Table of Contents
17 |
18 | 1. [WordPress Plugin](#wordpress-plugin)
19 | 1. Why a plugin and not a theme?
20 | 1. Server-side routing
21 | 1. Auto-versioning CSS/JS
22 | 1. API Requests to WordPress
23 | 1. [Angular Front-end](#angular-frontend)
24 | 1. Index template
25 | 1. Front-end routing
26 | 1. Concatenating JS includes
27 | 1. HTML5 `pushState` routing
28 | 1. [Gulp Build](#gulp-build)
29 | 1. Watch & Build CSS/JS
30 | 1. JS Compile Tasks
31 | 1. LESS Compile Tasks
32 |
33 | ## WordPress Plugin
34 |
35 | To begin we we'll start by creating a fresh WordPress plugin. I'm assuming you know how to do this already, but if not then I recommend reading the [detailed plugin guidelines](https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/) provided on WordPress.org. This plugin will serve as a backend to our angular app, and will handling the following logic for us:
36 |
37 | 1. Add routing rules to WordPress for serving custom API endpoints
38 | 2. Add routing rules to WordPress to load our base app index template at `/wordpress-angular-plugin/**`. The trailing wildcard is critical for supporting HTML5 pushState routing within our Angular app.
39 | 3. Process and cache HTTP API requests to third-party providers on the server-side (i.e. the US National Weather Service)
40 |
41 | ### Why a plugin and not a theme?
42 |
43 | There are a few key benefits to building both the backend and front-end inside of a single WordPress plugin:
44 |
45 | 1. **Simplicity:** I don't have to support two separate servers, one for a WordPress backend API and another for serving the Angular.js app's HTML. With this approach I can easily do both from one environment.
46 | 1. **Access to WP:** I've found that it's useful to have easy server-side access to WordPress when working with it as a backend. A few scenarios include processing Gravity Forms submissions, passing values from server-side to client-side with `wp_localize_script()` when users are logged in, and various other things.
47 | 1. **Portability** By isolating everything into a WordPress plugin we can easily move our entire app from site to site, enabling and disabling on demand.
48 |
49 | All of the logic described in this tutorial could be used within a WordPress theme as well, the same concepts apply.
50 |
51 | ### Server-side routing
52 |
53 | Our WordPress plugin defines the URL where our Angular app will load based on the path of the plugin directory. The `intercept_wp_router()` method is applied to the `do_parse_request` filter to handle this:
54 |
55 | ~~~.language-php
56 | /**
57 | * Intercept WP Router
58 | *
59 | * Intercept WordPress rewrites and serve a
60 | * static HTML page for our angular.js app.
61 | */
62 | public function intercept_wp_router( $continue, WP $wp, $extra_query_vars ) {
63 |
64 | // Conditions for url path
65 | $url_match = ( substr( $_SERVER['REQUEST_URI'], 0, strlen( $this->base_href ) ) === $this->base_href );
66 | if ( ! $url_match )
67 | return $continue;
68 |
69 | // Vars for index view
70 | $main_js = $this->auto_version_file( 'dist/js/main.js' );
71 | $main_css = $this->auto_version_file( 'dist/css/main.css' );
72 | $plugin_url = $this->plugin_url;
73 | $base_href = $this->base_href;
74 | $page_title = 'WordPress Angular.js Plugin Demo App | kevinleary.net';
75 |
76 | // Browser caching for our main template
77 | $ttl = DAY_IN_SECONDS;
78 | header( "Cache-Control: public, max-age=$ttl" );
79 |
80 | // Load index view
81 | include_once( $this->plugin_dir . 'views/index.php' );
82 | exit;
83 | }
84 | ~~~
85 |
86 | If you want to change the base URL for your app to something custom you'll need to change the value of the public variable `base_href`. This is set in the `__constuct()` method of the `ngApp` Class. That's a mouthful, but basically you would find and modify this line within the plugin:
87 |
88 | ~~~.language-php
89 | dirname( __FILE__ )
90 | ~~~
91 |
92 | In the case of this tutorial, our main plugin file is `wordpress-angular-plugin/wordpress-angular-plugin.php` so the Angular app will load at `/wordpress-angular-plugin/` out of the box. **You can change this whatever you like in the plugin to customize the URL.**
93 |
94 | Once you load up `https://www.yoursite.com/wordpress-angular-plugin/` you should see the same Angular app demo currently available at:
95 |
96 | [kevinleary.net/wordpress-angular-plugin/](https://www.kevinleary.net/wordpress-angular-plugin/)
97 |
98 | ### Auto-versioning CSS/JS
99 |
100 | Google Chrome and other browsers will cache our *.css and *.js for an indefinite period of time. If we make changes to our Angular app's code, or our LESS stylesheet, browsers won't know the file has changed and could serve the old, previously cached version of the file to repeat visitors. For this reason, it's very important that we add version strings to static resources, or in our case the `/dist/js/main.js` and `/dist/css/main.css` files. **This is especially important for single page apps** because we are effectively loading EVERYTHING in the browser.
101 |
102 | Luckily, I've included a setup in this plugin that will handle this for you automatically. This is the only thing that PHP is actually used for in the `index.php` template.
103 |
104 | Here's the method that handles this for us:
105 |
106 | ~~~.language-php
107 | /**
108 | * Auto-version Assets
109 | */
110 | public function auto_version_file( $path_to_file ) {
111 | $file = $this->plugin_dir . $path_to_file;
112 | if ( ! file_exists( $file ) ) return false;
113 |
114 | $mtime = filemtime( $file );
115 | $url = $this->plugin_url . $path_to_file . '?v=' . $mtime;
116 |
117 | return $url;
118 | }
119 | ~~~
120 |
121 | Using PHP's `filemtime()` function we check the modified time of the CSS and JS files, and then we add the timestamp returned to the end of each file as a "version string" like this:
122 |
123 | * `/dist/css/main.css?v=1497114238`
124 | * `/dist/js/main.js?v=1497114238`
125 |
126 | Now you'll always have up to date assets loading for your users!
127 |
128 | ### API Requests to WordPress
129 |
130 | Now that we have the basic structure for an Angular app setup within a WordPress plugin let's look at how to make requests from client (Angular) to server (WordPress) by defining a few custom HTTP API routes. For demo purposes I've wired together a backend API that will:
131 |
132 | 1. Handle incoming requests to `/api/weather/{latitude:longitude}/`
133 | 2. Lookup a weather forecast for the provided lat/long using the National Weather Service API
134 | 3. Return the response body as JSON back to the client
135 |
136 | In a real-world scenario we could just do this inside of Angular entirely, but it serves as a good example to cover common situations where:
137 |
138 | 1. You want to make requests to a secured API without exposing keys on the client-side
139 | 2. You want to cache the results of remote API responses locally to server faster responses, and avoid overage costs for API's where you pay by the request
140 | 3. You want to serve cached results of remote API requests that have been performed by a worker process (_bonus points for performance_)
141 |
142 | In the context of our demo app, an API request is made to the weather API endpoint defined in our plugin to retreive the weather for a users location. This provides a basic demonstration of how to write a PHP backend service that processes input from our Angular app.
143 |
144 | **Important Note**
145 |
146 | I am deliberately NOT using the official [WP REST API](https://wordpress.org/plugins/json-rest-api/) here. I personally believe in building minimal systems that solve specific problems, I think it make a big difference in terms of maintenance, sustainability and security. This is entirely my own opinion, but I beleive it's better to build your own microservices like this rather than load in the entire WP JSON api for many circumstances.
147 |
148 | ## Angular App Front-end
149 |
150 | _The Angular.js app is currently using Angular 1. Most of the project I work on at the moment are still using Angular 1, only a few have made the switch to 2. Because some of this code is directly pulled from those projects I found it easier to work with Angular 1. **In the future I will update this to Angular 2 on another branch.**_
151 |
152 | The Angular 1 app in this demo is very basic, but it does handle a few important things well:
153 |
154 | 1. Concatenates, minifies and proressively enhances LESS and JS using Gulp
155 | 1. Provides HTML5 pushState routing - NO hashbangs here (e.g. www.you.com/pathname/#/slug)
156 | 1. Handles 404's beneath our plugin's top level URL path
157 | 1. Route separation - Individual routes/views/controllers are broken down into separate files. When Angular projects get large and span multiple developers this is very helpful.
158 | 1. Uses the `$resource` service to interface with a custom HTTP API we'll define in WordPress
159 | 1. **Doesn't rely on the WordPress JSON API in anyway**
160 | 1. Provides access to the [Lodash](https://lodash.com/) as an Angular service and template helper
161 |
162 | ### Index template
163 |
164 | Our Angular app is served from a single file: `./views/index.php`. This is where we define our `[ng-app]`, the structure of our HTML doc, and the `` directive provided by [ui-router](https://ui-router.github.io/). This tag will specify where every view inside of our Angular app will load when the URL changes or initially loads.
165 |
166 | Using *.php and not *.html allows us to easily pass values from WordPress into Angular by adding them to an inline JSON object before we load `main.js`. This is a similar approadch to using the `wp_localize_script()` function in WordPress to pass PHP values from a plugin or theme into JS.
167 |
168 | ### Front-end Routing
169 |
170 | The Angular app is served from a single file within the plugin: `./views/index.php`. This is where we define our `[ng-app]`, the structure of our HTML doc, and the `` directive provided by [ui-router](https://ui-router.github.io/). This tag will specify where every view inside of our Angular app will load when the URL changes or initially loads **at or below the app path defined by our plugin.**
171 |
172 | This means that anything underneath this URL is handled by Angular.js routing and the [ui-router](https://ui-router.github.io/) module. We've specifically setup the way we route to and load in our `/views/index.php` file to support this structure. This will handle the WordPress angular routing in a seamless way so that:
173 |
174 | 1. If you visit the URL of an Angular defined route directly it will load in the expected route/view/controller configuration
175 | 2. When you browse from view-to-view within the app fully qualified URL's will be loaded into the browser using the HTML5 pushState API
176 |
177 | It is only possible to do this if **EVERYTHING** underneath our `/wordpress-angular-plugin/` page is handled by Angular and UI router.
178 |
179 | UI-Router defines routes for everything served by Angular undereath the `/wordpress-angular-plugin/` directory. Here's what a typical route definition looks like.
180 |
181 | ~~~.language-javascript
182 | /**
183 | * Home View
184 | */
185 |
186 | // Route
187 | mainApp.config( function( $stateProvider, WP ) {
188 | $stateProvider.state( {
189 | name: 'home',
190 | url: '/',
191 | templateUrl: WP.plugin_url + '/views/home.html',
192 | controller: 'homeCtrl'
193 | } );
194 | } );
195 |
196 | // Controller
197 | mainApp.controller( 'homeCtrl', function( $scope ) {} );
198 | ~~~
199 |
200 | This is pulled right from the contents of the `/views/home.html` template. The `mainApp` is our main ng module defined in `/js/main.js` with `angular.module()`.
201 |
202 | ### Concatenating JS Includes
203 |
204 | When you look at the source of `/js/main.js` you'll see a number of lines that look like this:
205 |
206 | ~~~.language-javascript
207 | // =require ../vendor/angular-sanitize.js
208 | // =require ../vendor/angular-animate.js
209 | // =require ../vendor/angular-ui-router.js
210 | ~~~
211 |
212 | These define external JS files that we want to include in our JS app. These are handled by gulp during the compile process with the help of the [gulp-include](https://www.npmjs.com/package/gulp-include) plugin. For now I find that this approach is straight forward and easier to work with than Webpack or CommonJS, but if you find that your `/dist/js/main.js` file is getting too large then it may be best to work with [Webpack](https://webpack.github.io/) instead.
213 |
214 | _More information on the build process can be found in the [compiling with gulp](#gulp-compiling) section._
215 |
216 | ## Compiling with Gulp
217 |
218 | The plugin uses [Gulp](http://gulpjs.com/) to compile our CSS/JS and also prepares our HTML, with a few smart additions.
219 |
220 | ### Watch & Build CSS/JS
221 |
222 | I recommend that you use the watch process to build your CSS/JS on-demand as it changes. This provides a faster web development workflow. To start gulp in watch mode open a Terminal window and go to the directory where you've cloned this plugin. Enter the `gulp` command in your prompt and Gulp will begin watching for JS/CSS changes.
223 |
224 | If you've just cloned the repo you can build everything with the `gulp build` command. This will run through every process needed to everything we need to serve our micro app (CSS/JS/HTML).
225 |
226 | ### JS Compile Task
227 |
228 | The `js-main` gulp task uses a few gulp plugins to compile and prepare our JavaScript files for the app. This part of the turorial is entirely opinionated, it's a set of tasks that I commonly use so I've included them here.
229 |
230 | 1. Sourcemaps will be generated for easier debugging
231 | 1. The [gulp-include](https://www.npmjs.com/package/gulp-include) plugin will provide an interface similar to CodeKit, allowing you to concatenate multiple JS files into a single compiled file
232 | 1. The [ng-annotate](https://www.npmjs.com/package/ng-annotate) plugin adds and removes AngularJS dependency injection annotations.
233 | 1. Uglify handles minification to compress our source
234 | 1. Caching is added to help speed up Uglify minification
235 |
236 | ### LESS Compile Task
237 |
238 | The `less` task is pretty straight forward. It compiles LESS to CSS, with the following helpful additions:
239 |
240 | 1. Backwards support to automatically add browser prefixes with autoprefixer
241 | 2. Concatenation and minification handled by cleanCSS
242 |
243 | ## Conclusion
244 |
245 | The combination of an Angular.js front-end and a WordPress API backend provides a powerful framework for building all kinds of find things. Hopefully this tutorial gives you a few ideas about how work with the two technologies in your projects. I do myself all the time. **If you have any questions, comments or feedback please let me know in the comments below.**
--------------------------------------------------------------------------------
/vendor/angular-sanitize.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @license AngularJS v1.5.8
3 | * (c) 2010-2016 Google, Inc. http://angularjs.org
4 | * License: MIT
5 | */
6 | (function(window, angular) {'use strict';
7 |
8 | /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
9 | * Any commits to this file should be reviewed with security in mind. *
10 | * Changes to this file can potentially create security vulnerabilities. *
11 | * An approval from 2 Core members with history of modifying *
12 | * this file is required. *
13 | * *
14 | * Does the change somehow allow for arbitrary javascript to be executed? *
15 | * Or allows for someone to change the prototype of built-in objects? *
16 | * Or gives undesired access to variables likes document or window? *
17 | * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
18 |
19 | var $sanitizeMinErr = angular.$$minErr('$sanitize');
20 | var bind;
21 | var extend;
22 | var forEach;
23 | var isDefined;
24 | var lowercase;
25 | var noop;
26 | var htmlParser;
27 | var htmlSanitizeWriter;
28 |
29 | /**
30 | * @ngdoc module
31 | * @name ngSanitize
32 | * @description
33 | *
34 | * # ngSanitize
35 | *
36 | * The `ngSanitize` module provides functionality to sanitize HTML.
37 | *
38 | *
39 | *
40 | *
41 | * See {@link ngSanitize.$sanitize `$sanitize`} for usage.
42 | */
43 |
44 | /**
45 | * @ngdoc service
46 | * @name $sanitize
47 | * @kind function
48 | *
49 | * @description
50 | * Sanitizes an html string by stripping all potentially dangerous tokens.
51 | *
52 | * The input is sanitized by parsing the HTML into tokens. All safe tokens (from a whitelist) are
53 | * then serialized back to properly escaped html string. This means that no unsafe input can make
54 | * it into the returned string.
55 | *
56 | * The whitelist for URL sanitization of attribute values is configured using the functions
57 | * `aHrefSanitizationWhitelist` and `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider
58 | * `$compileProvider`}.
59 | *
60 | * The input may also contain SVG markup if this is enabled via {@link $sanitizeProvider}.
61 | *
62 | * @param {string} html HTML input.
63 | * @returns {string} Sanitized HTML.
64 | *
65 | * @example
66 |
67 |
68 |
80 |
81 | Snippet:
82 |
83 |
84 |
Directive
85 |
How
86 |
Source
87 |
Rendered
88 |
89 |
90 |
ng-bind-html
91 |
Automatically uses $sanitize
92 |
<div ng-bind-html="snippet"> </div>
93 |
94 |
95 |
96 |
ng-bind-html
97 |
Bypass $sanitize by explicitly trusting the dangerous value
By enabling this setting without taking other precautions, you might expose your
182 | * application to click-hijacking attacks. In these attacks, sanitized svg elements could be positioned
183 | * outside of the containing element and be rendered over other elements on the page (e.g. a login
184 | * link). Such behavior can then result in phishing incidents.
185 | *
186 | *
To protect against these, explicitly setup `overflow: hidden` css rule for all potential svg
187 | * tags within the sanitized content:
614 |
615 |
616 | angular.module('linkyExample', ['ngSanitize'])
617 | .controller('ExampleController', ['$scope', function($scope) {
618 | $scope.snippet =
619 | 'Pretty text with some links:\n'+
620 | 'http://angularjs.org/,\n'+
621 | 'mailto:us@somewhere.org,\n'+
622 | 'another@somewhere.org,\n'+
623 | 'and one more: ftp://127.0.0.1/.';
624 | $scope.snippetWithSingleURL = 'http://angularjs.org/';
625 | }]);
626 |
627 |
628 | it('should linkify the snippet with urls', function() {
629 | expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
630 | toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' +
631 | 'another@somewhere.org, and one more: ftp://127.0.0.1/.');
632 | expect(element.all(by.css('#linky-filter a')).count()).toEqual(4);
633 | });
634 |
635 | it('should not linkify snippet without the linky filter', function() {
636 | expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()).
637 | toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' +
638 | 'another@somewhere.org, and one more: ftp://127.0.0.1/.');
639 | expect(element.all(by.css('#escaped-html a')).count()).toEqual(0);
640 | });
641 |
642 | it('should update', function() {
643 | element(by.model('snippet')).clear();
644 | element(by.model('snippet')).sendKeys('new http://link.');
645 | expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
646 | toBe('new http://link.');
647 | expect(element.all(by.css('#linky-filter a')).count()).toEqual(1);
648 | expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText())
649 | .toBe('new http://link.');
650 | });
651 |
652 | it('should work with the target property', function() {
653 | expect(element(by.id('linky-target')).
654 | element(by.binding("snippetWithSingleURL | linky:'_blank'")).getText()).
655 | toBe('http://angularjs.org/');
656 | expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank');
657 | });
658 |
659 | it('should optionally add custom attributes', function() {
660 | expect(element(by.id('linky-custom-attributes')).
661 | element(by.binding("snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}")).getText()).
662 | toBe('http://angularjs.org/');
663 | expect(element(by.css('#linky-custom-attributes a')).getAttribute('rel')).toEqual('nofollow');
664 | });
665 |
666 |
667 | */
668 | angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) {
669 | var LINKY_URL_REGEXP =
670 | /((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"\u201d\u2019]/i,
671 | MAILTO_REGEXP = /^mailto:/i;
672 |
673 | var linkyMinErr = angular.$$minErr('linky');
674 | var isDefined = angular.isDefined;
675 | var isFunction = angular.isFunction;
676 | var isObject = angular.isObject;
677 | var isString = angular.isString;
678 |
679 | return function(text, target, attributes) {
680 | if (text == null || text === '') return text;
681 | if (!isString(text)) throw linkyMinErr('notstring', 'Expected string but received: {0}', text);
682 |
683 | var attributesFn =
684 | isFunction(attributes) ? attributes :
685 | isObject(attributes) ? function getAttributesObject() {return attributes;} :
686 | function getEmptyAttributesObject() {return {};};
687 |
688 | var match;
689 | var raw = text;
690 | var html = [];
691 | var url;
692 | var i;
693 | while ((match = raw.match(LINKY_URL_REGEXP))) {
694 | // We can not end in these as they are sometimes found at the end of the sentence
695 | url = match[0];
696 | // if we did not match ftp/http/www/mailto then assume mailto
697 | if (!match[2] && !match[4]) {
698 | url = (match[3] ? 'http://' : 'mailto:') + url;
699 | }
700 | i = match.index;
701 | addText(raw.substr(0, i));
702 | addLink(url, match[0].replace(MAILTO_REGEXP, ''));
703 | raw = raw.substring(i + match[0].length);
704 | }
705 | addText(raw);
706 | return $sanitize(html.join(''));
707 |
708 | function addText(text) {
709 | if (!text) {
710 | return;
711 | }
712 | html.push(sanitizeText(text));
713 | }
714 |
715 | function addLink(url, text) {
716 | var key, linkAttributes = attributesFn(url);
717 | html.push('');
731 | addText(text);
732 | html.push('');
733 | }
734 | };
735 | }]);
736 |
737 |
738 | })(window, window.angular);
739 |
--------------------------------------------------------------------------------
/vendor/angular-resource.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @license AngularJS v1.5.8
3 | * (c) 2010-2016 Google, Inc. http://angularjs.org
4 | * License: MIT
5 | */
6 | (function(window, angular) {'use strict';
7 |
8 | var $resourceMinErr = angular.$$minErr('$resource');
9 |
10 | // Helper functions and regex to lookup a dotted path on an object
11 | // stopping at undefined/null. The path must be composed of ASCII
12 | // identifiers (just like $parse)
13 | var MEMBER_NAME_REGEX = /^(\.[a-zA-Z_$@][0-9a-zA-Z_$@]*)+$/;
14 |
15 | function isValidDottedPath(path) {
16 | return (path != null && path !== '' && path !== 'hasOwnProperty' &&
17 | MEMBER_NAME_REGEX.test('.' + path));
18 | }
19 |
20 | function lookupDottedPath(obj, path) {
21 | if (!isValidDottedPath(path)) {
22 | throw $resourceMinErr('badmember', 'Dotted member path "@{0}" is invalid.', path);
23 | }
24 | var keys = path.split('.');
25 | for (var i = 0, ii = keys.length; i < ii && angular.isDefined(obj); i++) {
26 | var key = keys[i];
27 | obj = (obj !== null) ? obj[key] : undefined;
28 | }
29 | return obj;
30 | }
31 |
32 | /**
33 | * Create a shallow copy of an object and clear other fields from the destination
34 | */
35 | function shallowClearAndCopy(src, dst) {
36 | dst = dst || {};
37 |
38 | angular.forEach(dst, function(value, key) {
39 | delete dst[key];
40 | });
41 |
42 | for (var key in src) {
43 | if (src.hasOwnProperty(key) && !(key.charAt(0) === '$' && key.charAt(1) === '$')) {
44 | dst[key] = src[key];
45 | }
46 | }
47 |
48 | return dst;
49 | }
50 |
51 | /**
52 | * @ngdoc module
53 | * @name ngResource
54 | * @description
55 | *
56 | * # ngResource
57 | *
58 | * The `ngResource` module provides interaction support with RESTful services
59 | * via the $resource service.
60 | *
61 | *
62 | *
63 | *
64 | * See {@link ngResource.$resourceProvider} and {@link ngResource.$resource} for usage.
65 | */
66 |
67 | /**
68 | * @ngdoc provider
69 | * @name $resourceProvider
70 | *
71 | * @description
72 | *
73 | * Use `$resourceProvider` to change the default behavior of the {@link ngResource.$resource}
74 | * service.
75 | *
76 | * ## Dependencies
77 | * Requires the {@link ngResource } module to be installed.
78 | *
79 | */
80 |
81 | /**
82 | * @ngdoc service
83 | * @name $resource
84 | * @requires $http
85 | * @requires ng.$log
86 | * @requires $q
87 | * @requires ng.$timeout
88 | *
89 | * @description
90 | * A factory which creates a resource object that lets you interact with
91 | * [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer) server-side data sources.
92 | *
93 | * The returned resource object has action methods which provide high-level behaviors without
94 | * the need to interact with the low level {@link ng.$http $http} service.
95 | *
96 | * Requires the {@link ngResource `ngResource`} module to be installed.
97 | *
98 | * By default, trailing slashes will be stripped from the calculated URLs,
99 | * which can pose problems with server backends that do not expect that
100 | * behavior. This can be disabled by configuring the `$resourceProvider` like
101 | * this:
102 | *
103 | * ```js
104 | app.config(['$resourceProvider', function($resourceProvider) {
105 | // Don't strip trailing slashes from calculated URLs
106 | $resourceProvider.defaults.stripTrailingSlashes = false;
107 | }]);
108 | * ```
109 | *
110 | * @param {string} url A parameterized URL template with parameters prefixed by `:` as in
111 | * `/user/:username`. If you are using a URL with a port number (e.g.
112 | * `http://example.com:8080/api`), it will be respected.
113 | *
114 | * If you are using a url with a suffix, just add the suffix, like this:
115 | * `$resource('http://example.com/resource.json')` or `$resource('http://example.com/:id.json')`
116 | * or even `$resource('http://example.com/resource/:resource_id.:format')`
117 | * If the parameter before the suffix is empty, :resource_id in this case, then the `/.` will be
118 | * collapsed down to a single `.`. If you need this sequence to appear and not collapse then you
119 | * can escape it with `/\.`.
120 | *
121 | * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in
122 | * `actions` methods. If a parameter value is a function, it will be called every time
123 | * a param value needs to be obtained for a request (unless the param was overridden). The function
124 | * will be passed the current data value as an argument.
125 | *
126 | * Each key value in the parameter object is first bound to url template if present and then any
127 | * excess keys are appended to the url search query after the `?`.
128 | *
129 | * Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in
130 | * URL `/path/greet?salutation=Hello`.
131 | *
132 | * If the parameter value is prefixed with `@`, then the value for that parameter will be
133 | * extracted from the corresponding property on the `data` object (provided when calling a
134 | * "non-GET" action method).
135 | * For example, if the `defaultParam` object is `{someParam: '@someProp'}` then the value of
136 | * `someParam` will be `data.someProp`.
137 | * Note that the parameter will be ignored, when calling a "GET" action method (i.e. an action
138 | * method that does not accept a request body)
139 | *
140 | * @param {Object.