├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmignore ├── README.md ├── circle.yml ├── docs ├── API.md ├── COMPLEX_EXAMPLE.md └── SIMPLE_EXAMPLE.md ├── package.json ├── src ├── createClientResolver.js ├── createHooks.js ├── createResolver.js ├── helpers.js ├── index.js ├── locationStorage.js └── resolve.js └── test ├── createHooks.js ├── createResolver.js ├── helpers.js ├── locationStorage.js └── resolve.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets":["es2015","react"], 3 | "plugins":[ 4 | "transform-class-properties", 5 | "transform-object-rest-spread" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs. 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # We recommend you to keep these unchanged. 10 | charset = utf-8 11 | end_of_line = lf 12 | indent_size = 2 13 | indent_style = space 14 | insert_final_newline = true 15 | trim_trailing_whitespace = true 16 | 17 | [package.json] 18 | indent_style = space 19 | indent_size = 2 20 | 21 | [*.md] 22 | trim_trailing_whitespace = false 23 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-airbnb", 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "mocha": true 7 | }, 8 | "ecmaFeatures": { 9 | "destructing": true, 10 | "classes": true 11 | }, 12 | "rules": { 13 | "import/default": 0, 14 | "import/no-duplicates": 0, 15 | "import/named": 0, 16 | "import/namespace": 0, 17 | "import/no-unresolved": 0, 18 | "import/no-named-as-default": 2, 19 | "comma-dangle": 0, 20 | // not sure why airbnb turned this on. gross! 21 | "indent": [2, 2, {"SwitchCase": 1}], 22 | "no-console": 0, 23 | "no-alert": 0, 24 | "max-len":[2,140] 25 | }, 26 | "plugins": [ 27 | "import" 28 | ], 29 | "parser": "babel-eslint" 30 | } 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib/ 3 | npm-debug.log 4 | .idea/ 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecated - REASYNC 2 | 3 | Library for connecting React components to async actions like fetching 4 | 5 | ## Warning 6 | 7 | The package is currently in beta version. Use with own risk. It's used in production on own closed-source app. 8 | 9 | #### Docs 10 | 11 | - [Installation & Simple Example](https://github.com/svrcekmichal/reasync/blob/master/docs/SIMPLE_EXAMPLE.md) 12 | - [Complex Example](https://github.com/svrcekmichal/reasync/blob/master/docs/COMPLEX_EXAMPLE.md) 13 | - [API](https://github.com/svrcekmichal/reasync/blob/master/docs/API.md) 14 | 15 | ## Why I need this? 16 | 17 | Let's say we have universal application. We want to fetch some data on server, before server render. 18 | We also want to do some work only on server, before render and we want to track server action to let's say 19 | google analytics after render. 20 | 21 | On the client, we hydrate app with data from server, but if server fail we want to fetch data from client. 22 | After data are fetched we want to start rendering, then fetch some data after render, you want do some action 23 | only on client and when everything is done we want to track some actions to analytics too. We want to track 24 | actions even if something before failed. We want to show user some loader before transition is done. 25 | 26 | You can configure lifecycle of this events with `reasync`. 27 | 28 | ## Why I have created this package? 29 | 30 | Long time ago, few people started using react-redux project for managing routing state in redux. In those times, idea of prefetching 31 | and deferred fetching was used making router transition from one route to another more sophisticated. React-redux package was awesome, 32 | but it stared to get bloated and handling to much and it was also complicated to setup. 33 | 34 | People started to migrate to react-router-redux, which was much more simplified, but it was not possible to easily create react-redux transition functionality. 35 | I found it awesome to be able to delay transition and to fetch data or do any other async work when i want to. 36 | 37 | This package is not about how to fetch data, query some storage or another async actions. It's about way to tell, when i need to execute that async action. 38 | Do I need some data before server start to render? Do I need to track something only on server? Or load some storage only on client? Do I need them before 39 | page is shown to client? And what should be done before transition, what after? 40 | 41 | ## Used in 42 | 43 | Package was extracted from non-oss project, but it is used in my boilerplate: 44 | 45 | - [svrcekmichal/universal-react](https://github.com/svrcekmichal/universal-react) 46 | - [svrcekmichal/react-production-starter](https://github.com/svrcekmichal/react-production-starter) fork of awesome [@jaredpalmer](https://twitter.com/jaredpalmer) boilerplate [React Production Starter](https://github.com/jaredpalmer/react-production-starter) 47 | 48 | ## Related projects 49 | 50 | - [React Resolver](https://github.com/ericclemmons/react-resolver) by [@ericclemmons](https://twitter.com/ericclemmons) 51 | - [React Transmit](https://github.com/RickWong/react-transmit) by [@rygu](https://twitter.com/rygu) 52 | - [AsyncProps for React Router](https://github.com/rackt/async-props) by [@ryanflorence](https://twitter.com/ryanflorence) 53 | - [React Async](https://github.com/andreypopp/react-async) by [@andreypopp](https://twitter.com/andreypopp) 54 | - [Redial](https://github.com/markdalgleish/redial) by [@markdalgleish](https://twitter.com/markdalgleish) 55 | 56 | ## Future 57 | 58 | There's so much to do, like write tests, simplify usage, cleanup the mess 59 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 4.0 4 | environment: 5 | CONTINUOUS_INTEGRATION: true 6 | 7 | dependencies: 8 | cache_directories: 9 | - node_modules 10 | override: 11 | - npm prune && npm install 12 | -------------------------------------------------------------------------------- /docs/API.md: -------------------------------------------------------------------------------- 1 | # REASYNC - API 2 | 3 | #### Docs 4 | 5 | - [Home](https://github.com/svrcekmichal/reasync) 6 | - [Installation & Simple Example](https://github.com/svrcekmichal/reasync/blob/master/docs/SIMPLE_EXAMPLE.md) 7 | - [Complex Example](https://github.com/svrcekmichal/reasync/blob/master/docs/COMPLEX_EXAMPLE.md) 8 | - [Api](https://github.com/svrcekmichal/reasync/blob/master/docs/API.md) 9 | 10 | 11 | ## resolve(hookName:String, toResolve:{(...attributes):Promise}):void 12 | 13 | resolve function is used for decorating of components with data needed. First argument is name of the decoration, 14 | second is function which will be triggered when your app need it. Attributes received in toResolve functions are 15 | location, params, components and all custom attributes defined in `createResolve`, respectively `createClientResolver` 16 | 17 | ```javascript 18 | import { resolve } from 'reasync'; 19 | import { Component } from 'react'; 20 | 21 | const toResolve = ({location, analytics}) => analytics.push(location); 22 | 23 | //analytics is custom object defined by user 24 | @resolve('MY_ANALYTICS_EVENT', toResolve) 25 | class SomeComponent extends Component { 26 | ... 27 | ``` 28 | 29 | If you want to use es5 syntax, you can use `resolve` as function. Just don't forget to export it's result 30 | 31 | ```javascript 32 | export const someComponentWithResolve = resolve('MY_ANALYTICS_EVENT', toResolve)(SomeComponent); 33 | ``` 34 | 35 | ## preResolve(toResolve:{(...attributes):Promise}):void 36 | ## deferResolve(toResolve:{(...attributes):Promise}):void 37 | 38 | preResolve and deferResolve are two out of box created resolve types. 39 | ```javascript 40 | export const preResolve = resolve.bind(undefined, PRE_RESOLVE_HOOK); 41 | export const deferResolve = resolve.bind(undefined, DEFER_RESOLVE_HOOK); 42 | ``` 43 | 44 | Those are two basic resolve functions, but you can create own. Resolve example reworked: 45 | ```javascript 46 | export const myAnalyticsResolve = resolve.bind(undefined, 'MY_ANALYTICS_EVENT'); 47 | 48 | //then in another file you import myAnalyticsResolve 49 | const toResolve = ({location, analytics}) => analytics.push(location); 50 | @myAnalyticsResolve(toResolve) 51 | class SomeComponent extends Component { 52 | ... 53 | ``` 54 | 55 | ## createGlobalHook(action:{(...attributes):Promise}[, hookOptions:HookOptions]):HookObject 56 | 57 | Used for creating hook object which will fire global actions. This can be used for showing loader bar or some actions, that 58 | must execute on every route transition. Attributes received in action parameter are 59 | location, params, components and all custom attributes defined in `createResolve`, respectively `createClientResolver`. 60 | 61 | ## createLocalHook(hookName:String[, hookOptions:HookOptions]):HookObject 62 | 63 | Used for creating hook object which will fire component resolve actions. hookName attribute must be 64 | same as in resolve function defined above. 65 | 66 | ## createTransitionHook([hookOptions:HookOptions]):HookObject 67 | 68 | Special hook used for defining when should transition or render (on server) execute. 69 | 70 | ## HookOptions 71 | ### stopOnException = true 72 | 73 | If any promise returned from globalHook action or localHook component action will be rejected, no more hooks layer will be triggered. 74 | Only exception are hooks which have `executeIfPreviousFailed` set to true. 75 | 76 | ### executeIfPreviousFailed = false 77 | 78 | If any previous hooks rejected, only hooks defined with `executeIfPreviousFailed` to true will be fired. 79 | 80 | ## createResolver():Resolver 81 | 82 | Creates empty resolver object. It's used for creating hook layers which will be triggered parallelly and chaining them to sequence. 83 | Resolver is meant to be used on the server. On the client, you can use `createClientResolver`, which wrap createResolver and adds more 84 | functionality around transitioning 85 | 86 | ## Resolver.addHooks(...attributes:String|Function|HookObject):Object 87 | 88 | Creates hook layer for resolver. All attributes in this function will be triggered at the same time, and if any of them return promise, triggering 89 | of next layer hooks will be delayed. If promise of hook, which have `stopOnException` set to `true` will reject, every next hook layer will trigger only hooks 90 | with `executeIfPreviousFailed` set to `true`. 91 | 92 | `addHooks` accepts all three of hook types, but you can insert string or function too. String will be transformed to `createLocalHook(name)` and function to 93 | `createGlobalHook(action)` with options set to default values. 94 | 95 | Object returned from addHooks have two methods, `addHooks` and `setErroHandler` because we suggest to write them in chain for better readability. 96 | 97 | ## Resolver.setErrorHandler(errorHandler:{(err):void}) 98 | 99 | If any of the hooks failed, you can create custom error handler. Error handler accept fucntion which have error as it's first argument. 100 | 101 | ## Resolver.triggerHooks(components:ReactComponent[], attributes:Object[, transition:Function]):Promise 102 | 103 | Function for triggering transition. It accepts react components from `react-router`, all attributes you want to pass to resolve functions and global hook's actions. 104 | We recommend to insert `location`, `params` from renderProps. If you are using `redux`, you can insert getState and dispatch, but it's up to you what you need and what you don't. 105 | 106 | Only single notice, make sure that attributes inserted to all resolver are same. Client resolver is inserting `location` and `params` in it's implementation. 107 | 108 | ## createClientResolver(history:History, routes: Routes, initLocation:Location[, attributes:Object]):ClientResolver 109 | 110 | Used for creating client resolver object. First attribute must be history object, from `react-router` or dirrectly fro m `history` package. Second attributes are routes used in router 111 | and third argument is object with you custom attributes. You can read more about attributes in `Resolver.triggerHooks` method. 112 | 113 | Client resolver will trigger hooks on every route transition which change `location.pathname` or `location.search`. 114 | You can change this with `setTransitionRule` or trigger hooks with `forceTrigger`. 115 | 116 | ## ClientResolver.addHooks(...attributes:String|Function|HookObject):Object 117 | 118 | Same as `Resolver.addHooks` 119 | 120 | ## ClientResolver.setErrorHandler(errorHandler:{(err):void}):void 121 | 122 | Same as `Resolver.setErrorHandler` 123 | 124 | ## ClientResolver.forceTrigger():void 125 | 126 | If you want to trigger all hooks, withou user changing location, you can call this method and it will run all hooks as defined. 127 | 128 | ## ClientResolver.setTransitionRule(rule:{(oldLocation:Location,newLocation:Location):bool}):void 129 | 130 | If you want to change, which transition is transition and which doesn't, use this. By default if oldLocation and newLoation don't have same 131 | pathname and search, it's considered to be transition. 132 | -------------------------------------------------------------------------------- /docs/COMPLEX_EXAMPLE.md: -------------------------------------------------------------------------------- 1 | # REASYNC - Complex Example 2 | 3 | #### Docs 4 | 5 | - [Home](https://github.com/svrcekmichal/reasync) 6 | - [Installation & Simple Example](https://github.com/svrcekmichal/reasync/blob/master/docs/SIMPLE_EXAMPLE.md) 7 | - [API](https://github.com/svrcekmichal/reasync/blob/master/docs/API.md) 8 | 9 | # TO BE DONE -------------------------------------------------------------------------------- /docs/SIMPLE_EXAMPLE.md: -------------------------------------------------------------------------------- 1 | # REASYNC - Installation & Simple Example 2 | 3 | #### Docs 4 | 5 | - [Home](https://github.com/svrcekmichal/reasync) 6 | - [Complex Example](https://github.com/svrcekmichal/reasync/blob/master/docs/COMPLEX_EXAMPLE.md) 7 | - [API](https://github.com/svrcekmichal/reasync/blob/master/docs/API.md) 8 | 9 | ## Installation 10 | 11 | ```bash 12 | npm i -S reasync 13 | ``` 14 | 15 | ## Simple Example 16 | 17 | ### What we are going to do? 18 | 19 | We have universal app, we want to fetch some data or create some asyn actions when we need. 20 | 21 | Let's say, on client we want fired actions before transition to new page, 22 | and after user transition to new page we want to fetch more data. We also want to track something to our own google analytics implementation. 23 | 24 | On server we want to render full page with data which will client fetch before transition and after transition at once. We don't want to track server 25 | to analytics. 26 | 27 | ### How to make client side? 28 | 29 | First, there are 3 hooks we need, one for resolve before, one for resolve after and one for analytics. Reasync has out of the box two types, for before and after resolve 30 | called `preResolve` and `deferResolve`, but the third one we must create. We also need transitionHook to tell reasyn when should transition happen. Transition hook can be 31 | created with `createTransitionHook` 32 | 33 | ```javascript 34 | //reasync-setup.js 35 | 36 | import { PRE_RESOLVE_HOOK, DEFER_RESOLVE_HOOK, resolve, createTransitionHook } from 'reasync'; 37 | export { preResolve, deferResolve} from 'reasync'; //thiw will be used later 38 | 39 | export const MY_ANALYTICS_HOOK = 'myAnalyticsHook'; 40 | export const myAnalyticsResolve = resolve.bind(undefined,MY_ANALYTICS_HOOK); 41 | 42 | ``` 43 | 44 | Now we have all three hooks, we need. So it's time to connect it with clientResolver. Even if something failed we want user to 45 | see new page, but if you want to show old page with some error message just remove `executeIfPreviousFailed` 46 | 47 | ```javascript 48 | //top of the file 49 | import {createClientResolver as _createClientResolver} from 'reasync'; 50 | 51 | //bottom of file 52 | export const createClientResolver = (history, location, initLocation, customAttributes) => { 53 | const resolver = _createClientResolver(history, location, initLocation, customAttributes); 54 | resolver 55 | .addHooks(PRE_RESOLVE_HOOK) //this will fire first 56 | .addHooks(createTransitionHook({executeIfPreviousFailed:true}),DEFER_RESOLVE_HOOK) //this two will fire after resolve, parallelly 57 | .addHooks(MY_ANALYTICS_HOOK); //this will fire when everything is done 58 | 59 | if (__DEVELOPMENT__) { 60 | resolver.setErrorHandler((err) => console.log(err)); //in development we want to log errors 61 | } 62 | return resolver; 63 | } 64 | ``` 65 | 66 | So now when we have client resolver it's time to connect it to our app: 67 | 68 | ```javascript 69 | //client.js 70 | import React from 'react'; 71 | import ReactDOM from 'react-dom'; 72 | import createStore from './redux/createStore'; 73 | import { getRoutes } from './routes'; 74 | import { Provider } from 'react-redux'; 75 | import { Router, browserHistory } from 'react-router'; 76 | import { createClientResolver } from './reasync-setup'; //here we import our reasync setup file 77 | 78 | const { pathname, search, hash } = window.location; 79 | const url = `${pathname}${search}${hash}`; 80 | const location = browserHistory.createLocation(url); 81 | 82 | const store = createStore(browserHistory, window.__data__); 83 | const routes = getRoutes(store); 84 | const mountPoint = document.getElementById('content'); 85 | 86 | const attrs = {getState:store.getState, dispatch:store.dispatch}; 87 | const resolver = createClientResolver(history, routes, location, attrs); //here we hook it to our history and routes 88 | if(!window.__data__) { // if on server something failed and we don't have data, we force trigger 89 | resolver.forceTransition(); 90 | } 91 | 92 | ReactDOM.render( 93 | 94 | 95 | , 96 | mountPoint 97 | ); 98 | 99 | ``` 100 | 101 | ### On server 102 | Differences on the server: 103 | 1. we don't want to trigger transition before deferResolve finish 104 | 2. we don't want to trigger analytics hook 105 | 3. we don't want to render page if something failed, so no `executeIfPreviousFailed` 106 | 107 | ```javascript 108 | //reasync-setup.js 109 | 110 | //top of the file 111 | import {createResolver} from 'reasync'; 112 | 113 | //bottom of the file 114 | export const createServerResolver = () => { 115 | const resolver = createResolver(); 116 | resolver 117 | .addHooks(PRE_RESOLVE_HOOK) //this will fire first 118 | .addHooks(DEFER_RESOLVE_HOOK) //this will fire after PRE_RESOLVE_HOOK, if you don't have dependencies there you can fire them in parallelly 119 | .addHooks(createTransitionHook()); //this will fire when everything is done 120 | 121 | if (__DEVELOPMENT__) { 122 | resolver.setErrorHandler((err) => console.log(err)); //in development we want to log errors 123 | } 124 | return resolver; 125 | } 126 | ``` 127 | 128 | Connecting on the server is really simple: 129 | 130 | ```javascript 131 | //server.js 132 | //import createStore, getRoutes, reducers etc. 133 | import { createServerResolver } from 'reasync-setup'; 134 | 135 | const client = createClient(req.cookies); 136 | const history = createMemoryHistory(req.originalUrl); 137 | const store = createStore(memoryHistory, undefined, client); 138 | const routes = getRoutes(store); 139 | 140 | const resolver = createServerResolver(); //we create our server resolver 141 | 142 | match({ history, routes, location: req.originalUrl }, (error, redirectLocation, renderProps) => { 143 | //if error or redirectLocation do whatever you want 144 | if (renderProps) { 145 | const { components, location, params } = renderProps; 146 | const { getState, dispatch } = store; 147 | const attrs = { location, params, getState, dispatch }; 148 | 149 | return resolver.triggerHooks(components, attrs, () => { 150 | //your render function, everything resolved successfully 151 | }).catch(() => { 152 | //something failed, so try to hydrate on client 153 | }); 154 | } 155 | 156 | // hydrate on client 404 157 | }); 158 | ``` 159 | 160 | Now everything is connected, and you can start decorating your components 161 | 162 | ### Component decorator 163 | 164 | Use one of available decorators: 165 | ```javascript 166 | import {preResolve, deferResolve, myAnalyticsHook} from 'reasync-setup'; 167 | ``` 168 | 169 | And use for decorating of component 170 | 171 | ```javascript 172 | import {preResolve,deferResolve} from 'reasync'; 173 | 174 | const fetchUser = ({dispatch}) => dispatch(fetchUser()); // fetch some user 175 | 176 | const fetchUserProfile = ({getState,dispatch}) => { 177 | if(isUserProfileFetched(getState())){ //check if you really need fetch 178 | return dispatch(fetchUserProfile())); //dispatch action 179 | } 180 | return Promise.resolve(); 181 | } 182 | 183 | @preResolve(fetchUser) 184 | @deferResolve(fetchUserProfile) 185 | @myAnalyticsHook(({location}) => /* do somethign with location */) 186 | export default class App extends Component { 187 | ... 188 | 189 | ``` 190 | 191 | #### Don't want to use decorators? 192 | 193 | You can use same as above, only don't export class, but result of function 194 | ```javascript 195 | export default preResolve(fetchUser)(App); //exported component 196 | ``` 197 | 198 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reasync", 3 | "version": "1.0.0-rc.4", 4 | "description": "Library for connecting react components to async actions like fetching", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "prepublish": "npm run lint && npm run test && npm run build", 8 | "build": "node_modules/.bin/babel ./src -d ./lib --ignore '__tests__'", 9 | "lint": "eslint -c .eslintrc src", 10 | "test": "mocha --compilers js:babel-register", 11 | "test:watch": "npm run test -- --watch" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/svrcekmichal/reasync.git" 16 | }, 17 | "author": "Michal Svrcek ", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/svrcekmichal/reasync/issues" 21 | }, 22 | "homepage": "https://github.com/svrcekmichal/reasync", 23 | "devDependencies": { 24 | "babel-cli": "^6.5.1", 25 | "babel-eslint": "^5.0.0", 26 | "babel-plugin-transform-class-properties": "^6.4.0", 27 | "babel-plugin-transform-object-rest-spread": "^6.3.13", 28 | "babel-preset-es2015": "^6.3.13", 29 | "babel-preset-react": "^6.3.13", 30 | "babel-register": "^6.5.2", 31 | "chai": "^3.5.0", 32 | "eslint": "^2.2.0", 33 | "eslint-config-airbnb": "^6.0.2", 34 | "eslint-plugin-import": "^1.0.0-beta.0", 35 | "eslint-plugin-react": "^4.0.0", 36 | "mocha": "^2.4.5" 37 | }, 38 | "dependencies": { 39 | "react": "^0.14.7", 40 | "react-router": "^2.0.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/createClientResolver.js: -------------------------------------------------------------------------------- 1 | import { match } from 'react-router'; 2 | import { createLocationStorage } from './locationStorage'; 3 | import { createResolver } from './createResolver'; 4 | 5 | export const createClientResolver = (history, routes, initLocation, custom = {}) => { 6 | const resolver = createResolver(); 7 | 8 | const locationStorage = createLocationStorage(initLocation); 9 | 10 | let transitionRule = (lastLocation, newLocation) => 11 | lastLocation.pathname !== newLocation.pathname || 12 | lastLocation.search !== newLocation.search; 13 | 14 | const setTransitionRule = rule => { 15 | transitionRule = rule; 16 | }; 17 | 18 | const isTransition = location => transitionRule(locationStorage.getLastLocation(), location); 19 | 20 | const transition = (location, continueTransition, forced = false) => { 21 | if (!forced && !isTransition(location)) return; 22 | match({ history, location, routes }, (error, redirectLocation, renderProps) => { 23 | if (renderProps) { 24 | const attrs = { ...custom, location: renderProps.location, params: renderProps.params }; 25 | resolver.triggerHooks(renderProps.components, attrs, () => { 26 | continueTransition(); 27 | locationStorage.setNewLocation(location); 28 | }); 29 | } 30 | }); 31 | }; 32 | 33 | history.listenBefore(transition); 34 | 35 | const forceTrigger = () => transition(locationStorage.getLastLocation(), () => {}, true); 36 | 37 | const setErrorHandler = handler => { 38 | resolver.setErrorHandler(handler); 39 | }; 40 | 41 | const addHooks = (...hooks) => { 42 | resolver.addHooks(...hooks); 43 | return { addHooks, setErrorHandler }; 44 | }; 45 | 46 | return { 47 | addHooks, 48 | setErrorHandler, 49 | forceTrigger, 50 | setTransitionRule 51 | }; 52 | }; 53 | -------------------------------------------------------------------------------- /src/createHooks.js: -------------------------------------------------------------------------------- 1 | import { isString, isFunction } from './helpers'; 2 | 3 | const TYPE_LOCAL = 'LOCAL'; 4 | const TYPE_GLOBAL = 'GLOBAL'; 5 | const TYPE_TRANSITION = 'TRANSITION'; 6 | 7 | const createHook = (type, action, { stopOnException = true, executeIfPreviousFailed = false } = {}) => ({ 8 | type, 9 | action, 10 | stopOnException, 11 | executeIfPreviousFailed 12 | }); 13 | 14 | export const createLocalHook = (name, options) => { 15 | if (!isString(name)) { 16 | throw new Error('Local hook name must be string'); 17 | } 18 | return createHook(TYPE_LOCAL, name, options); 19 | }; 20 | 21 | export const createGlobalHook = (callback, options) => { 22 | if (!isFunction(callback)) { 23 | throw new Error('Global hook callback must be function'); 24 | } 25 | return createHook(TYPE_GLOBAL, callback, options); 26 | }; 27 | 28 | export const createTransitionHook = (options) => createHook(TYPE_TRANSITION, null, options); 29 | 30 | export const isLocalHook = hook => hook.type === TYPE_LOCAL && isString(hook.action); 31 | 32 | export const isGlobalHook = hook => hook.type === TYPE_GLOBAL && isFunction(hook.action); 33 | 34 | export const isTransitionHook = hook => hook.type === TYPE_TRANSITION; 35 | 36 | export const createHookObject = (action) => { 37 | if (isFunction(action)) { 38 | return createGlobalHook(action); 39 | } else if (isString(action)) { 40 | return createLocalHook(action); 41 | } else if (isLocalHook(action) || isGlobalHook(action) || isTransitionHook(action)) { 42 | return action; 43 | } 44 | throw new Error('Invalid argument received. Must be hook object, string or function.'); 45 | }; 46 | -------------------------------------------------------------------------------- /src/createResolver.js: -------------------------------------------------------------------------------- 1 | import { 2 | isLocalHook, 3 | isGlobalHook, 4 | isTransitionHook, 5 | createHookObject 6 | } from './createHooks'; 7 | 8 | import { 9 | getDependencies 10 | } from './helpers'; 11 | 12 | export const createResolver = () => { 13 | const registeredHooks = []; 14 | let errorHandler = () => {}; 15 | 16 | const setErrorHandler = handler => { 17 | errorHandler = handler; 18 | }; 19 | 20 | const triggerHooks = (components, attrs, transition = () => {}) => { 21 | let failed = false; 22 | const trigger = registeredHooks.reduce((promise, hookLayer) => promise.then(() => Promise.all(hookLayer 23 | .filter(singleHook => !failed || singleHook.executeIfPreviousFailed) 24 | .map(singleHook => { 25 | let hookAction; 26 | if (isLocalHook(singleHook)) hookAction = getDependencies(components, singleHook.action, attrs); 27 | else if (isGlobalHook(singleHook)) hookAction = singleHook.action(attrs); 28 | else if (isTransitionHook(singleHook)) hookAction = transition(); 29 | else { 30 | console.log('Invalid hook, skipping.', singleHook); 31 | hookAction = Promise.resolve(); 32 | } 33 | if (!singleHook.stopOnException && hookAction && typeof hookAction.catch !== 'undefined') { 34 | hookAction = hookAction.catch(errorHandler); 35 | } 36 | return hookAction; 37 | })).catch((err) => { 38 | failed = true; 39 | errorHandler(err); 40 | }) 41 | ), Promise.resolve()); 42 | return trigger.then(() => failed ? Promise.reject() : Promise.resolve()); 43 | }; 44 | 45 | const addHooks = (...hooks) => { 46 | registeredHooks.push(hooks.map(createHookObject)); 47 | return { addHooks, setErrorHandler }; 48 | }; 49 | 50 | return { addHooks, setErrorHandler, triggerHooks }; 51 | }; 52 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | 2 | export const isFunction = func => typeof func === 'function'; 3 | 4 | export const isString = text => typeof text === 'string' || text instanceof String; 5 | 6 | export const isObject = object => !isFunction(object) && object === Object(object); 7 | 8 | const flattenObjectComponents = components => components.reduce((collector, component) => { 9 | if (isObject(component)) { 10 | for (const key in component) { 11 | if (component.hasOwnProperty(key)) collector.push(component[key]); 12 | } 13 | } else { 14 | collector.push(component); 15 | } 16 | return collector; 17 | }, []); 18 | 19 | export const getDependencies = (components, type, attrs) => { 20 | const toResolve = flattenObjectComponents(components) 21 | .filter(component => component && component.asyncResolve && component.asyncResolve[type]) 22 | .map(component => component.asyncResolve[type]) 23 | .filter(resolve => resolve); 24 | const promises = [].concat.apply([], toResolve) 25 | .map(resolve => Promise.resolve(resolve(attrs))); 26 | return Promise.all(promises); 27 | }; 28 | 29 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { 2 | PRE_RESOLVE_HOOK, 3 | DEFER_RESOLVE_HOOK, 4 | preResolve, 5 | preResolve as pre, 6 | deferResolve, 7 | deferResolve as defer, 8 | resolve 9 | } from './resolve'; 10 | 11 | export { 12 | createResolver 13 | } from './createResolver'; 14 | 15 | export { 16 | createClientResolver 17 | } from './createClientResolver'; 18 | 19 | export { 20 | createGlobalHook, 21 | createLocalHook, 22 | createTransitionHook 23 | } from './createHooks'; 24 | -------------------------------------------------------------------------------- /src/locationStorage.js: -------------------------------------------------------------------------------- 1 | export const createLocationStorage = (location = undefined) => { 2 | let lastLocation = location || { 3 | pathname: undefined, 4 | search: undefined, 5 | query: undefined, 6 | state: undefined, 7 | action: undefined, 8 | }; 9 | 10 | const setNewLocation = newlocation => { 11 | lastLocation = newlocation; 12 | }; 13 | 14 | const getLastLocation = () => lastLocation; 15 | 16 | return { 17 | setNewLocation, 18 | getLastLocation 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /src/resolve.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export const PRE_RESOLVE_HOOK = 'preResolveHook'; 4 | export const DEFER_RESOLVE_HOOK = 'deferResolveHook'; 5 | 6 | export const resolve = (name, toResolve) => WrappedComponent => { 7 | let component; 8 | if (typeof WrappedComponent.asyncResolve === 'undefined') { 9 | component = class AsyncResolve extends Component { 10 | static asyncResolve = Object.create(null); 11 | 12 | render() { 13 | return ; 14 | } 15 | }; 16 | } else { 17 | component = WrappedComponent; 18 | } 19 | 20 | if (typeof component.asyncResolve[name] === 'undefined') { 21 | component.asyncResolve[name] = []; 22 | } 23 | 24 | component.asyncResolve[name].push(toResolve); 25 | return component; 26 | }; 27 | 28 | export const preResolve = resolve.bind(undefined, PRE_RESOLVE_HOOK); 29 | 30 | export const deferResolve = resolve.bind(undefined, DEFER_RESOLVE_HOOK); 31 | -------------------------------------------------------------------------------- /test/createHooks.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | 3 | import { 4 | createLocalHook, 5 | createGlobalHook, 6 | createTransitionHook, 7 | isLocalHook, 8 | isGlobalHook, 9 | isTransitionHook, 10 | createHookObject 11 | } from '../src/createHooks' 12 | 13 | describe('createHooks', () => { 14 | 15 | describe('createLocalHook', () => { 16 | 17 | it('returns localHook', () => { 18 | const localHook = createLocalHook('abc'); 19 | expect(isLocalHook(localHook)).to.be.ok; 20 | expect(isGlobalHook(localHook)).to.not.be.ok; 21 | expect(isTransitionHook(localHook)).to.not.be.ok; 22 | }); 23 | 24 | it('throws if first argument is not a string', () => { 25 | expect(() => createLocalHook(() => {})).to.throw(Error); 26 | }) 27 | 28 | }); 29 | 30 | describe('createGlobalHook', () => { 31 | 32 | it('returns globalHook', () => { 33 | const localHook = createGlobalHook(() => {}); 34 | expect(isLocalHook(localHook)).to.not.be.ok; 35 | expect(isGlobalHook(localHook)).to.be.ok; 36 | expect(isTransitionHook(localHook)).to.not.be.ok; 37 | }); 38 | 39 | it('throws if first argument is not a function', () => { 40 | expect(() => createGlobalHook('abc')).to.throw(Error); 41 | }) 42 | 43 | }); 44 | 45 | describe('createTransitionHook', () => { 46 | it('returns transitionHook', () => { 47 | const localHook = createTransitionHook(); 48 | expect(isLocalHook(localHook)).to.not.be.ok; 49 | expect(isGlobalHook(localHook)).to.not.be.ok; 50 | expect(isTransitionHook(localHook)).to.be.ok; 51 | }); 52 | }); 53 | 54 | const localHook = createLocalHook('abc'); 55 | const globalHook = createGlobalHook(() => {}); 56 | const transitionHook = createTransitionHook(); 57 | const nothing = {}; 58 | 59 | describe('isLocalHook', () => { 60 | 61 | it('successful detect of localHook', () => { 62 | expect(isLocalHook(localHook)).to.be.ok; 63 | expect(isLocalHook(globalHook)).to.be.not.ok; 64 | expect(isLocalHook(transitionHook)).to.not.be.ok; 65 | expect(isLocalHook(nothing)).to.not.be.ok; 66 | }) 67 | 68 | }); 69 | 70 | describe('isGlobalHook', () => { 71 | it('successful detect of globalHook', () => { 72 | expect(isGlobalHook(localHook)).to.not.be.ok; 73 | expect(isGlobalHook(globalHook)).to.be.ok; 74 | expect(isGlobalHook(transitionHook)).to.not.be.ok; 75 | expect(isGlobalHook(nothing)).to.not.be.ok; 76 | }) 77 | }); 78 | 79 | describe('isTransitionHook', () => { 80 | it('successful detect of transitionHook', () => { 81 | expect(isTransitionHook(localHook)).to.be.not.ok; 82 | expect(isTransitionHook(globalHook)).to.be.not.ok; 83 | expect(isTransitionHook(transitionHook)).to.be.ok; 84 | expect(isTransitionHook(nothing)).to.not.be.ok; 85 | }) 86 | }); 87 | 88 | describe('createHookObject', () => { 89 | 90 | it('creates localHook from string', () => { 91 | expect(isLocalHook(createHookObject('abc'))).to.be.ok; 92 | }); 93 | 94 | it('creates globalHook from function', () => { 95 | expect(isGlobalHook(createHookObject(() => {}))).to.be.ok; 96 | }); 97 | 98 | it('return same hook if inserted', () => { 99 | expect(createHookObject(localHook)).to.equal(localHook); 100 | expect(createHookObject(globalHook)).to.equal(globalHook); 101 | expect(createHookObject(transitionHook)).to.equal(transitionHook); 102 | }); 103 | 104 | it('throws if not hook object, string or function inserted', () => { 105 | expect(() => createHookObject({})).to.throw(Error); 106 | }); 107 | 108 | }); 109 | 110 | }); 111 | 112 | -------------------------------------------------------------------------------- /test/createResolver.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | 3 | import { createGlobalHook, createTransitionHook } from '../src/createHooks'; 4 | 5 | import { createResolver } from '../src/createResolver' 6 | 7 | const asyncTest = (done, test) => { 8 | try { 9 | test(); 10 | done(); 11 | } catch (e) { 12 | done(e); 13 | } 14 | }; 15 | 16 | const setTimeoutPromise = (beforeResolve, success = true, time = 2) => () => new Promise((resolve,reject) => setTimeout(() => { 17 | beforeResolve(); 18 | success ? resolve() : reject('Rejected by user'); 19 | },time)); 20 | 21 | describe('createResolver', () => { 22 | 23 | let startTime; 24 | let resolver; 25 | 26 | beforeEach(() => { 27 | startTime = new Date(); 28 | resolver = createResolver(); 29 | }); 30 | 31 | it('trigger hooks in sequence', done => { 32 | let p1Time, p2Time, c2Time; 33 | const p1 = setTimeoutPromise(() => p1Time = new Date()); 34 | const p2 = setTimeoutPromise(() => p2Time = new Date()); 35 | const c2 = () => c2Time = new Date(); 36 | 37 | resolver 38 | .addHooks(p1) 39 | .addHooks(p2,c2); 40 | 41 | resolver.triggerHooks([],{}).then(() => { 42 | asyncTest(done,() => { 43 | expect(p1Time - startTime).to.be.below(p2Time - startTime); 44 | // c2 resolve right after, in some cases it can fire in same millisecond as p1 finish 45 | expect(p1Time - startTime).to.be.most(c2Time - startTime); 46 | }); 47 | }); 48 | 49 | }); 50 | 51 | it('trigger hooks almost parallelly', done => { 52 | let p1Time, p2Time, p3Time, p4Time, c2Time, c3Time; 53 | 54 | const p1 = setTimeoutPromise(() => p1Time = new Date()); 55 | const p2 = setTimeoutPromise(() => p2Time = new Date()); 56 | const p3 = setTimeoutPromise(() => p3Time = new Date()); 57 | const p4 = setTimeoutPromise(() => p4Time = new Date()); 58 | const c2 = () => c2Time = new Date(); 59 | const c3 = () => c3Time = new Date(); 60 | 61 | resolver 62 | .addHooks(p1) 63 | .addHooks(p2, p3, c2, c3) 64 | .addHooks(p4); 65 | 66 | resolver.triggerHooks([],{}).then(() => { 67 | asyncTest(done,() => { 68 | expect(p1Time - startTime).to.be.below(p2Time - startTime); 69 | expect(p1Time - startTime).to.be.below(p3Time - startTime); 70 | expect(p1Time - startTime).to.be.most(c2Time - startTime); 71 | expect(p4Time - startTime).to.be.least(c3Time - startTime); 72 | expect(p4Time - startTime).to.be.above(p2Time - startTime); 73 | expect(p4Time - startTime).to.be.above(p3Time - startTime); 74 | }); 75 | }); 76 | 77 | }); 78 | 79 | it('trigger transition function when transition hook triggered', done => { 80 | let p1Time, p2Time, transitionTime; 81 | const p1 = setTimeoutPromise(() => p1Time = new Date()); 82 | const transition = () => transitionTime = new Date(); 83 | const p2 = setTimeoutPromise(() => p2Time = new Date()); 84 | 85 | resolver 86 | .addHooks(p1) 87 | .addHooks(createTransitionHook()) 88 | .addHooks(p2); 89 | 90 | resolver.triggerHooks([],{},transition).then(() => { 91 | asyncTest(done,() => { 92 | expect(transitionTime).to.be.ok; 93 | expect(p1Time - startTime).to.be.most(transitionTime - startTime); 94 | expect(p2Time - startTime).to.be.least(transitionTime - startTime); 95 | }); 96 | }); 97 | }); 98 | 99 | it('stop on hook layer which rejected promise', done => { 100 | let p1Time, p2Time, p3Time; 101 | const p1 = setTimeoutPromise(() => p1Time = new Date()); 102 | const p2 = setTimeoutPromise(() => p2Time = new Date(), false); 103 | const p3 = setTimeoutPromise(() => p3Time = new Date()); 104 | 105 | resolver 106 | .addHooks(p1) 107 | .addHooks(createGlobalHook(p2, {stopOnException:true})) 108 | .addHooks(p3); 109 | 110 | resolver.triggerHooks([],{}).catch(() => { 111 | asyncTest(done,() => { 112 | expect(p1Time).to.be.ok; 113 | expect(p2Time).to.be.ok; 114 | expect(p3Time).to.not.be.ok; 115 | }); 116 | }); 117 | }); 118 | 119 | it('execute promises which should execute even if previous failed', done => { 120 | let p1Time, p2Time, p3Time, p4Time; 121 | const p1 = setTimeoutPromise(() => p1Time = new Date()); 122 | const p2 = setTimeoutPromise(() => p2Time = new Date(), false); 123 | const p3 = setTimeoutPromise(() => p3Time = new Date()); 124 | const p4 = setTimeoutPromise(() => p4Time = new Date()); 125 | 126 | resolver 127 | .addHooks(p1) 128 | .addHooks(createGlobalHook(p2, {stopOnException:true})) 129 | .addHooks( 130 | p3, 131 | createGlobalHook(p4, {executeIfPreviousFailed: true}) 132 | ); 133 | 134 | resolver.triggerHooks([],{}).catch(() => { 135 | asyncTest(done,() => { 136 | expect(p1Time).to.be.ok; 137 | expect(p2Time).to.be.ok; 138 | expect(p3Time).to.not.be.ok; 139 | expect(p4Time).to.be.ok; 140 | }); 141 | }); 142 | }); 143 | 144 | it('triggerHooks return promise which resolve if everything resolved', done => { 145 | let p1Time, p2Time, successTime, failureTime; 146 | const p1 = setTimeoutPromise(() => p1Time = new Date()); 147 | const p2 = setTimeoutPromise(() => p2Time = new Date()); 148 | 149 | resolver 150 | .addHooks(p1) 151 | .addHooks(p2); 152 | 153 | resolver.triggerHooks([],{}).then( 154 | () => successTime = new Date(), 155 | () => failureTime = new Date() 156 | ).then(() => { 157 | asyncTest(done, () => { 158 | expect(successTime).to.be.ok; 159 | expect(p1Time - startTime).to.be.below(successTime - startTime); 160 | expect(p2Time - startTime).to.be.most(successTime - startTime); 161 | expect(failureTime).to.not.be.ok; 162 | }) 163 | }) 164 | }); 165 | 166 | it('triggerHooks return promise which reject if stopOnException failed', done => { 167 | let p1Time, p2Time, successTime, failureTime; 168 | const p1 = setTimeoutPromise(() => p1Time = new Date()); 169 | const p2 = setTimeoutPromise(() => p2Time = new Date(), false); 170 | 171 | resolver 172 | .addHooks(p1) 173 | .addHooks(createGlobalHook(p2,{stopOnException:true})); 174 | 175 | resolver.triggerHooks([],{}).then( 176 | () => successTime = new Date(), 177 | () => failureTime = new Date() 178 | ).then(() => { 179 | asyncTest(done, () => { 180 | expect(successTime).to.not.be.ok; 181 | expect(failureTime).to.be.ok; 182 | expect(p1Time - startTime).to.be.below(failureTime - startTime); 183 | expect(p2Time - startTime).to.be.most(failureTime - startTime); 184 | }) 185 | }) 186 | }); 187 | 188 | }); 189 | 190 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {resolve} from '../src/resolve'; 3 | import {getDependencies, isFunction, isString } from '../src/helpers'; 4 | 5 | describe('helpers', () => { 6 | 7 | describe('getDependencies', () => { 8 | 9 | it('should receive passed attributes', () => { 10 | const availableAttributes = 'attributes'; 11 | let receivedAttributes = undefined; 12 | 13 | const pre = (attributes) => { 14 | receivedAttributes = attributes; 15 | }; 16 | 17 | const components = [resolve('hookName', pre)({})]; 18 | getDependencies(components, 'hookName', availableAttributes); 19 | expect(availableAttributes).to.equal(receivedAttributes); 20 | }); 21 | 22 | it('should return only wanted resolve', () => { 23 | 24 | let hook1Called = false; 25 | let hook2Called = false; 26 | 27 | const hook1 = (attributes) => hook1Called = true; 28 | const hook2 = (attributes) => hook2Called = true; 29 | const hooked1 = resolve('hook1', hook1)({}); 30 | const hookedAll = resolve('hook2', hook2)(hooked1); 31 | 32 | getDependencies([hookedAll], 'hook1', {}); 33 | 34 | expect(hook1Called).to.be.ok; 35 | expect(hook2Called).to.not.be.ok; 36 | }); 37 | 38 | it('should resolve getComponents/components object map', () => { 39 | 40 | let hook1Count = 0; 41 | let hook2Count = 0; 42 | 43 | const hook1 = (attributes) => hook1Count++; 44 | const hook2 = (attributes) => hook2Count++; 45 | 46 | const hooked = [ 47 | resolve('hook1', hook1)({}), 48 | { 49 | contentA: resolve('hook1', hook1)({}), 50 | contentB: resolve('hook2', hook2)({}) 51 | } 52 | ]; 53 | 54 | getDependencies(hooked, 'hook1', {}); 55 | 56 | expect(hook1Count).to.equal(2); 57 | expect(hook2Count).to.equal(0); 58 | 59 | }) 60 | 61 | 62 | }); 63 | 64 | describe('typeHelpers', () => { 65 | 66 | const bool = true; 67 | const num = 23; 68 | const str = 'abc'; 69 | const undef = undefined; 70 | const typeNull = null; 71 | const arr = []; 72 | const obj = {}; 73 | const func = () => {}; 74 | 75 | it('expect isString to detect only string', () => { 76 | expect(isString(bool)).to.not.be.ok; 77 | expect(isString(num)).to.not.be.ok; 78 | expect(isString(str)).to.be.ok; 79 | expect(isString(undef)).to.not.be.ok; 80 | expect(isString(typeNull)).to.not.be.ok; 81 | expect(isString(arr)).to.not.be.ok; 82 | expect(isString(obj)).to.not.be.ok; 83 | expect(isString(func)).to.not.be.ok; 84 | 85 | }); 86 | 87 | it('expect isFunction to detect only string', () => { 88 | expect(isFunction(bool)).to.not.be.ok; 89 | expect(isFunction(num)).to.not.be.ok; 90 | expect(isFunction(str)).to.not.be.ok; 91 | expect(isFunction(undef)).to.not.be.ok; 92 | expect(isFunction(typeNull)).to.not.be.ok; 93 | expect(isFunction(arr)).to.not.be.ok; 94 | expect(isFunction(obj)).to.not.be.ok; 95 | expect(isFunction(func)).to.be.ok; 96 | }); 97 | }); 98 | 99 | }); 100 | -------------------------------------------------------------------------------- /test/locationStorage.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {createLocationStorage} from '../src/locationStorage'; 3 | 4 | describe('locationStorage', () => { 5 | 6 | it('init value keys are undefined', () => { 7 | const initLocation = createLocationStorage().getLastLocation(); 8 | expect(typeof initLocation.pathname === 'undefined').to.be.ok; 9 | expect(typeof initLocation.search === 'undefined').to.be.ok; 10 | }); 11 | 12 | it('replace init location with new location', () => { 13 | const locationStorage = createLocationStorage(); 14 | const newLocation = { 15 | pathname:'abc', 16 | search:'def' 17 | }; 18 | 19 | locationStorage.setNewLocation(newLocation); 20 | expect(locationStorage.getLastLocation()).to.equal(newLocation); 21 | }); 22 | 23 | it('replace previous location with new location', () => { 24 | const locationStorage = createLocationStorage(); 25 | const locationA = { 26 | pathname:'abc', 27 | search:'def' 28 | }; 29 | const locationB = { 30 | pathname:'abc', 31 | search:'def' 32 | }; 33 | 34 | locationStorage.setNewLocation(locationA); 35 | locationStorage.setNewLocation(locationB); 36 | expect(locationStorage.getLastLocation()).to.equal(locationB); 37 | 38 | }); 39 | 40 | it('return same object on duplicate getLastLocation()', () => { 41 | const locationStorage = createLocationStorage(); 42 | const newLocation = { 43 | pathname:'abc', 44 | search:'def' 45 | }; 46 | 47 | locationStorage.setNewLocation(newLocation); 48 | expect(locationStorage.getLastLocation()).to.equal(newLocation); 49 | expect(locationStorage.getLastLocation()).to.equal(newLocation); 50 | }); 51 | 52 | }); 53 | -------------------------------------------------------------------------------- /test/resolve.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {resolve} from '../src/resolve'; 3 | 4 | describe('resolve', () => { 5 | 6 | const componentA = (props) => (
); 7 | 8 | it('should create new component with asyncResolve property', () => { 9 | const newComponent = resolve('someName')(componentA); 10 | expect(newComponent).to.have.property('asyncResolve'); 11 | }); 12 | 13 | it('wrapped component should be not modified', () => { 14 | const newComponent = resolve('someName')(componentA); 15 | expect(componentA).to.not.have.property('asyncResolve'); 16 | expect(componentA).to.not.equal(newComponent); 17 | }); 18 | 19 | it('after multiple usage one AsyncResolve component should be created', () => { 20 | const first = resolve('someName')(componentA); 21 | const second = resolve('someName')(first); 22 | expect(first).to.equal(second); 23 | }); 24 | 25 | it('static asyncResolve property should have map with key string and value array', () => { 26 | const newComponent = resolve('someName',1)(componentA); 27 | expect(newComponent.asyncResolve).to.have.property('someName'); 28 | expect(newComponent.asyncResolve.someName).to.be.instanceof(Array); 29 | expect(newComponent.asyncResolve.someName).to.include(1); 30 | }); 31 | 32 | it('on multiple usage all resolves should be added to array', () => { 33 | const one = resolve('someName',1)(componentA); 34 | resolve('someName',2)(one); 35 | expect(one.asyncResolve.someName).to.include(1); 36 | expect(one.asyncResolve.someName).to.include(2); 37 | }); 38 | 39 | }); 40 | --------------------------------------------------------------------------------