├── .gitignore
├── .travis.yml
├── .editorconfig
├── package.json
├── bower.json
├── LICENSE
├── Gruntfile.js
├── .eslintrc
├── README.md
└── payload.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .project
3 | .settings/
4 | bower_components/
5 | node_modules/
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "stable"
4 | before_script:
5 | - npm install -g grunt-cli
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 | indent_style = space
3 | indent_size = 4
4 | end_of_line = lf
5 | charset = utf-8
6 | trim_trailing_whitespace = true
7 | insert_final_newline = false
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "payloadjs",
3 | "version": "0.7.0",
4 | "description": "REST API payload management made easy. Payload.js is a javascript single page application (SPA) driver designed to interact intuitively with REST APIs from within web and mobile apps.",
5 | "main": "payload.js",
6 | "scripts": {
7 | "test": "grunt test"
8 | },
9 | "repository": "https://github.com/pklauzinski/payload.git",
10 | "keywords": [
11 | "payload",
12 | "payloadjs",
13 | "payload.js",
14 | "rest",
15 | "spa",
16 | "single-page-application",
17 | "html5",
18 | "ajax"
19 | ],
20 | "author": "Philip Klauzinski (http://webtopian.com)",
21 | "license": "MIT",
22 | "bugs": {
23 | "url": "https://github.com/pklauzinski/payload/issues"
24 | },
25 | "homepage": "http://payloadjs.com",
26 | "dependencies": {
27 | "jquery": ">=1.7"
28 | },
29 | "devDependencies": {
30 | "grunt": "^1.0.3",
31 | "grunt-bump": "^0.8.0",
32 | "gruntify-eslint": "5.0.0"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "payloadjs",
3 | "homepage": "http://payloadjs.com",
4 | "authors": [
5 | {
6 | "name": "Philip Klauzinski",
7 | "homepage": "http://webtopian.com"
8 | }
9 | ],
10 | "repository": {
11 | "type": "git",
12 | "url": "https://github.com/pklauzinski/payload"
13 | },
14 | "description": "Payload.js is a javascript single page application (SPA) driver for REST API payload management. It is designed to interact intuitively with REST API endpoints from within web and mobile apps.",
15 | "main": "payload.js",
16 | "moduleType": [
17 | "amd",
18 | "globals",
19 | "node"
20 | ],
21 | "dependencies": {
22 | "jquery": ">=1.7"
23 | },
24 | "keywords": [
25 | "payload",
26 | "payloadjs",
27 | "payload.js",
28 | "rest",
29 | "spa",
30 | "single-page-application",
31 | "html5",
32 | "ajax"
33 | ],
34 | "license": "MIT",
35 | "ignore": [
36 | "**/.*",
37 | "node_modules",
38 | "bower_components",
39 | "test",
40 | "tests",
41 | "package*.json",
42 | "Gruntfile.js"
43 | ]
44 | }
45 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Philip Klauzinski
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | module.exports = function(grunt) {
2 |
3 | 'use strict';
4 |
5 | grunt.initConfig({
6 |
7 | pkg: grunt.file.readJSON('package.json'),
8 |
9 | /**
10 | * https://github.com/gyandeeps/gruntify-eslint
11 | */
12 | eslint: {
13 | options: {
14 | configFile: '.eslintrc'
15 | },
16 | src: ['Gruntfile.js', 'payload.js']
17 | },
18 |
19 | /**
20 | * https://github.com/vojtajina/grunt-bump
21 | */
22 | bump: {
23 | options: {
24 | files: ['package.json'],
25 | updateConfigs: ['pkg'],
26 | commit: true,
27 | commitMessage: 'Release version %VERSION%',
28 | commitFiles: ['.'],
29 | createTag: true,
30 | tagName: 'v%VERSION%',
31 | tagMessage: 'Release version %VERSION%',
32 | push: true,
33 | pushTo: 'origin',
34 | gitDescribeOptions: '--tags --always --abbrev=1 --dirty=-d',
35 | globalReplace: false,
36 | prereleaseName: false,
37 | metadata: '',
38 | regExp: false
39 | }
40 | }
41 |
42 | });
43 |
44 | grunt.loadNpmTasks('gruntify-eslint');
45 | grunt.loadNpmTasks('grunt-bump');
46 |
47 | grunt.registerTask('default', ['eslint']);
48 | grunt.registerTask('test', ['eslint']);
49 | };
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true
4 | },
5 | "globals": {
6 | "jQuery": true
7 | },
8 | "rules": {
9 | "curly": [
10 | 2,
11 | "all"
12 | ],
13 | "eqeqeq": 2,
14 | "no-eq-null": 2,
15 | "guard-for-in": 2,
16 | "no-unused-vars": [
17 | 2,
18 | {
19 | "args": "none"
20 | }
21 | ],
22 | "strict": [
23 | 2,
24 | "safe"
25 | ],
26 | "no-use-before-define": [
27 | 2,
28 | {
29 | "functions": false
30 | }
31 | ],
32 | "operator-linebreak": [
33 | 2,
34 | "after"
35 | ],
36 | "max-len": [
37 | 2,
38 | {
39 | "code": 200,
40 | "tabWidth": 4,
41 | "ignoreComments": true
42 | }
43 | ],
44 | "indent": [
45 | 2,
46 | 4,
47 | {
48 | "SwitchCase": 1
49 | }
50 | ],
51 | "quotes": [
52 | 2,
53 | "single"
54 | ],
55 | "no-multi-str": 2,
56 | "no-mixed-spaces-and-tabs": 2,
57 | "no-trailing-spaces": 2,
58 | "space-unary-ops": [
59 | 2,
60 | {
61 | "nonwords": false,
62 | "overrides": {}
63 | }
64 | ],
65 | "brace-style": [
66 | 2,
67 | "1tbs",
68 | {
69 | "allowSingleLine": true
70 | }
71 | ],
72 | "comma-spacing": [
73 | 2,
74 | {
75 | "before": false,
76 | "after": true
77 | }
78 | ],
79 | "comma-dangle": [
80 | 2,
81 | "never"
82 | ],
83 | "comma-style": [
84 | "error",
85 | "last"
86 | ],
87 | "keyword-spacing": [
88 | 2,
89 | {}
90 | ],
91 | "space-infix-ops": 2,
92 | "space-before-blocks": [
93 | 2,
94 | "always"
95 | ],
96 | "space-before-function-paren": [
97 | 2,
98 | {
99 | "anonymous": "ignore",
100 | "named": "never"
101 | }
102 | ],
103 | "array-bracket-spacing": [
104 | 2,
105 | "never"
106 | ],
107 | "space-in-parens": [
108 | 2,
109 | "never"
110 | ],
111 | "no-multiple-empty-lines": 2,
112 | "no-with": 2,
113 | "no-spaced-func": 2,
114 | "key-spacing": [
115 | 2,
116 | {
117 | "beforeColon": false,
118 | "afterColon": true
119 | }
120 | ],
121 | "dot-notation": 2,
122 | "semi": [
123 | 2,
124 | "always"
125 | ]
126 | }
127 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Payload.js
2 |
3 | [](https://travis-ci.org/payloadjs/payload)
4 | [](https://www.npmjs.com/package/payloadjs)
5 | [](https://github.com/payloadjs/payload)
6 | [](LICENSE)
7 |
8 | Payload.js is a javascript single page application (SPA) driver that allows you to automate and manage REST API requests and render [Handlebars](http://handlebarsjs.com/) templates or raw HTML from the response data. It also allows you to render Handlebars templates with expressions populated by data in memory, and includes a **pub/sub**, or **publish/subscribe**, API for managing custom events.
9 |
10 | When DOM objects imbued with Payload.js's selectors are activated, a call to `Payload.apiRequest()` is performed, which involves making an XHR request and/or rendering a template. Payload.js also contains **rendered template/response caching** and an **extensions API** as additional means of integration.
11 |
12 | ## Table of Contents
13 |
14 | - [Installation](#installation)
15 | - [Install from NPM](#install-from-npm)
16 | - [Install from Bower](#install-from-bower)
17 | - [Dependencies](#dependencies)
18 | - [Selectors](#selectors)
19 | - [HTML5 API](#html5-api)
20 | - [Payload.js Initialization Options](#payload-js-initialization-options)
21 | - [Primary Methods](#primary-methods)
22 | - [Helper Methods](#helper-methods)
23 | - [API Request Handling](#api-request-handling)
24 | - [API Request Flow](#api-request-flow)
25 | - [API Callback Params](#api-callback-params)
26 | - [API Object & HTML Attributes](#api-object--html-attributes)
27 | - [Template Data](#template-data)
28 | - [Payload.js Object Properties](#payloadjs-object-properties)
29 |
30 | ## Installation
31 |
32 | Payload.js can be cloned or downloaded [directly from GitHub](https://github.com/payloadjs/payload), installed from [NPM](https://www.npmjs.com/), or installed from [Bower](http://bower.io/).
33 |
34 | ### Install from NPM
35 |
36 | ```sh
37 | $ npm install payloadjs --save
38 | ```
39 |
40 | ### Install from Bower
41 |
42 | ```sh
43 | $ bower install payloadjs --save
44 | ```
45 |
46 | ## Dependencies
47 | - [jQuery](https://jquery.com/) >= v1.7
48 | - [Handlebars runtime](http://handlebarsjs.com/precompilation.html) - version depends entirely on your compiler version
49 |
50 | > **Note:** Handlebars is not required if you choose to only work with raw XHR responses, e.g. HTML returned directly from an API request.
51 |
52 | ## Selectors
53 |
54 | Payload.js automatically binds to HTML elements based on the following selectors:
55 |
56 | - **Links** (activated on click)
57 | - `a[data-selector]`
58 | - `a[data-url]`
59 | - `button[data-selector]`
60 | - `button[data-url]`
61 | - **Forms** (activated on submit)
62 | - `form[data-selector]`
63 | - `form[data-url]`
64 |
65 | Payload.js selectors can also contain the `data-auto-load` attribute to cause them to be automatically invoked on page/template load. Selectors are used to invoke API calls and/or render templates when they receive an appropriate trigger event or they are called directly with `Payload.apiRequest()`.
66 |
67 | ## HTML5 API
68 |
69 | The most useful feature of Payload.js is its intuitive HTML5 API. It can be used out of the box with little to no configuration and interacted with entirely from HTML. This makes Payload.js accessible to the non-javascript-savvy developer.
70 |
71 | ```html
72 |
77 | ```
78 |
79 | ## Payload.js Initialization Options
80 |
81 | | Option | Type | Default | Description |
82 | |----------------------|----------------------|-------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
83 | | `apiAccessToken` | `string` | `''` | Supply an access token for your application and it will be added as the `Authorization` request header for all XHR requests. |
84 | | `apiAfterRender` | `function` | `$.noop` | Invoked just after rendering a template. |
85 | | `apiBeforeRender` | `function` | `$.noop` | Invoked just before rendering a template. |
86 | | `apiCallback` | `function` | `$.noop` | Called during API request handling before XHR requests (but after loading a template if no XHR request is required). See [API Callback Params](#api-callback-params) for more information about the arguments. |
87 | | `apiOnClick` | `function` | `function() { return true; }` | Invoked on API link activation; if it returns a false value, Payload.js will skip performing an API request. |
88 | | `apiOnSubmit` | `function` | `function() { return true; }` | Invoked on API form submission; if it returns a false value, Payload.js will skip performing an API request. |
89 | | `apiResponseParent` | `string` | `''` | If set, unserialized XHR response objects will use the specified property as the parent key for response data (e.g. if all API responses contain `{"response": {}}`). |
90 | | `context` | `object` or `string` | `'body'` | Payload.js will only monitor events on the given DOM object or selector string. |
91 | | `dataNamespace` | `string` | `''` | If set, Payload will look for `data-[dataNamespace]-` attributes instead of `data-` attributes. This can be used to prevent `data-` attribute naming conflicts. |
92 | | `debug` | `boolean` | `false` | If `true`, outputs useful debugging information to the `console`. |
93 | | `loadingDefault` | `boolean` | `true` | If `true`, sets the default behavior for all XHR requests to clear the `$target` element and show the `loadingHtml` within it. If `false`, the `$target` element's content will not be cleared until updated by the `$api` response. This can be overridden for any API request using the `data-loading` attribute. |
94 | | `loadingHtml` | `string` | `Loading...` | The default HTML to insert into API `$target` if the `loading` flag for the request is `true`. |
95 | | `partials` | `object` | `Handlebars.partials` | The namespace containing precompiled Handlebars partials. |
96 | | `subscribers` | `array` | `[]` | List of events and callback functions to pass to `Payload.addSubscribers` method. These subscribers are initialized upon the `Payload.delivery` call. |
97 | | `templates` | `object` | `Handlebars.templates` | The namespace containing precompiled Handlebars templates. |
98 | | `timeout` | `number` | `0` | Timeout value, in milliseconds, before XHR requests are aborted. This can be overridden for any API request using the `data-timeout` attribute. If set to `0`, no timeout will occur. |
99 | | `xhrAlways` | `function` | `$.noop` | Called after each XHR request, regardless of success/failure. |
100 | | `xhrBeforeSend` | `function` | `$.noop` | Called before each XHR request. |
101 | | `xhrDone` | `function` | `$.noop` | Called after each successful XHR request. |
102 | | `xhrFail` | `function` | `$.noop` | Called after each failed XHR request. |
103 |
104 | ## Primary Methods
105 |
106 | - `deliver(options)` - Used to initialize the initial options to Payload.js and start monitoring the Payload.js context for events; see [Payload.js Options](#payload-js-initialization-options).
107 | - `apiRequest($origin)` - Automatically called when a selector is activated. May also be called explicitly by passing in a jQuery object with the proper data attributes. See [API Request Handling](#api-request-handling) for more information about this method.
108 | - `triggerAutoLoad($element)` - Perform an API request on any DOM nodes containing the attribute `data-auto-load`. If `$element` is given, perform an API request on the given jQuery object instead of on the Payload.js `$context`.
109 | - `publish(eventName, arguments)` - Publish a Payload.js. Any arguments given will pass through to the event handlers subscribed to the event named.
110 | - `subscribe(eventName, function)` - Subscribe to a Payload.js event. When the specified event is published the function provided will be invoked and passed event-specific arguments.
111 | - `unsubscribe(eventName, function)` - Stop subscribing to a Payload.js event.
112 | - `addSubscribers(subscribers)` - Subscribe multiple functions to an array of events: `subscribers={events: [], methods: []}`.
113 | - `clearCache(type, key)` - Based on the `type`, the cached "response" data will be cleared for the given key. If no `type` is specified all caches are cleared. If a `type` is specified but no `key`, then all items within that `type` of cache are cleared.
114 |
115 | ## Helper Methods
116 |
117 | - `merge(options)` - Allows you to merge or *extend* the current Payload.js options with new options via the given object.
118 | - `serializeObject($form)` - Serializes form data into an object, automatically creating arrays for form fields containing the same name.
119 | - `debug(m)` - Allows you to pass in a single method as a string with the subsequent parameters being that method's arguments, *OR* pass in an object containing keys as method names and values as method arguments (single argument or array of multiple arguments).
120 |
121 | ## API Request Handling
122 |
123 | Once a selector has been activated, Payload.js performs any API calls requested and renders the template specified. Your app can interact with Payload.js via various callback methods and by "subscribing" to API events. Upon completion of rendering a template, Payload.js will call `Payload.triggerAutoLoad($target)`.
124 |
125 | When performing an API request Payload.js will also manage showing and hiding loading indicators. If the "data-loading" attribute is set to "true" (or the "loadingDefault" option is true) the "$target" element will be cleared and have the "loadingHtml" inserted. Otherwise Payload.js will look for an element with the attribute `data-role` set to `loading` and call `jQuery.show()` on it.
126 |
127 | ### API Request Flow
128 |
129 | Note that DOM objects must either perform an API call by having `api.url` set (see the [API Object and HTML attributes](#api-object-and-html-attributes)) or specifying a template to load.
130 |
131 | 1. Subscribe to events with the `beforeRender` namespace to modify any data before the view is rendered.
132 | 1. Invoke `apiCallback` method set in Payload.js options (see [API Callback Params](#api-callback-params)).
133 | 1. If no API URL was specified or a cached view was used, publish the API events and trigger `auto-load`.
134 | 1. Show any configured loading indicators.
135 | 1. Perform the XHR.
136 | 1. Failures will invoke the `xhrFail` callback option.
137 | 1. The `xhrAlways` callback option always gets called on XHR completion.
138 | 1. The `xhrDone` callback is invoked upon a successful XHR.
139 | 1. If a template selector is defined, render the template into the specified location. If `api.loading` was set, the loading element will quickly fade out first. The `xhrDone` callback option is invoked, API events are published, and `triggerAutoLoad($target)` is called.
140 | 1. Cache the XHR response if requested.
141 |
142 | ### API Callback Params
143 |
144 | When invoking events or invoking callbacks for `apiCallback`, `apiBeforeRender`, and `apiAfterRender`, a `params` argument is passed to the handler. It contains the following properties:
145 |
146 | - `$origin` - The jQuery object the Payload.js API originated on.
147 | - `$target` - The jQuery object pointed to by `$origin`'s `[data-selector]` attribute.
148 | - `api` - The API object described in [API Object and HTML Attributes](#api-object-and-html-attributes).
149 |
150 | If the API call involves making a XHR request the following additional attributes are abilable on the "params" object:
151 |
152 | - `response` - API response
153 | - `status` - jQuery status result
154 | - `jqXHR` - jQuery XHR response object
155 | - `html` - template HTML (undefined until HTML is rendered)
156 | - `api` - The API object described in [API Object and HTML Attributes](#api-object-and-html-attributes).
157 |
158 | ### API Object and HTML Attributes
159 |
160 | The API object defined below is passed within the API params to the various callback methods. The various options are controlled through HTML `data-` attributes on the `$origin` object, which points to a `$target` object for template rendering.
161 |
162 | - `href` - The `href` attribute value from `$origin`; for reference and for use with an external routing component
163 | - `url` - `$origin` `data-url` or `action` (form) attribute; Used as API URL if set
164 | - `method` - `$origin` `data-method` or `method` attribute; HTTP method to use in API call (default: `'get'`)
165 | - `cacheRequest` - `$origin` `data-cache-request` attribute; if `true` flag XHR request to be cached
166 | - `cacheResponse` - `$origin` `data-cache-response` attribute; if `true` use cached response from Payload.js
167 | - `type` - jQuery XHR request type (default: `'json'`)
168 | - `selector` - `$origin` `data-selector` attribute; jQuery selector for the API $target that a template will be loaded into
169 | - `template` - `$origin "data-template" attribute; name of the Handlebars template to load into the location specified by `data-selector` (overrides `data-partial` if also set)
170 | - `partial` - `$origin` `data-partial` attribute; name of the Handlebars partial template to load into the location specified by `data-selector`
171 | - `events` - `$origin` `data-publish` attribute; space-separated list of events to have published to Payload.js subscribers
172 | - `requestData` - combination of JSON serialized form data and any JSON-encoded values within $origin `data-form` attribute
173 | - `templateData` - See [Template Data](#template-data) below
174 | - `loading` - `$origin` `data-loading` attribute; if "true" (or the `loadingDefault` option is true) the `$target` element will be cleared and have the `loadingHtml` from Payload.js options inserted during API request handling.
175 |
176 | ### Template Data
177 |
178 | Every Handlebars template always has the following data available:
179 |
180 | - `app` - Any custom application data contained in `Payload.appData`
181 | - `request` - `href`, `url`, `method`, and `cacheKey` for the API call
182 | - `view` - A dictionary of all `data-*` attributes from `$origin`
183 |
184 | ## Payload.js Object Properties
185 |
186 | - `options` - Current set of options (see [Payload.js Options](#payload-js-initialization-options))
187 | - `appData` - Object for storing arbitrary application data; provided as `app` within template data
188 | - `cache` - Object containing `response` cache; available for external component cache
--------------------------------------------------------------------------------
/payload.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Payload.js - Javascript Single Page Application Driver
3 | * @see {@link http://payloadjs.com}
4 | *
5 | * @copyright 2015-2017, Philip Klauzinski
6 | * @license Released under the MIT license (http://www.opensource.org/licenses/mit-license.php)
7 | * @author Philip Klauzinski (http://webtopian.com)
8 | * @requires jQuery v1.7+
9 | * @preserve
10 | */
11 | (function(root, factory) {
12 |
13 | 'use strict';
14 |
15 | if (typeof define === 'function' && define.amd) {
16 | // AMD
17 | define(['jquery'], factory);
18 | } else if (typeof exports === 'object') {
19 | // Node
20 | module.exports = factory(require('jquery'));
21 | } else {
22 | // Browser globals
23 | root.Payload = factory(root.jQuery);
24 | }
25 |
26 | }(this, function($) {
27 |
28 | 'use strict';
29 |
30 | /**
31 | * Payload.js constructor function
32 | *
33 | * @constructor
34 | */
35 | var Payload = function() {
36 |
37 | var _this = this,
38 |
39 | /**
40 | * Will be set to given application DOM context
41 | *
42 | * @private
43 | */
44 | _$context,
45 |
46 | /**
47 | * Default options
48 | *
49 | * @type {{}}
50 | * @private
51 | */
52 | _options = {
53 | apiAccessToken: '',
54 | apiAfterRender: $.noop,
55 | apiBeforeRender: $.noop,
56 | apiCallback: $.noop,
57 | apiOnClick: function() {
58 | return true;
59 | },
60 | apiOnSubmit: function() {
61 | return true;
62 | },
63 | apiResponseParent: '',
64 | context: 'body',
65 | dataNamespace: '',
66 | debug: false,
67 | loadingDefault: true,
68 | loadingHtml: 'Loading...',
69 | partials: (typeof Handlebars === 'undefined') ? {} : Handlebars.partials,
70 | subscribers: [], // [ {events: [], methods: [] } ]
71 | templates: (typeof Handlebars === 'undefined') ? {} : Handlebars.templates,
72 | timeout: 0,
73 | contentType: 'application/json',
74 | xhrAlways: $.noop,
75 | xhrBeforeSend: $.noop,
76 | xhrDone: $.noop,
77 | xhrFail: $.noop
78 | },
79 |
80 | _dataPrefix = 'data-' + (_options.dataNamespace ? $.trim(_options.dataNamespace) + '-' : ''),
81 |
82 | _selectors = {
83 | API_FORM: 'form[' + _dataPrefix + 'selector],form[' + _dataPrefix + 'url]',
84 | API_LINK: 'a[' + _dataPrefix + 'selector],a[' + _dataPrefix + 'url],button[' + _dataPrefix + 'selector],button[' + _dataPrefix + 'url]',
85 | AUTO_LOAD: '[' + _dataPrefix + 'auto-load]',
86 | CLICK: '[' + _dataPrefix + 'click]',
87 | LOADING: '[' + _dataPrefix + 'role="loading"]'
88 | },
89 |
90 | _cache = {
91 | response: {}
92 | },
93 |
94 | _payloadEvents = [
95 | 'init',
96 | 'apiBeforeRender',
97 | 'apiAfterRender',
98 | 'xhrAlways',
99 | 'xhrBeforeSend',
100 | 'xhrDone',
101 | 'xhrFail'
102 | ],
103 |
104 | _$payloadEvents = $({}),
105 |
106 | _$userEvents = $({}),
107 |
108 | /**
109 | * Stores registered Payload components
110 | * If a component is unregistered, it will be removed from this object
111 | *
112 | * @type {{}}
113 | * @private
114 | */
115 | _components = {},
116 |
117 | /**
118 | * Safe console debug
119 | * http://webtopian.com/safe-firebug-console-in-javascript
120 | *
121 | * @param m
122 | * @private
123 | */
124 | _debug = function(m) {
125 | var args, sMethod;
126 | if (_options.debug && typeof console === 'object' && (typeof m === 'object' || typeof console[m] === 'function')) {
127 | if (typeof m === 'object') {
128 | for (sMethod in m) {
129 | if (m.hasOwnProperty(sMethod)) {
130 | if (typeof console[sMethod] === 'function') {
131 | args = (typeof m[sMethod] === 'string' || (typeof m[sMethod] === 'object' && m[sMethod].length === undefined)) ? [m[sMethod]] : m[sMethod];
132 | console[sMethod].apply(console, args);
133 | } else {
134 | console.log(m[sMethod]);
135 | }
136 | }
137 | }
138 | } else {
139 | console[m].apply(console, Array.prototype.slice.call(arguments, 1));
140 | }
141 | }
142 | },
143 |
144 | /**
145 | * Throw Payload.js specific error
146 | *
147 | * @param text
148 | * @private
149 | */
150 | _error = function(text) {
151 | throw 'Payload.js: ' + text;
152 | },
153 |
154 | /**
155 | * Publish Payload internal event
156 | *
157 | * @private
158 | */
159 | _pub = function() {
160 | _$payloadEvents.trigger.apply(_$payloadEvents, arguments);
161 | },
162 |
163 | /**
164 | * Subscribe to Payload internal event
165 | *
166 | * @private
167 | */
168 | _sub = function() {
169 | _$payloadEvents.on.apply(_$payloadEvents, arguments);
170 | },
171 |
172 | /**
173 | * Unsubscribe from Payload internal event
174 | *
175 | * @private
176 | */
177 | _unsub = function() {
178 | _$payloadEvents.off.apply(_$payloadEvents, arguments);
179 | },
180 |
181 | /**
182 | * Test if browser storage API is both supported and available
183 | *
184 | * @param type
185 | * @returns {boolean}
186 | * @private
187 | */
188 | _storageAvailable = function(type) {
189 | try {
190 | var storage = window[type],
191 | name = '__storage_test__';
192 |
193 | storage.setItem(name, 1);
194 | storage.removeItem(name);
195 | return true;
196 | } catch (e) {
197 | return false;
198 | }
199 | },
200 |
201 | /**
202 | * Internal JSON API for localStorage
203 | *
204 | * @type {{}}
205 | * @private
206 | */
207 | _storage = {
208 | /**
209 | * Safely store obj to localStorage or sessionStorage under specified id
210 | * Defaults to localStorage. Pass optional _type param as 'session' to access sessionStorage.
211 | *
212 | * @param id
213 | * @param obj
214 | * @param _type
215 | */
216 | set: function(id, obj, _type) {
217 | var type = _type === 'session' ? 'sessionStorage' : 'localStorage';
218 | if (_storageAvailable(type)) {
219 | window[type].setItem(id, JSON.stringify(obj));
220 | } else {
221 | _this.debug('warn', type + ' not available');
222 | }
223 | return obj;
224 | },
225 |
226 | /**
227 | * Safely get object in localStorage or sessionStorage under specified id
228 | * Defaults to localStorage. Pass optional _type param as 'session' to access sessionStorage.
229 | *
230 | * @param id
231 | * @param _type
232 | */
233 | get: function(id, _type) {
234 | var type = _type === 'session' ? 'sessionStorage' : 'localStorage',
235 | storage = {};
236 |
237 | if (_storageAvailable(type)) {
238 | storage = JSON.parse(window[type].getItem(id));
239 | } else {
240 | _this.debug('warn', type + ' not available');
241 | }
242 | // storage could be null, so return {} if so
243 | return storage || {};
244 | },
245 |
246 | /**
247 | * Safely remove localStorage or sessionStorage item under specified id
248 | * Defaults to localStorage. Pass optional _type param as 'session' to access sessionStorage.
249 | *
250 | * @param id
251 | * @param _type
252 | */
253 | remove: function(id, _type) {
254 | var type = _type === 'session' ? 'sessionStorage' : 'localStorage';
255 | if (_storageAvailable(type)) {
256 | window[type].removeItem(id);
257 | return true;
258 | } else {
259 | _this.debug('warn', type + ' not available');
260 | return false;
261 | }
262 | }
263 | },
264 |
265 | /**
266 | * Shortcut methods for accessing _storage API as sessionStorage only
267 | *
268 | * @type {{}}
269 | * @private
270 | */
271 | _session = {
272 | set: function(id, obj) {
273 | return _storage.set(id, obj, 'session');
274 | },
275 | get: function(id) {
276 | return _storage.get(id, 'session');
277 | },
278 | remove: function(id) {
279 | return _storage.remove(id, 'session');
280 | }
281 | },
282 |
283 | // Delegation methods
284 |
285 | _delegateApiRequests = function() {
286 | _$context.on('click.api-request auto-load.api-request', _selectors.API_LINK, function(e) {
287 | var $this = $(this);
288 | e.preventDefault();
289 | if ($this.prop('disabled') || $this.hasClass('disabled')) {
290 | return;
291 | }
292 | if (_options.apiOnClick($this, e)) {
293 | _this.apiRequest($this);
294 | }
295 | }).on('submit.api-request auto-load.api-request', _selectors.API_FORM, function(e) {
296 | var $this = $(this);
297 | e.preventDefault();
298 | if (_options.apiOnSubmit($this, e)) {
299 | _this.apiRequest($this);
300 | }
301 | });
302 | },
303 |
304 | _delegateClicks = function() {
305 | _$context.on('click.click', _selectors.CLICK, function(e) {
306 | var $this = $(this),
307 | $click = $($this.attr(_dataPrefix + 'click'));
308 |
309 | e.preventDefault();
310 | $click.click();
311 | });
312 | },
313 |
314 | _initDelegatedBehaviors = function() {
315 | _delegateApiRequests();
316 | _delegateClicks();
317 | },
318 |
319 | /**
320 | * Initialization
321 | *
322 | * @returns {Payload}
323 | * @private
324 | */
325 | _initialize = function() {
326 | _$context = $(_options.context);
327 | if (!_$context.length) {
328 | _error('Selector "' + _options.context + '" not found');
329 | }
330 | _initDelegatedBehaviors();
331 | if (_options.subscribers.length) {
332 | _this.addSubscribers(_options.subscribers);
333 | }
334 | return _this;
335 | },
336 |
337 | /**
338 | * Publish the user defined events for a given API request
339 | *
340 | * @param params
341 | * @param namespace
342 | * @private
343 | */
344 | _publishUserEvents = function(params, namespace) {
345 | var i, event_name;
346 | for (i = 0; i < params.api.events.length; i++) {
347 | event_name = params.api.events[i] + '.' + (namespace || 'afterRender');
348 | _this.publish(event_name, [params]);
349 | }
350 | }
351 |
352 | ; // End private var declaration
353 |
354 | /**
355 | *
356 | * Public vars and methods
357 | *
358 | */
359 |
360 | this.options = _options;
361 |
362 | /**
363 | * Stores registered Payload components and gives public access to them, if needed
364 | * Additionally, if a component is unregistered, it will be removed from this object
365 | *
366 | * @type {{}}
367 | */
368 | this.components = {};
369 |
370 | /**
371 | * This object is supplied for storing custom data within your app
372 | * It is exposed within templates via the "app" namespace
373 | *
374 | * @type {{}}
375 | */
376 | this.appData = {};
377 |
378 | /**
379 | * Expose the internal _cache object for external editing
380 | *
381 | * @type {{}}
382 | */
383 | this.cache = _cache;
384 |
385 | /**
386 | * Expose the internal _debug method for external use
387 | *
388 | * @type {_debug}
389 | */
390 | this.debug = _debug;
391 |
392 | /**
393 | * Expose internal storage API
394 | *
395 | * @type {{}}
396 | */
397 | this.storage = _storage;
398 |
399 | /**
400 | * Expose internal storage API with only sessionStorage
401 | *
402 | * @type {{}}
403 | */
404 | this.session = _session;
405 |
406 | /**
407 | * Deliver Payload API functionality to the specified context
408 | *
409 | * @param opts
410 | * @returns {Payload}
411 | */
412 | this.deliver = function(opts) {
413 | if (typeof(opts) === 'function') {
414 | _options.apiCallback = opts;
415 | } else if (typeof(opts) === 'object') {
416 | _this.merge(opts);
417 | } else if (typeof(opts) === 'string') {
418 | _options.context = opts;
419 | }
420 | return _initialize();
421 | };
422 |
423 | /**
424 | * Merge the current options with the given new options
425 | *
426 | * @param opts
427 | * @returns {*}
428 | */
429 | this.merge = function(opts) {
430 | return $.extend(_options, opts);
431 | };
432 |
433 | /**
434 | * Make an API request via the given jQuery $origin object
435 | * This method is called internally when a Payload API object is interacted with in the DOM
436 | * It may also be called directly by supplying any jQuery object with the appropriate attributes
437 | * Optionally pass in data as second parameter to be sent with request
438 | *
439 | * @param $origin
440 | * @param data
441 | * @returns {Payload}
442 | * @todo - break this up into more granular methods
443 | */
444 | this.apiRequest = function($origin, data) {
445 | var api = {
446 | href: $origin.attr('href'),
447 | url: $origin.attr(_dataPrefix + 'url') || $origin.attr('action'),
448 | method: ($origin.attr(_dataPrefix + 'method') || $origin.attr('method') || 'get').toLowerCase(),
449 | cacheRequest: $origin.attr(_dataPrefix + 'cache-request') || false,
450 | cacheResponse: $origin.attr(_dataPrefix + 'cache-response') || false,
451 | type: $origin.attr(_dataPrefix + 'type') || 'json',
452 | selector: $origin.attr(_dataPrefix + 'selector') || false,
453 | template: _options.templates[$origin.attr(_dataPrefix + 'template')] || false,
454 | partial: _options.partials[$origin.attr(_dataPrefix + 'partial')] || false,
455 | events: $origin.attr(_dataPrefix + 'publish') ? $origin.attr(_dataPrefix + 'publish').split(' ') : [],
456 | requestData: $.extend(
457 | data !== undefined && data.constructor === Array ? [] : {},
458 | _this.serializeObject($origin), data || {},
459 | JSON.parse($origin.attr(_dataPrefix + 'form') || '{}')
460 | ),
461 | timeout: $origin.attr(_dataPrefix + 'timeout') || _options.timeout,
462 | templateData: {
463 | app: _this.appData,
464 | view: $origin.data()
465 | },
466 | token: $origin.attr(_dataPrefix + 'token') || _options.apiAccessToken || false,
467 | contentType: $origin.attr(_dataPrefix + 'content-type') || _options.contentType
468 | },
469 | templateName = $origin.attr(_dataPrefix + 'template') || $origin.attr(_dataPrefix + 'partial'),
470 | api_request, $target, $loading, $load, html, templateData, params;
471 |
472 | // Add the request payload to the template data under "request" namespace
473 | api.templateData.request = $.extend({
474 | href: api.href,
475 | url: api.url,
476 | method: api.method,
477 | cacheKey: api.url + $origin.serialize()
478 | }, api.requestData);
479 | // Grab a reference to its easier to refer to the cache key
480 | // ... which may be modified by the "pre" events below
481 | api_request = api.templateData.request;
482 |
483 | api.loading = ($origin.attr(_dataPrefix + 'loading') ?
484 | JSON.parse($origin.attr(_dataPrefix + 'loading')) : _options.loadingDefault);
485 |
486 | // Begin template sequence
487 | if (api.url || api.selector && (api.template || api.partial)) {
488 | $target = $(api.selector);
489 | $loading = $origin.find('[' + _dataPrefix + 'role="loading"]');
490 | params = {
491 | $origin: $origin,
492 | $target: $target,
493 | api: api
494 | };
495 |
496 | if (!api.url) {
497 | // User events with "pre" namespace triggered before render
498 | _publishUserEvents(params, 'beforeRender');
499 | _options.apiBeforeRender(params);
500 | _pub('apiBeforeRender', [params]);
501 | html = api.template ? api.template(api.templateData) : api.partial(api.templateData);
502 | $target.html(html);
503 | _options.apiAfterRender(params);
504 | _pub('apiAfterRender', [params]);
505 | }
506 |
507 | _options.apiCallback(params);
508 |
509 | if (!api.url) {
510 | _publishUserEvents(params);
511 | _this.triggerAutoLoad($target.find(_selectors.AUTO_LOAD));
512 | return _this;
513 | }
514 |
515 | if (api.cacheResponse &&
516 | _cache.response[api_request.cacheKey] &&
517 | _cache.response[api_request.cacheKey].data &&
518 | _cache.response[api_request.cacheKey].done
519 | ) {
520 | templateData = $.extend({}, _cache.response[api_request.cacheKey].data, api.templateData);
521 | params.response = _cache.response[api_request.cacheKey].response;
522 | // User events with "pre" namespace triggered before render
523 | _publishUserEvents(params, 'beforeRender');
524 | _options.apiBeforeRender(params);
525 | _pub('apiBeforeRender', [params]);
526 | html = api.template ? api.template(templateData) : api.partial(templateData);
527 | $target.html(html);
528 | _options.apiAfterRender(params);
529 | _pub('apiAfterRender', [params]);
530 | _cache.response[api_request.cacheKey].done();
531 | _publishUserEvents(params);
532 | _this.triggerAutoLoad($target.find(_selectors.AUTO_LOAD));
533 | return _this;
534 | }
535 |
536 | // Begin AJAX sequence
537 |
538 | // Set selector as busy, and show loading indicator if available
539 | $target.attr('aria-busy', true);
540 | if (api.loading) {
541 | $load = $(_options.loadingHtml).attr(_dataPrefix + 'role', 'loading');
542 | $target.empty().prepend($load);
543 | } else if ($loading.length) {
544 | $loading.show();
545 | }
546 |
547 | $.ajax({
548 | url: api.url,
549 | type: api.method,
550 | dataType: api.type,
551 | data: (api.method === 'get') ? $.param(data || {}) : JSON.stringify(api.requestData),
552 | contentType: api.contentType,
553 | cache: api.cacheRequest,
554 | timeout: api.timeout,
555 | beforeSend: function(jqXHR, settings) {
556 | var params = {
557 | jqXHR: jqXHR,
558 | settings: settings,
559 | $origin: $origin,
560 | $target: $target,
561 | api: api
562 | };
563 | if (api.token) {
564 | jqXHR.setRequestHeader('Authorization', api.token);
565 | }
566 | _options.xhrBeforeSend(params);
567 | _pub('xhrBeforeSend', [params]);
568 | }
569 | }).done(function(response, status, jqXHR) {
570 | var responseData = _options.apiResponseParent ? response[_options.apiResponseParent] : response,
571 | templateData = $.extend({}, api.templateData, $.isArray(responseData) ? {data: responseData} : responseData),
572 | params = {
573 | response: response,
574 | status: status,
575 | jqXHR: jqXHR,
576 | $origin: $origin,
577 | $target: $target,
578 | html: undefined, // filled in below after HTML is rendered
579 | api: $.extend(api, {templateData: templateData})
580 | },
581 | xhrDone = function() {
582 | _options.xhrDone(params);
583 | _pub('xhrDone', [params]);
584 | _this.triggerAutoLoad($target.find(_selectors.AUTO_LOAD));
585 | },
586 | $loading = $target.find(_selectors.LOADING);
587 |
588 | // User events with "pre" namespace triggered before render
589 | _publishUserEvents(params, 'beforeRender');
590 |
591 | if ($target.length && api.loading && $loading.length) {
592 | $loading.fadeOut(100, function() {
593 | _options.apiBeforeRender(params);
594 | _pub('apiBeforeRender', [params]);
595 | html = templateName ? (api.template ? api.template(templateData) : api.partial(templateData)) : false;
596 | params.html = html;
597 | $target.html(html);
598 | _options.apiAfterRender(params);
599 | _pub('apiAfterRender', [params]);
600 | xhrDone();
601 | _publishUserEvents(params);
602 | });
603 | } else {
604 | if ($target.length) {
605 | _options.apiBeforeRender(params);
606 | _pub('apiBeforeRender', [params]);
607 | html = templateName ? (api.template ? api.template(templateData) : api.partial(templateData)) : false;
608 | params.html = html;
609 | $target.html(html);
610 | _options.apiAfterRender(params);
611 | _pub('apiAfterRender', [params]);
612 | }
613 | xhrDone();
614 | _publishUserEvents(params);
615 | }
616 |
617 | if (api.cacheResponse) {
618 | _cache.response[api_request.cacheKey] = {
619 | response: response,
620 | data: templateData,
621 | done: xhrDone
622 | };
623 | }
624 | }).fail(function(jqXHR, status, error) {
625 | var params = {
626 | jqXHR: jqXHR,
627 | status: status,
628 | error: error,
629 | $origin: $origin,
630 | $target: $target,
631 | api: api
632 | };
633 | _options.xhrFail(params);
634 | _pub('xhrFail', [params]);
635 | }).always(function(responseORjqXHR, status, jqXHRorError) {
636 | var success = (status === 'success'),
637 | params = {
638 | response: success ? responseORjqXHR : null,
639 | jqXHR: (status === 'success') ? jqXHRorError : responseORjqXHR,
640 | status: status,
641 | error: success ? null : jqXHRorError,
642 | $origin: $origin,
643 | $target: $target,
644 | api: api
645 | };
646 |
647 | _options.xhrAlways(params);
648 | _pub('xhrAlways', [params]);
649 | // Remove selector busy status
650 | $target.removeAttr('aria-busy');
651 | });
652 | }
653 | return _this;
654 | };
655 |
656 | /**
657 | * Trigger the 'auto-load' event on given jQuery object
658 | * When no argument is passed, trigger 'auto-load' if found within the app context
659 | *
660 | * @param $e
661 | * @returns {Payload}
662 | */
663 | this.triggerAutoLoad = function($e) {
664 | if ($e !== undefined) {
665 | if ($e instanceof $ && $e.length) {
666 | $e.each(function() {
667 | _this.apiRequest($(this));
668 | });
669 | }
670 | } else {
671 | _$context.find(_selectors.AUTO_LOAD).each(function() {
672 | _this.apiRequest($(this));
673 | });
674 | }
675 | return _this;
676 | };
677 |
678 | /**
679 | * Publish a custom event
680 | * based on https://github.com/cowboy/jquery-tiny-pubsub
681 | *
682 | * @returns {Payload}
683 | */
684 | this.publish = function() {
685 | if (arguments[0].indexOf('.') === -1) {
686 | arguments[0] += '.afterRender';
687 | }
688 | _debug('info', '"' + arguments[0] + '"', 'event published.');
689 | _$userEvents.trigger.apply(_$userEvents, arguments);
690 | return _this;
691 | };
692 |
693 | /**
694 | * Subscribe to a custom event
695 | * based on https://github.com/cowboy/jquery-tiny-pubsub
696 | *
697 | * @returns {Payload}
698 | */
699 | this.subscribe = function() {
700 | var namespaceIndex = arguments[0].indexOf('.');
701 | if (namespaceIndex === -1) {
702 | arguments[0] += '.afterRender';
703 | } else if (arguments[0].substr(namespaceIndex) === '.pre') {
704 | // @todo: remove the check for 'pre' namespace in v1.0
705 | arguments[0] = arguments[0].replace('.pre', '.beforeRender');
706 | _debug('warn', 'ATTENTION: The user event \'pre\' namespace has been deprecated. Use \'beforeRender\' instead.');
707 | }
708 | _$userEvents.on.apply(_$userEvents, arguments);
709 | return _this;
710 | };
711 |
712 | /**
713 | * Unsubscribe from a custom event
714 | * based on https://github.com/cowboy/jquery-tiny-pubsub
715 | *
716 | * @returns {Payload}
717 | */
718 | this.unsubscribe = function() {
719 | _$userEvents.off.apply(_$userEvents, arguments);
720 | return _this;
721 | };
722 |
723 | /**
724 | * Subscribe custom events to given methods in array of {events: [], methods: []} objects
725 | *
726 | * @param subscribers
727 | * @returns {Payload}
728 | */
729 | this.addSubscribers = function(subscribers) {
730 | $.each(subscribers, function(i, val) {
731 | if (val.events) {
732 | if (typeof val.events === 'string') {
733 | val.events = [val.events];
734 | }
735 | } else {
736 | return;
737 | }
738 | if (val.methods) {
739 | if (typeof val.methods === 'function') {
740 | val.methods = [val.methods];
741 | }
742 | } else {
743 | return;
744 | }
745 | $.each(val.events, function(j, ev) {
746 | $.each(val.methods, function(k, func) {
747 | _this.subscribe(ev, func);
748 | });
749 | });
750 | });
751 | return _this;
752 | };
753 |
754 | /**
755 | * Convert given form input data to an object
756 | *
757 | * @param $form
758 | * @returns {{}}
759 | */
760 | this.serializeObject = function($form) {
761 | var o = {},
762 | a = $form.serializeArray();
763 |
764 | $.each(a, function() {
765 | if (o[this.name] !== undefined) {
766 | if (!o[this.name].push) {
767 | o[this.name] = [o[this.name]];
768 | }
769 | o[this.name].push(this.value || '');
770 | } else {
771 | o[this.name] = this.value || '';
772 | }
773 | });
774 | return o;
775 | };
776 |
777 | /**
778 | * Clear cache of specified type ('response' or 'view') and optionally a single key
779 | * To clear all cache, pass no parameters
780 | *
781 | * @param type
782 | * @param key
783 | * @returns {Payload}
784 | */
785 | this.clearCache = function(type, key) {
786 | if (type === undefined) {
787 | _cache = {
788 | response: {}
789 | };
790 | } else if (type === 'response') {
791 | if (key === undefined) {
792 | _cache.response = {};
793 | } else {
794 | delete _cache.response[key];
795 | }
796 | } else {
797 | return _error('clearCache() - Incorrect type defined');
798 | }
799 | return _this;
800 | };
801 |
802 | /**
803 | * Register a component by supplying the name (str) and options (obj)
804 | *
805 | * @param name
806 | * @param options
807 | * @returns {Payload}
808 | */
809 | this.registerComponent = function(name, options) {
810 | var i;
811 | if (_components[name] !== undefined) {
812 | return _error('registerComponent() - "' + name + '" component already exists');
813 | }
814 | if (options === undefined) {
815 | return _error('registerComponent() - "' + name + '" component options not defined');
816 | }
817 | _components[name] = options;
818 | for (i = 0; i < _payloadEvents.length; i++) {
819 | if (options[_payloadEvents[i]]) {
820 | _sub(_payloadEvents[i] + '.' + name, options[_payloadEvents[i]]);
821 | }
822 | }
823 | _pub('init.' + name);
824 | return _this;
825 | };
826 |
827 | /**
828 | * Unregister a previously registered component by supplying the name (str)
829 | *
830 | * @param name
831 | * @returns {Payload}
832 | */
833 | this.unregisterComponent = function(name) {
834 | if (_components[name] === undefined) {
835 | return _error('unregisterComponent() - "' + name + '" component does not exist');
836 | }
837 | _unsub('.' + name);
838 | delete _components[name];
839 | return _this;
840 | };
841 |
842 | };
843 |
844 | return new Payload();
845 |
846 | }));
--------------------------------------------------------------------------------