├── .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 | 
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 |
65 |
66 |
67 | Fetching Users List. Please Wait...
68 |
69 |
70 | Oops. Somthing Went Wrong :(
71 |
72 |
73 |
74 | -
75 | {{ user.name }}
76 |
77 |
78 |
79 |
80 |
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 |
146 |
147 |
148 | Fetching Users List. Please Wait...
149 |
150 |
151 | Oops. Somthing Went Wrong :(
152 |
153 |
154 |
155 | -
156 | {{ user.name }}
157 |
158 |
159 |
160 |
161 |
162 |
163 |
197 |
198 |
199 |
200 | - Status: {$state}
201 | - Data: {$data}
202 | - Error: {$error}
203 |
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 |
--------------------------------------------------------------------------------