├── .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 | [![Build Status](https://travis-ci.org/pklauzinski/payload.svg?branch=master)](https://travis-ci.org/payloadjs/payload) 4 | [![npm version](https://img.shields.io/npm/v/payloadjs.svg)](https://www.npmjs.com/package/payloadjs) 5 | [![Bower version](https://img.shields.io/bower/v/payloadjs.svg)](https://github.com/payloadjs/payload) 6 | [![MIT Licensed](https://img.shields.io/badge/license-MIT-blue.svg)](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 | })); --------------------------------------------------------------------------------