├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .prettierrc ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build.sh ├── commitlint.config.js ├── docs └── images │ ├── after.png │ └── before.png ├── package-lock.json ├── package.json └── src ├── pure.js ├── svelte.js ├── utils ├── debounce.js └── typeCheck.js └── vue ├── asyncAction.js ├── index.js └── plugin.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: 'babel-eslint', 3 | env: { 4 | browser: true, 5 | es6: true, 6 | }, 7 | parserOptions: { 8 | ecmaVersion: 6, 9 | sourceType: 'module', 10 | }, 11 | extends: ['airbnb-base', 'prettier'], 12 | rules: { 13 | 'no-restricted-syntax': [ 14 | 'error', 15 | { 16 | selector: 'ForInStatement', 17 | message: 18 | 'for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array.', 19 | }, 20 | { 21 | selector: 'LabeledStatement', 22 | message: 23 | 'Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.', 24 | }, 25 | { 26 | selector: 'WithStatement', 27 | message: 28 | '`with` is disallowed in strict mode because it makes code impossible to predict and optimize.', 29 | }, 30 | ], 31 | 'func-names': ['off'], 32 | 'import/no-extraneous-dependencies': ['error', { devDependencies: true }], 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | /logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | dist 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # IDE / Editor 61 | .idea 62 | 63 | # macOS 64 | .DS_Store 65 | 66 | # Vim swap files 67 | *.swp -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | build.sh 2 | commitlint.config.js 3 | .prettierrc 4 | .gitignore -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 11 | build. 12 | 2. Update the README.md with details of changes to the interface, this includes new environment 13 | variables, exposed ports, useful file locations and container parameters. 14 | 3. Increase the version numbers in any examples files and the README.md to the new version that this 15 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 16 | 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you 17 | do not have permission to do that, you may request the second reviewer to merge it for you. 18 | 19 | ## Code of Conduct 20 | 21 | ### Our Pledge 22 | 23 | In the interest of fostering an open and welcoming environment, we as 24 | contributors and maintainers pledge to making participation in our project and 25 | our community a harassment-free experience for everyone, regardless of age, body 26 | size, disability, ethnicity, gender identity and expression, level of experience, 27 | nationality, personal appearance, race, religion, or sexual identity and 28 | orientation. 29 | 30 | ### Our Standards 31 | 32 | Examples of behavior that contributes to creating a positive environment 33 | include: 34 | 35 | * Using welcoming and inclusive language 36 | * Being respectful of differing viewpoints and experiences 37 | * Gracefully accepting constructive criticism 38 | * Focusing on what is best for the community 39 | * Showing empathy towards other community members 40 | 41 | Examples of unacceptable behavior by participants include: 42 | 43 | * The use of sexualized language or imagery and unwelcome sexual attention or 44 | advances 45 | * Trolling, insulting/derogatory comments, and personal or political attacks 46 | * Public or private harassment 47 | * Publishing others' private information, such as a physical or electronic 48 | address, without explicit permission 49 | * Other conduct which could reasonably be considered inappropriate in a 50 | professional setting 51 | 52 | ### Our Responsibilities 53 | 54 | Project maintainers are responsible for clarifying the standards of acceptable 55 | behavior and are expected to take appropriate and fair corrective action in 56 | response to any instances of unacceptable behavior. 57 | 58 | Project maintainers have the right and responsibility to remove, edit, or 59 | reject comments, commits, code, wiki edits, issues, and other contributions 60 | that are not aligned to this Code of Conduct, or to ban temporarily or 61 | permanently any contributor for other behaviors that they deem inappropriate, 62 | threatening, offensive, or harmful. 63 | 64 | ### Scope 65 | 66 | This Code of Conduct applies both within project spaces and in public spaces 67 | when an individual is representing the project or its community. Examples of 68 | representing a project or community include using an official project e-mail 69 | address, posting via an official social media account, or acting as an appointed 70 | representative at an online or offline event. Representation of a project may be 71 | further defined and clarified by project maintainers. 72 | 73 | ### Enforcement 74 | 75 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 76 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All 77 | complaints will be reviewed and investigated and will result in a response that 78 | is deemed necessary and appropriate to the circumstances. The project team is 79 | obligated to maintain confidentiality with regard to the reporter of an incident. 80 | Further details of specific enforcement policies may be posted separately. 81 | 82 | Project maintainers who do not follow or enforce the Code of Conduct in good 83 | faith may face temporary or permanent repercussions as determined by other 84 | members of the project's leadership. 85 | 86 | ### Attribution 87 | 88 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 89 | available at [http://contributor-covenant.org/version/1/4][version] 90 | 91 | [homepage]: http://contributor-covenant.org 92 | [version]: http://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-present CafeBazaar Front-End Chapter Team 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | ## Overview 6 | 7 | Handling async actions(like API calls) is so tedious. Showing loading state and handling options like debouncing needs a lot of code duplications. 8 | 9 | Async-Actions proposes a more efficient way of handling those actions without code duplications. 10 | 11 | ![Async Action Before/After Comparison](https://user-images.githubusercontent.com/2771377/96008074-0a6ab200-0e4c-11eb-8440-90026e4cf449.png) 12 | 13 | ## How It Works 14 | 15 | Actions are just simple functions. Async-Actions adds `state`, `error`, `pending` and `data` properties to your functions and dynamically updates these properties. 16 | 17 | #### Action lifecycle and possible values of the `state` property 18 | 19 | | Value | Description | 20 | | ------------ | ----------------------------------------------------------------------------------------------------- | 21 | | notInitiated | Action has not been called yet. | 22 | | pending | Action has been called, but it has not been completed yet. | 23 | | fulfilled | Action has been completed successfully, and the result value is accessible using the `data` property. | 24 | | rejected | Action has been rejected with an error which is accessible using `error` property. | 25 | 26 | For checking if an action is in the **pending** status or not, you can also use the `pending` property on the action, which is more convenient. 27 | 28 | ## Installation 29 | 30 | You can install Async-Actions with NPM or Yarn. 31 | 32 | ```bash 33 | npm install @cafebazaar/async-actions --save 34 | ``` 35 | 36 | or 37 | 38 | ```bash 39 | yarn add @cafebazaar/async-actions 40 | ``` 41 | 42 | ## Usage 43 | 44 | You can use Async-Actions in [pure JS](#pure-js). Also there are built in extensions for [Vue.js](#vuejs) and [Svelte](#svelte). 45 | 46 | ### Vue.js 47 | 48 | `Vue.observable` provided by default as the observable function in the Vue version, and you don't need to pass it. There are two ways to use Async-Actions in a Vue.js project. 49 | 50 | #### 1. Define actions in component options 51 | 52 | For declaring async-actions in this way, you need to import the plugin and `use` it as a Vue plugin to enable the functionality globally on all components. 53 | 54 | ```javascript 55 | import Vue from 'vue'; 56 | import AsyncActions from '@cafebazaar/async-actions/vue'; 57 | 58 | Vue.use(AsyncActions); 59 | ``` 60 | 61 | Then, you can define async-actions in all components using `asyncActions` property. 62 | 63 | ```javascript 64 | 81 | 82 | 97 | ``` 98 | 99 | The List of all options is available [here](#options). 100 | 101 | If an action does not need any options, you can define it as a function for the sake of simplicity. 102 | 103 | ```javascript 104 | 116 | ``` 117 | 118 | #### Options 119 | 120 | | Property | Description | type | Required | Default | 121 | | ----------- | ----------------------------------------------------------------------- | -------- | -------- | ------- | 122 | | handler | action's handler | function | true | | 123 | | immediate | determines handler function should be called immediately after creation | boolean | false | false | 124 | | debounce | debounce time in miliseconds | number | false | 0 | 125 | | initialData | initial value of `data` property of action | any | false | null | 126 | 127 | #### 2. Create asyncActions outside of components 128 | 129 | In this way, you can create asyncActions anywhere and use them as regular functions. 130 | 131 | ```javascript 132 | // usersActions.js 133 | 134 | import { asyncAction } from '@cafebazaar/async-actions/vue'; 135 | import { someApiCall } from './api'; 136 | 137 | export const getUsers = asyncAction(() => someApiCall(), { 138 | initialData: [], 139 | }); 140 | ``` 141 | 142 | And after that, you can import and use it inside Vue components: 143 | 144 | ```javascript 145 | 162 | 163 | 197 | 198 |
199 | 204 |
205 | ``` 206 | 207 | The List of all options is available [here](#options). 208 | 209 | You can use asyncAction outside of svelte file and import it and use it directly inside DOM. 210 | 211 | ### Pure JS 212 | 213 | You can define an async-action using `asyncAction` method which gets a handler function and configuration options as its parameters. When using the pure version, you must provide an observable function which used for updating action properties. 214 | 215 | ```javascript 216 | import { asyncAction } from '@cafebazaar/async-actions/pure'; 217 | import customObservable from 'utils/observable'; 218 | 219 | const myAsyncAction = asyncAction( 220 | Promise.resolve('Hello'), 221 | options, 222 | customObservable 223 | ); 224 | ``` 225 | 226 | List of all options are available [here](#options). 227 | 228 | ## License 229 | 230 | [MIT](https://github.com/cafebazaar/async-actions/blob/master/LICENSE) 231 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | microbundle build -i src/pure.js -o index.js --compress --name async-actions --no-sourcemap 4 | microbundle build -i src/svelte.js -o svelte.js --compress --name async-actions --no-sourcemap 5 | microbundle build -i src/vue/index.js -o vue.js --compress --name async-actions --no-sourcemap -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-angular'], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/images/after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cafebazaar/async-actions/74e0710ed08abb91af30d614c3575f30d2007c7b/docs/images/after.png -------------------------------------------------------------------------------- /docs/images/before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cafebazaar/async-actions/74e0710ed08abb91af30d614c3575f30d2007c7b/docs/images/before.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cafebazaar/async-actions", 3 | "version": "0.4.4", 4 | "description": "", 5 | "author": "Bazaar Front-End Chapter", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/cafebazaar/async-actions.git" 10 | }, 11 | "keywords": [ 12 | "vue", 13 | "svelte", 14 | "async", 15 | "promise", 16 | "loading" 17 | ], 18 | "scripts": { 19 | "lint": "eslint --cache --ignore-path .gitignore .", 20 | "build": "./build.sh" 21 | }, 22 | "husky": { 23 | "hooks": { 24 | "pre-commit": "lint-staged", 25 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 26 | } 27 | }, 28 | "lint-staged": { 29 | "*.js": "yarn lint --fix" 30 | }, 31 | "devDependencies": { 32 | "@commitlint/config-angular": "^9.1.1", 33 | "babel-eslint": "^10.1.0", 34 | "commitlint": "^9.1.0", 35 | "eslint": "7.2.0", 36 | "eslint-config-airbnb-base": "14.2.0", 37 | "eslint-config-prettier": "^6.11.0", 38 | "eslint-plugin-import": "^2.21.2", 39 | "husky": "^4.2.5", 40 | "lint-staged": "^10.2.11", 41 | "microbundle": "^0.12.3", 42 | "prettier": "^2.0.5" 43 | }, 44 | "peerDependencies": { 45 | "svelte": "^3.24.0", 46 | "vue": "^2.6.12" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/pure.js: -------------------------------------------------------------------------------- 1 | import debounceFn from './utils/debounce'; 2 | import { isNumber } from './utils/typeCheck'; 3 | 4 | export function asyncAction( 5 | fn, 6 | { initialData = null, debounce = 0, immediate = false, ctx = null } = {}, 7 | observableFn 8 | ) { 9 | if (debounce && (!isNumber(debounce) || debounce < 0)) { 10 | throw new Error('debounce option must be a positive number'); 11 | } 12 | 13 | const stateObject = observableFn({ 14 | state: 'notInitiated', 15 | data: initialData, 16 | error: null, 17 | }); 18 | 19 | let rtFn = function (...args) { 20 | stateObject.state = 'pending'; 21 | stateObject.error = null; 22 | stateObject.data = null; 23 | 24 | return Promise.resolve(fn.apply(ctx || this, args)) 25 | .then((res) => { 26 | stateObject.state = 'fulfilled'; 27 | stateObject.data = res; 28 | return res; 29 | }) 30 | .catch((err) => { 31 | stateObject.state = 'rejected'; 32 | stateObject.error = err; 33 | throw err; 34 | }); 35 | }; 36 | 37 | if (debounce) { 38 | rtFn = debounceFn(rtFn, debounce); 39 | } 40 | 41 | Object.defineProperties(rtFn, { 42 | state: { 43 | get() { 44 | return stateObject.state; 45 | }, 46 | }, 47 | error: { 48 | get() { 49 | return stateObject.error; 50 | }, 51 | }, 52 | data: { 53 | get() { 54 | return stateObject.data; 55 | }, 56 | }, 57 | pending: { 58 | get() { 59 | return stateObject.state === 'pending'; 60 | }, 61 | }, 62 | }); 63 | 64 | if (immediate) { 65 | rtFn.call(this); 66 | } 67 | 68 | return rtFn; 69 | } 70 | 71 | export function asyncActionCreator(options, observableFn) { 72 | return (fn, callSideOptions) => 73 | asyncAction(fn, { 74 | ...options, 75 | ...callSideOptions, 76 | }, observableFn); 77 | } 78 | -------------------------------------------------------------------------------- /src/svelte.js: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store'; 2 | import { asyncAction } from './pure'; 3 | 4 | export default function(fn, options){ 5 | return asyncAction(fn, options, (stateObject)=>{ 6 | 7 | const newStateObject = {}; 8 | 9 | Object.keys(stateObject).forEach((key)=>{ 10 | const observedValue = writable(stateObject[key]); 11 | Object.defineProperty(newStateObject, key, { 12 | get(){ 13 | return observedValue; 14 | }, 15 | set(val){ 16 | observedValue.update(() => val) 17 | } 18 | }) 19 | }) 20 | 21 | return newStateObject; 22 | }); 23 | } -------------------------------------------------------------------------------- /src/utils/debounce.js: -------------------------------------------------------------------------------- 1 | export default function (fn, wait) { 2 | let timerId; 3 | 4 | return function (...args) { 5 | if (timerId) { 6 | clearTimeout(timerId); 7 | } 8 | 9 | timerId = setTimeout(fn.bind(this, ...args), wait); 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/typeCheck.js: -------------------------------------------------------------------------------- 1 | export function isNumber(num) { 2 | return ( 3 | (typeof num === 'number' && !Number.isNaN(num)) || num instanceof Number 4 | ); 5 | } 6 | 7 | export function isFunction(val) { 8 | return ( 9 | Object.prototype.toString.call(val) === '[object Function]' || 10 | typeof val === 'function' || 11 | val instanceof Function 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/vue/asyncAction.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { asyncAction } from '../pure'; 3 | 4 | export default function (fn, options) { 5 | return asyncAction(fn, options, Vue.observable); 6 | } 7 | -------------------------------------------------------------------------------- /src/vue/index.js: -------------------------------------------------------------------------------- 1 | export { default as asyncAction } from './asyncAction'; 2 | export { default } from './plugin'; 3 | -------------------------------------------------------------------------------- /src/vue/plugin.js: -------------------------------------------------------------------------------- 1 | import { isFunction } from '../utils/typeCheck'; 2 | import asyncAction from './asyncAction'; 3 | 4 | export default { 5 | install(Vue) { 6 | Vue.mixin({ 7 | beforeCreate() { 8 | if (!this.$options.asyncActions) { 9 | return; 10 | } 11 | 12 | Object.keys(this.$options.asyncActions).forEach((action) => { 13 | const configs = this.$options.asyncActions[action]; 14 | 15 | if (isFunction(configs)) { 16 | this[action] = asyncAction(configs, { ctx: this }); 17 | } else { 18 | const { handler, ...options } = configs; 19 | 20 | if (!isFunction(handler)) { 21 | throw new Error('[async-actions] handler must be a function'); 22 | } 23 | 24 | this[action] = asyncAction(handler, { 25 | ctx: this, 26 | ...options, 27 | }); 28 | } 29 | }); 30 | }, 31 | }); 32 | }, 33 | }; 34 | --------------------------------------------------------------------------------