├── .babelrc
├── .editorconfig
├── .eslintrc.yml
├── .gitignore
├── .npmignore
├── .travis.yml
├── LICENSE
├── README.md
├── docs
├── en-US
│ ├── api.md
│ └── recipes.md
└── zh-CN
│ ├── README.md
│ ├── api.md
│ └── recipes.md
├── examples
├── counter-immer
│ ├── .babelrc
│ ├── .eslintrc.yml
│ ├── README.md
│ ├── package.json
│ ├── public
│ │ └── index.html
│ ├── src
│ │ ├── App.css
│ │ ├── App.jsx
│ │ ├── index.jsx
│ │ └── models
│ │ │ └── counter.js
│ └── webpack.config.js
├── counter-immutable
│ ├── .babelrc
│ ├── .eslintrc.yml
│ ├── README.md
│ ├── package.json
│ ├── public
│ │ └── index.html
│ ├── src
│ │ ├── App.css
│ │ ├── App.jsx
│ │ ├── index.jsx
│ │ └── models
│ │ │ └── counter.js
│ └── webpack.config.js
├── counter-persist(v5)-immutable
│ ├── README.md
│ ├── package.json
│ ├── public
│ │ └── index.html
│ ├── src
│ │ ├── App.css
│ │ ├── App.jsx
│ │ ├── autoMergeImmutable.js
│ │ ├── index.jsx
│ │ └── models
│ │ │ └── counter.js
│ └── webpack.config.js
├── counter-persist(v5)
│ ├── README.md
│ ├── package.json
│ ├── public
│ │ └── index.html
│ ├── src
│ │ ├── App.css
│ │ ├── App.jsx
│ │ ├── index.jsx
│ │ └── models
│ │ │ └── counter.js
│ └── webpack.config.js
├── counter-persist-immutable
│ ├── .babelrc
│ ├── .eslintrc.yml
│ ├── README.md
│ ├── package.json
│ ├── public
│ │ └── index.html
│ ├── src
│ │ ├── App.css
│ │ ├── App.jsx
│ │ ├── index.jsx
│ │ └── models
│ │ │ └── counter.js
│ └── webpack.config.js
├── counter-persist
│ ├── .babelrc
│ ├── .eslintrc.yml
│ ├── README.md
│ ├── package.json
│ ├── public
│ │ └── index.html
│ ├── src
│ │ ├── App.css
│ │ ├── App.jsx
│ │ ├── index.jsx
│ │ └── models
│ │ │ └── counter.js
│ └── webpack.config.js
├── counter-undo
│ ├── .babelrc
│ ├── .eslintrc.yml
│ ├── README.md
│ ├── package.json
│ ├── public
│ │ └── index.html
│ ├── src
│ │ ├── App.css
│ │ ├── App.jsx
│ │ ├── index.jsx
│ │ └── models
│ │ │ └── counter.js
│ └── webpack.config.js
├── counter
│ ├── .babelrc
│ ├── .eslintrc.yml
│ ├── README.md
│ ├── package.json
│ ├── public
│ │ └── index.html
│ ├── src
│ │ ├── App.css
│ │ ├── App.jsx
│ │ ├── index.jsx
│ │ └── models
│ │ │ └── counter.js
│ └── webpack.config.js
└── simple-router
│ ├── .babelrc
│ ├── .eslintrc.yml
│ ├── README.md
│ ├── package.json
│ ├── public
│ └── index.html
│ ├── src
│ ├── Router.jsx
│ ├── components
│ │ ├── About.jsx
│ │ ├── AddTopic.jsx
│ │ ├── Header.jsx
│ │ ├── Home.jsx
│ │ ├── Topic.jsx
│ │ └── Topics.jsx
│ └── index.jsx
│ └── webpack.config.js
├── mickey.svg
├── package.json
├── src
├── ActionsProvider.js
├── Plugin.js
├── Provider.js
├── actions.js
├── baseModel.js
├── checkModel.js
├── constants.js
├── createApp.js
├── createErrorHandler.js
├── createHistory.js
├── createModel.js
├── createPromiseMiddleware.js
├── createReducer.js
├── createRender.js
├── createStore.js
├── getReducer.js
├── getSaga.js
├── index.js
├── injectActions.js
├── internalModel.js
├── prefixDispatch.js
├── registerModel.js
├── utils.js
└── watcher.js
└── test
├── index.js
├── setup.js
└── spec
├── ActionsProvider.spec.js
├── Plugin.spec.js
├── checkModel.spec.js
├── createApp.spec.js
├── createErrorHandler.spec.js
├── createHistory.spec.js
├── createModel.spec.js
├── getSaga.spec.js
├── injectActions.spec.js
├── utils.spec.js
└── watcher.spec.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "env",
5 | {
6 | "modules": false,
7 | "loose": true,
8 | "useBuiltIns": true
9 | }
10 | ],
11 | "react",
12 | "stage-0"
13 | ],
14 | "plugins": [
15 | "transform-runtime"
16 | ],
17 | "env": {
18 | "test": {
19 | "presets": [
20 | [
21 | "env"
22 | ],
23 | "react",
24 | "stage-0"
25 | ],
26 | "plugins": [
27 | "istanbul",
28 | "transform-decorators-legacy"
29 | ]
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: http://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | [*]
7 | charset = utf-8
8 | end_of_line = lf
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 | indent_style = space
12 | indent_size = 2
13 |
14 | [*.md]
15 | trim_trailing_whitespace = false
16 |
17 | [Makefile]
18 | indent_style = tab
19 |
--------------------------------------------------------------------------------
/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | extends: eslint-config-airbnb
2 | parser: babel-eslint
3 |
4 | env:
5 | es6: true
6 | node: true
7 | mocha: true
8 | browser: true
9 |
10 | rules:
11 | semi:
12 | - warn
13 | - never
14 | arrow-body-style:
15 | - error
16 | - as-needed
17 | no-param-reassign:
18 | - error
19 | - props: false
20 |
21 | parserOptions:
22 | ecmaFeatures:
23 | experimentalObjectRestSpread: true
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .idea
3 | .vscode
4 | .nyc_output
5 | npm-debug.log
6 | node_modules
7 | dist
8 | /lib
9 | /test/coverage
10 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .idea
3 | .vscode
4 | .eslintrc.yml
5 | .babelrc
6 | .npmignore
7 | .gitignore
8 | .editorconfig
9 | .nyc_output
10 | npm-debug.log
11 | node_modules
12 | /test/coverage
13 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: node_js
3 |
4 | node_js:
5 | - "6"
6 | - "7"
7 |
8 | install: npm install
9 |
10 | script:
11 | - npm run lint
12 | - npm test
13 |
14 | after_success:
15 | - npm run coveralls
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 W.Y.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Mickey
2 |
3 |
4 |
5 | > Lightweight front-end framework for creating React and Redux based app painlessly.
6 |
7 | > Totally base on [redux](https://github.com/reactjs/redux), [redux-saga](https://github.com/yelouafi/redux-saga) and [react-router](https://github.com/ReactTraining/react-router), very friendly to redux users.
8 |
9 | > (Inspired by [dva](https://github.com/dvajs/dva))
10 |
11 | [](https://github.com/mickeyjsx/mickey/blob/master/LICENSE)
12 |
13 | [](https://www.npmjs.com/package/mickey)
14 | [](https://travis-ci.org/mickeyjsx/mickey)
15 | [](https://coveralls.io/r/mickeyjsx/mickey)
16 | [](https://npmjs.org/package/mickey)
17 | [](https://david-dm.org/mickeyjsx/mickey)
18 | [](http://packagequality.com/#?package=mickey)
19 |
20 | [查看中文](./docs/zh-CN/README.md)
21 |
22 | ## Features
23 |
24 | - **Minimal API** (Only 6 newly introduced). Easy to learn, easy to start
25 | - **No [`diapatch`](https://github.com/reactjs/react-redux/blob/master/docs/api.md#inject-just-dispatch-and-dont-listen-to-store), no [`put`](https://redux-saga.js.org/docs/api/#putaction)**, just forget about [action types](http://redux.js.org/docs/basics/Actions.html)
26 | - **Support loading models dynamically** with [code-splitting](https://webpack.js.org/guides/code-splitting/) to improve performance
27 | - **Support HMR** for components and models with [babel-plugin-mickey-model-loader](https://github.com/mickeyjsx/babel-plugin-mickey-model-loader)
28 | - **Full-featured hook mechanism**
29 |
30 | ## Quick Start
31 |
32 | Use [create-react-app](https://github.com/facebookincubator/create-react-app) to create an app:
33 |
34 | ```shell
35 | $ npm i -g create-react-app
36 | $ create-react-app my-app
37 | ```
38 |
39 | Then install mickey from npm:
40 |
41 | ```shell
42 | $ cd my-app
43 | $ npm install mickey --save
44 | $ npm start
45 | ```
46 |
47 | Update `index.js` as follow:
48 |
49 | ```jsx
50 | import React from 'react'
51 | import createApp, {connect, injectActions} from 'mickey'
52 |
53 | // 1. Initialize
54 | const app = createApp()
55 |
56 | // 2. Model
57 | app.model({
58 | namespace: 'counter',
59 | state: {
60 | count: 0,
61 | loading: false,
62 | },
63 | increment: state => ({ ...state, count: state.count + 1 }),
64 | decrement: state => ({ ...state, count: state.count - 1 }),
65 | incrementAsync: {
66 | * effect(payload, { call }, { succeed }) {
67 | const delay = timeout => new Promise((resolve) => {
68 | setTimeout(resolve, timeout)
69 | })
70 | yield call(delay, 2000)
71 | yield succeed()
72 | },
73 | prepare: state => ({ ...state, loading: true }),
74 | succeed: state => ({ ...state, count: state.count + 1, loading: false }),
75 | },
76 | })
77 |
78 | // 3. Component
79 | const Comp = (props) => (
80 |
81 |
{props.counter.count}
82 |
83 |
84 |
85 |
86 | )
87 |
88 | // 4. Connect state with component and inject `actions`
89 | const App = injectActions(
90 | connect(state => ({ counter: state.counter })(Comp))
91 | )
92 |
93 | // 5. View
94 | app.render(, document.getElementById('root'))
95 | ```
96 |
97 | ## Examples
98 |
99 | - [Counter](./examples/counter): Basic usage of mickey
100 | - [Counter-Persist](./examples/counter-persist): Work with [redux-persist](https://github.com/rt2zz/redux-persist)
101 | - [Counter-Persist(v5)](./examples/counter-persist(v5)): Work with [redux-persist v5](https://github.com/rt2zz/redux-persist)
102 | - [Counter-Immutable](./examples/counter-immutable): Work with [ImmutableJS](https://github.com/facebook/immutable-js)
103 | - [Counter-Immer](./examples/counter-immer): Work with [Immer](https://github.com/mweststrate/immer)
104 | - [Counter-Persist-Immutable](./examples/counter-persist-immutable): Work with [redux-persist](https://github.com/rt2zz/redux-persist) and [ImmutableJS](https://github.com/facebook/immutable-js)
105 | - [Counter-Persist(v5)-Immutable](./examples/counter-persist(v5)-immutable): Work with [redux-persist v5](https://github.com/rt2zz/redux-persist) and [ImmutableJS](https://github.com/facebook/immutable-js)
106 | - [Counter-Undo](./examples/counter-undo): Work with [redux-undo](https://github.com/omnidan/redux-undo)
107 | - [Simple-Router](./examples/simple-router): Base on [react-router@4.x](https://reacttraining.com/react-router/)
108 | - [mickey-todo](https://github.com/mickeyjsx/mickey-todo) ([demo](https://mickeyjsx.github.io/todo)): Simple Todo application build with mickey
109 | - [mickey-vstar](https://github.com/mickeyjsx/mickey-vstar) ([demo](http://mickeyjsx.github.io/vstar)): A web app to show your or others GitHub repos stars
110 | - [HackerNews](https://github.com/mickeyjsx/mickey-hackernews) ([demo](http://mickeyjsx.github.io/hackernews)): [HackerNews](https://github.com/vuejs/vue-hackernews-2.0) clone built with mickey
111 |
112 | ## More
113 |
114 | - [API Reference](./docs/en-US/api.md)
115 | - [API 文档](./docs/zh-CN/api.md)
116 | - [mickey.svg](./mickey.svg) badaged in this document is download from [Free Vectors](http://all-free-download.com/free-vector/download/disney-disney-vector_288586.html)
117 |
118 |
119 | ## Related
120 |
121 | - [mickey-model-extend](https://github.com/mickeyjsx/mickey-model-extend) Utility method to extend mickey model
122 | - [babel-plugin-mickey-model-loader](https://github.com/mickeyjsx/babel-plugin-mickey-model-loader) Inject a model loader function to mickey instance with hmr support
123 | - [babel-plugin-mickey-model-validator](https://github.com/mickeyjsx/babel-plugin-mickey-model-validator) Validate models shipped by mickey to avoid certain syntax pitfalls
124 |
125 |
126 | ## Contributing
127 |
128 | Pull requests and stars are highly welcome.
129 |
130 | For bugs and feature requests, please [create an issue](https://github.com/mickeyjsx/mickey/issues/new).
131 |
--------------------------------------------------------------------------------
/docs/en-US/api.md:
--------------------------------------------------------------------------------
1 | # API
2 |
3 | [查看中文](../zh-CN/api.md)
4 |
5 | ## Module exports
6 |
7 | 1. Default export a initialize method: `import createApp from 'mickey'`
8 | 2. Component and method
9 | - [<ActionsProvider actions>](#actionsprovider-actions)
10 | - [injectActions({propName = 'actions', withRef = false})](#injectactionscomponent-propname--actions-withref--false)
11 | 3. Directly export The following components and methods from [dependencies](https://github.com/mickeyjsx/mickey/blob/master/package.json#L31).
12 |
13 | - [redux](https://github.com/reactjs/redux)
14 | - [compose](http://redux.js.org/docs/api/compose.html)
15 | - [applyMiddleware](http://redux.js.org/docs/api/applyMiddleware.html)
16 | - [react-redux](https://github.com/reactjs/react-redux)
17 | - [connect](https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options)
18 | - [react-router](https://reacttraining.com/react-router/)
19 | - [StaticRouter](https://reacttraining.com/react-router/core/api/StaticRouter)
20 | - [MemoryRouter](https://reacttraining.com/react-router/web/api/MemoryRouter)
21 | - [Redirect](https://reacttraining.com/react-router/web/api/Redirect)
22 | - [Prompt](https://reacttraining.com/react-router/core/api/Prompt)
23 | - [Switch](https://reacttraining.com/react-router/core/api/Switch)
24 | - [Route](https://reacttraining.com/react-router/core/api/Route)
25 | - [withRouter](https://reacttraining.com/react-router/core/api/withRouter)
26 | - [react-router-dom](https://github.com/ReactTraining/react-router/tree/master/packages/react-router-dom)
27 | - [HashRouter](https://reacttraining.com/react-router/web/api/HashRouter)
28 | - [BrowserRouter](https://reacttraining.com/react-router/web/api/BrowserRouter)
29 | - [Link](https://reacttraining.com/react-router/web/api/Link)
30 | - [NavLink](https://reacttraining.com/react-router/web/api/NavLink)
31 |
32 | ## API
33 |
34 | **Initialization**
35 |
36 | - [createApp(options)](#createappoptions)
37 | - [options.initialState](#optionsinitialstate)
38 | - [options.initialReducer](#optionsinitialreducer)
39 | - [options.historyMode](#optionshistorymode)
40 | - [options.hooks](#optionshooks)
41 | - [options.hooks.onError](#optionshooksonerror)
42 | - [options.hooks.onAction](#optionshooksonaction)
43 | - [options.hooks.onEffect](#optionshooksoneffect)
44 | - [options.hooks.onReducer](#optionshooksonreducer)
45 | - [options.hooks.onStateChange](#optionshooksonstatechange)
46 | - [options.hooks.extraReducers](#optionshooksextrareducers)
47 | - [options.hooks.extraEnhancers](#optionshooksextraenhancers)
48 | - [options.extensions](#optionsextensions)
49 | - [options.extensions.createReducer](#createreducer)
50 | - [options.extensions.combineReducers](#combinereducers)
51 |
52 | **Methods**
53 |
54 | - [app.model(model)](#appmodelmodel)
55 | - [model.namespace](#modelnamespace)
56 | - [model.state](#modelstate)
57 | - [model.watcher](#modelwatcher)
58 | - [model.enhancers](#modelenhancers)
59 | - [model.createReducer](#modelcreatereducer)
60 | - [model[...actionsAndEffects]](#modelactionsandeffects)
61 | - [app.eject(namespace)](#appejectnamespace)
62 | - [app.has(namespace)](#apphasnamespace)
63 | - [app.load(pattern)](#apploadpattern)
64 | - [app.render(component, container, callback)](#apprendercomponent-container-callback)
65 |
66 | **Properties**
67 |
68 | - app.store
69 | - app.history
70 | - app.actions
71 | - app.plugin
72 |
73 | ### createApp(options)
74 |
75 | Create an instance of Mickey and return it:
76 |
77 | ```es6
78 | import createApp from 'mickey';
79 | const app = createApp(options);
80 | ```
81 |
82 |
83 | ## <ActionsProvider actions>
84 |
85 | Makes the `actions` available to the `injectActions()` calls in the component hierarchy below. We should never use this component, and it was used in the `render` method, like this:
86 |
87 | ```
88 |
89 |
90 |
91 | ```
92 |
93 | ## injectActions(Component, {propName = 'actions', withRef = false})
94 |
95 | Inject `actions` to a React component. By default the propName would be `actions`. If `withRef` is true, stores a ref to the wrapped component instance and makes it available via getWrappedInstance() method.
96 |
97 | For example, [Counter](https://github.com/mickeyjsx/mickey/blob/master/examples/counter):
98 |
99 | ```jsx
100 | import React from 'react'
101 | import { connect, injectActions } from 'mickey'
102 | import './App.css'
103 |
104 | const App = props => (
105 |
106 |
{props.count}
107 |
108 |
109 |
110 |
122 |
123 |
124 | )
125 |
126 | export default injectActions(connect(store => ({ ...store.counter }))(App))
127 | ```
128 |
--------------------------------------------------------------------------------
/docs/en-US/recipes.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mickeyjsx/mickey/af21557b89620f9425b2a3f191c94e0186189897/docs/en-US/recipes.md
--------------------------------------------------------------------------------
/docs/zh-CN/README.md:
--------------------------------------------------------------------------------
1 | # Mickey
2 |
3 |
4 |
5 | > 一款轻量、高效、易上手的前端框架,无痛开发基于 React 和 Redux 的应用
6 |
7 | > 完全基于 [redux](https://github.com/reactjs/redux),[redux-saga](https://github.com/yelouafi/redux-saga) 和 [react-router](https://github.com/ReactTraining/react-router) 实现,对 Redux 用户极其友好
8 |
9 | > (Inspired by [dva](https://github.com/dvajs/dva))
10 |
11 |
12 | [](https://github.com/mickeyjsx/mickey/blob/master/LICENSE)
13 |
14 | [](https://www.npmjs.com/package/mickey)
15 | [](https://travis-ci.org/mickeyjsx/mickey)
16 | [](https://coveralls.io/r/mickeyjsx/mickey)
17 | [](https://npmjs.org/package/mickey)
18 | [](https://david-dm.org/mickeyjsx/mickey)
19 | [](http://packagequality.com/#?package=mickey)
20 |
21 | [View README in English](../../README.md#features)
22 |
23 | ## 特性
24 |
25 | - **轻量的API**:只有 6 个新方法,易学易用
26 | - **不再需要使用 [`diapatch`](https://github.com/reactjs/react-redux/blob/master/docs/api.md#inject-just-dispatch-and-dont-listen-to-store) 和 [`put`](https://redux-saga.js.org/docs/api/#putaction)** 方法,也不需要维护 [action](http://redux.js.org/docs/basics/Actions.html) 字符串
27 | - **支持动态加载**:结合 [code-splitting](https://webpack.js.org/guides/code-splitting/) 可以实现路由和模型动态加载
28 | - **支持 HMR**:结合 [babel-plugin-mickey-model-loader](https://github.com/mickeyjsx/babel-plugin-mickey-model-loader) 实现组件和模型热替换
29 | - 功能完备的**插件机制**
30 |
31 | ## 快速开始
32 |
33 | 使用 [create-react-app](https://github.com/facebookincubator/create-react-app) 创建一个新的 app:
34 |
35 | ```shell
36 | $ npm i -g create-react-app
37 | $ create-react-app my-app
38 | ```
39 |
40 | 然后安装 mickey:
41 |
42 | ```shell
43 | $ cd my-app
44 | $ npm install mickey --save
45 | $ npm start
46 | ```
47 |
48 | 修改项目中的 `index.js` 如下:
49 |
50 | ```jsx
51 | import React from 'react'
52 | import createApp, {connect, injectActions} from 'mickey'
53 |
54 | // 1. Initialize
55 | const app = createApp()
56 |
57 | // 2. Model
58 | app.model({
59 | namespace: 'counter',
60 | state: {
61 | count: 0,
62 | loading: false,
63 | },
64 | increment: state => ({ ...state, count: state.count + 1 }),
65 | decrement: state => ({ ...state, count: state.count - 1 }),
66 | incrementAsync: {
67 | * effect(payload, { call }, { succeed }) {
68 | const delay = timeout => new Promise((resolve) => {
69 | setTimeout(resolve, timeout)
70 | })
71 | yield call(delay, 2000)
72 | yield succeed()
73 | },
74 | prepare: state => ({ ...state, loading: true }),
75 | succeed: state => ({ ...state, count: state.count + 1, loading: false }),
76 | },
77 | })
78 |
79 | // 3. Component
80 | const Comp = (props) => (
81 |
82 |
{props.counter.count}
83 |
84 |
85 |
86 |
87 | )
88 |
89 | // 4. Connect state with component and inject `actions`
90 | const App = injectActions(
91 | connect(state => ({ counter: state.counter })(Comp)
92 | )
93 |
94 | // 5. View
95 | app.render(, document.getElementById('root'))
96 | ```
97 |
98 | ## 示例项目
99 |
100 | - [Counter](../../examples/counter):简单的计数器
101 | - [Counter-Persist](../../examples/counter-persist):搭配 [redux-persist](https://github.com/rt2zz/redux-persist)使用
102 | - [Counter-Immutable](../../examples/counter-immutable):搭配 [ImmutableJS](https://github.com/facebook/immutable-js/) 使用
103 | - [Counter-Persist-Immutable](../../examples/counter-persist-immutable):搭配 [redux-persist](https://github.com/rt2zz/redux-persist) 和 [ImmutableJS](https://github.com/facebook/immutable-js/) 使用
104 | - [Counter-Undo](../../examples/counter-undo):搭配 [redux-undo](https://github.com/omnidan/redux-undo) 使用
105 | - [Simple-Router](./examples/simple-router):基于 [react-router@4.x](https://reacttraining.com/react-router/)
106 | - [mickey-todo](https://github.com/mickeyjsx/mickey-todo) ([demo](https://mickeyjsx.github.io/todo)): 简单的 TODO 应用
107 | - [mickey-vstar](https://github.com/mickeyjsx/mickey-vstar) ([demo](http://mickeyjsx.github.io/vstar)):查询指定 Github 账号中被加星项目并按加星数排序
108 | - [HackerNews](https://github.com/mickeyjsx/mickey-hackernews) ([demo](http://mickeyjsx.github.io/hackernews)):基于 mickey 实现的 [HackerNews](https://github.com/vuejs/vue-hackernews-2.0)
109 |
110 | ## 了解更多
111 |
112 | - [API Reference](../en-US/api.md)
113 | - [API 文档](./api.md)
114 | - [mickey.svg](../../mickey.svg) 下载自 [Free Vectors](http://all-free-download.com/free-vector/download/disney-disney-vector_288586.html)
115 |
116 | ## 相关项目
117 |
118 | - [mickey-model-extend](https://github.com/mickeyjsx/mickey-model-extend) 扩展 mickey model 的工具函数
119 | - [babel-plugin-mickey-model-loader](https://github.com/mickeyjsx/babel-plugin-mickey-model-loader) 向 mickey 实例中注入 `load` 方法并提供 HMR 支持
120 | - [babel-plugin-mickey-model-validator](https://github.com/mickeyjsx/babel-plugin-mickey-model-validator) 验证 mickey 模型中潜在的语法错误,如在异步 action 处理方法中调用 `call` 时忘记使用 `yield` 关键字
121 |
122 | ## 贡献代码
123 |
124 | 非常欢迎给我们提 MR,如果喜欢我们的代码请在右上角加星。
125 |
126 | 发现任何 BUG 和使用问题请给我们[提 ISSUE](https://github.com/mickeyjsx/mickey/issues/new)。
127 |
--------------------------------------------------------------------------------
/docs/zh-CN/recipes.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mickeyjsx/mickey/af21557b89620f9425b2a3f191c94e0186189897/docs/zh-CN/recipes.md
--------------------------------------------------------------------------------
/examples/counter-immer/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "es2015",
4 | "react",
5 | "stage-0"
6 | ],
7 | "plugins": [
8 | "add-module-exports",
9 | "transform-runtime"
10 | ],
11 | "env": {
12 | "test": {
13 | "plugins": [
14 | "istanbul"
15 | ]
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/examples/counter-immer/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | extends: eslint-config-airbnb
2 | parser: babel-eslint
3 |
4 | env:
5 | es6: true
6 | node: true
7 | mocha: true
8 | browser: true
9 |
10 | rules:
11 | semi:
12 | - warn
13 | - never
14 | arrow-body-style:
15 | - error
16 | - as-needed
17 | no-param-reassign:
18 | - error
19 | - props: false
20 |
21 | parserOptions:
22 | ecmaFeatures:
23 | experimentalObjectRestSpread: true
24 |
--------------------------------------------------------------------------------
/examples/counter-immer/README.md:
--------------------------------------------------------------------------------
1 | # Counter-Immutable
2 |
3 | > Counter example work with [ImmutableJS](https://github.com/facebook/immutable-js/) for mickey
4 |
5 | ## Usage
6 |
7 | ```bash
8 | $ npm install
9 | $ npm start
10 | ```
11 |
--------------------------------------------------------------------------------
/examples/counter-immer/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "counter-immuatable",
3 | "description": "Counter example work with ImmutableJS for mickey",
4 | "dependencies": {
5 | "immer": "^1.0.1",
6 | "immutable": "^3.8.1",
7 | "mickey": "^1.2.0",
8 | "react": "^15.6.1",
9 | "redux-immutablejs": "^0.0.8"
10 | },
11 | "devDependencies": {
12 | "babel-cli": "^6.26.0",
13 | "babel-core": "^6.26.0",
14 | "babel-eslint": "^7.2.3",
15 | "babel-loader": "^7.1.2",
16 | "babel-plugin-add-module-exports": "^0.2.1",
17 | "babel-plugin-transform-runtime": "^6.23.0",
18 | "babel-preset-es2015": "^6.24.1",
19 | "babel-preset-react": "^6.24.1",
20 | "babel-preset-stage-0": "^6.24.1",
21 | "cross-env": "^5.0.5",
22 | "css-loader": "^0.28.7",
23 | "eslint": "^4.5.0",
24 | "eslint-config-airbnb": "^15.1.0",
25 | "eslint-plugin-import": "^2.7.0",
26 | "eslint-plugin-jsx-a11y": "^5.1.1",
27 | "eslint-plugin-react": "^7.3.0",
28 | "husky": "^0.14.3",
29 | "rimraf": "^2.6.1",
30 | "style-loader": "^0.18.2",
31 | "webpack": "^3.5.5",
32 | "webpack-dev-server": "^2.7.1"
33 | },
34 | "scripts": {
35 | "lint": "eslint --ext .js,.jsx src",
36 | "start": "webpack-dev-server --open",
37 | "build": "cross-env NODE_ENV=production webpack -p"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/examples/counter-immer/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Counter Example - Mickey
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/examples/counter-immer/src/App.css:
--------------------------------------------------------------------------------
1 | body {
2 | height: 100vh;
3 | padding: 0;
4 | margin: 0;
5 | }
6 |
7 | #counter-app {
8 | margin: 0 auto;
9 | width: 300px;
10 | }
11 |
12 | h1 {
13 | font-size: 180px;
14 | margin: 0;
15 | text-align: center;
16 | }
17 |
18 | .btn-wrap {
19 | text-align: center;
20 | }
21 |
22 | button {
23 | background-color: #fff;
24 | border: 1px solid #a0a0a0;
25 | border-radius: 4px;
26 | font-size: 120%;
27 | outline: none;
28 | width: 60px;
29 | height: 32px;
30 | margin: 0 10px;
31 | cursor: pointer;
32 | }
33 |
--------------------------------------------------------------------------------
/examples/counter-immer/src/App.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 |
3 | import React from 'react'
4 | import { connect, injectActions } from 'mickey'
5 | import './App.css'
6 |
7 | const App = props => (
8 |
9 |
{props.count}
10 |
11 |
12 |
13 |
25 |
26 |
27 | )
28 |
29 | export default injectActions(
30 | connect((store) => {
31 | console.log('data:', store.counter) // eslint-disable-line
32 | return ({ ...store.counter })
33 | })(App),
34 | )
35 |
--------------------------------------------------------------------------------
/examples/counter-immer/src/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import createApp from 'mickey'
3 | import App from './App'
4 |
5 | // 1. Initialize
6 | const app = createApp({
7 | initialState: {},
8 | })
9 |
10 |
11 | // 2. Model
12 | app.model(require('./models/counter.js'))
13 |
14 | // 3. View
15 | app.render(, document.getElementById('root'))
16 |
--------------------------------------------------------------------------------
/examples/counter-immer/src/models/counter.js:
--------------------------------------------------------------------------------
1 | import produce from 'immer'
2 |
3 | const delay = timeout => new Promise((resolve) => {
4 | setTimeout(resolve, timeout)
5 | })
6 |
7 | export default {
8 | namespace: 'counter',
9 | state: {
10 | count: 0,
11 | loading: false,
12 | },
13 | increment: state => produce(state, (draft) => { draft.count += 1 }),
14 | decrement: state => produce(state, (draft) => { draft.count -= 1 }),
15 | incrementAsync: {
16 | * effect(payload, { call }, { succeed }) {
17 | yield call(delay, 2000)
18 | yield succeed()
19 | },
20 | prepare: state => produce(state, (draft) => { draft.loading = true }),
21 | succeed: state => produce(state, (draft) => {
22 | draft.count += 1
23 | draft.loading = false
24 | }),
25 | },
26 | }
27 |
--------------------------------------------------------------------------------
/examples/counter-immer/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const webpack = require('webpack')
3 |
4 | const config = {
5 | // in development,
6 | // 'webpack-dev-server/client' and 'webpac/hot/dev-server' will be automatically added
7 | entry: [
8 | './src/index.jsx',
9 | ],
10 | output: {
11 | path: path.resolve(__dirname, 'dist'),
12 | filename: 'app.js',
13 | publicPath: '/',
14 | },
15 | resolve: {
16 | extensions: ['*', '.js', '.jsx'],
17 | },
18 | module: {
19 | rules: [
20 | {
21 | test: /\.jsx?$/,
22 | include: [
23 | path.resolve(__dirname, 'src'),
24 | ],
25 | loader: 'babel-loader',
26 | },
27 | {
28 | test: /\.css$/,
29 | use: [
30 | {
31 | loader: 'style-loader',
32 | },
33 | {
34 | loader: 'css-loader',
35 | },
36 | ],
37 | },
38 | ],
39 | },
40 | }
41 |
42 | if (process.env.NODE_ENV === 'production') {
43 | config.devtool = 'source-map'
44 | // Exclude react and react-dom in the production bundle
45 | config.externals = {
46 | react: 'React',
47 | 'react-dom': 'ReactDOM',
48 | }
49 | } else {
50 | config.devtool = 'cheap-module-source-map'
51 | config.devServer = {
52 | contentBase: path.resolve(__dirname, 'public'),
53 | clientLogLevel: 'none',
54 | quiet: true,
55 | port: 8000,
56 | compress: true,
57 | hot: true,
58 | historyApiFallback: {
59 | disableDotRule: true,
60 | },
61 | }
62 |
63 | // HMR support
64 | config.plugins = [
65 | new webpack.HotModuleReplacementPlugin(),
66 | new webpack.NamedModulesPlugin(),
67 | ]
68 | }
69 |
70 | module.exports = config
71 |
--------------------------------------------------------------------------------
/examples/counter-immutable/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "es2015",
4 | "react",
5 | "stage-0"
6 | ],
7 | "plugins": [
8 | "add-module-exports",
9 | "transform-runtime"
10 | ],
11 | "env": {
12 | "test": {
13 | "plugins": [
14 | "istanbul"
15 | ]
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/examples/counter-immutable/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | extends: eslint-config-airbnb
2 | parser: babel-eslint
3 |
4 | env:
5 | es6: true
6 | node: true
7 | mocha: true
8 | browser: true
9 |
10 | rules:
11 | semi:
12 | - warn
13 | - never
14 | arrow-body-style:
15 | - error
16 | - as-needed
17 | no-param-reassign:
18 | - error
19 | - props: false
20 |
21 | parserOptions:
22 | ecmaFeatures:
23 | experimentalObjectRestSpread: true
24 |
--------------------------------------------------------------------------------
/examples/counter-immutable/README.md:
--------------------------------------------------------------------------------
1 | # Counter-Immutable
2 |
3 | > Counter example work with [ImmutableJS](https://github.com/facebook/immutable-js/) for mickey
4 |
5 | ## Usage
6 |
7 | ```bash
8 | $ npm install
9 | $ npm start
10 | ```
11 |
--------------------------------------------------------------------------------
/examples/counter-immutable/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "counter-immuatable",
3 | "description": "Counter example work with ImmutableJS for mickey",
4 | "dependencies": {
5 | "immutable": "^3.8.1",
6 | "mickey": "^0.2.4",
7 | "react": "^15.6.1",
8 | "redux-immutablejs": "^0.0.8"
9 | },
10 | "devDependencies": {
11 | "babel-cli": "^6.26.0",
12 | "babel-core": "^6.26.0",
13 | "babel-eslint": "^7.2.3",
14 | "babel-loader": "^7.1.2",
15 | "babel-plugin-add-module-exports": "^0.2.1",
16 | "babel-plugin-transform-runtime": "^6.23.0",
17 | "babel-preset-es2015": "^6.24.1",
18 | "babel-preset-react": "^6.24.1",
19 | "babel-preset-stage-0": "^6.24.1",
20 | "cross-env": "^5.0.5",
21 | "css-loader": "^0.28.7",
22 | "eslint": "^4.5.0",
23 | "eslint-config-airbnb": "^15.1.0",
24 | "eslint-plugin-import": "^2.7.0",
25 | "eslint-plugin-jsx-a11y": "^5.1.1",
26 | "eslint-plugin-react": "^7.3.0",
27 | "husky": "^0.14.3",
28 | "rimraf": "^2.6.1",
29 | "style-loader": "^0.18.2",
30 | "webpack": "^3.5.5",
31 | "webpack-dev-server": "^2.7.1"
32 | },
33 | "scripts": {
34 | "lint": "eslint --ext .js,.jsx src",
35 | "start": "webpack-dev-server --open",
36 | "build": "cross-env NODE_ENV=production webpack -p"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/examples/counter-immutable/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Counter Example - Mickey
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/examples/counter-immutable/src/App.css:
--------------------------------------------------------------------------------
1 | body {
2 | height: 100vh;
3 | padding: 0;
4 | margin: 0;
5 | }
6 |
7 | #counter-app {
8 | margin: 0 auto;
9 | width: 300px;
10 | }
11 |
12 | h1 {
13 | font-size: 180px;
14 | margin: 0;
15 | text-align: center;
16 | }
17 |
18 | .btn-wrap {
19 | text-align: center;
20 | }
21 |
22 | button {
23 | background-color: #fff;
24 | border: 1px solid #a0a0a0;
25 | border-radius: 4px;
26 | font-size: 120%;
27 | outline: none;
28 | width: 60px;
29 | height: 32px;
30 | margin: 0 10px;
31 | cursor: pointer;
32 | }
33 |
--------------------------------------------------------------------------------
/examples/counter-immutable/src/App.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 |
3 | import React from 'react'
4 | import { connect, injectActions } from 'mickey'
5 | import './App.css'
6 |
7 | const App = props => (
8 |
9 |
{props.count}
10 |
11 |
12 |
13 |
25 |
26 |
27 | )
28 |
29 | export default injectActions(
30 | connect((store) => {
31 | console.log('data:', store.get('counter').toJS()) // eslint-disable-line
32 | return ({ ...store.get('counter').toJS() })
33 | })(App),
34 | )
35 |
--------------------------------------------------------------------------------
/examples/counter-immutable/src/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import createApp from 'mickey'
3 | import Immutable from 'immutable'
4 | import { createReducer, combineReducers } from 'redux-immutablejs'
5 | import App from './App'
6 |
7 | // 1. Initialize
8 | const app = createApp({
9 | initialState: Immutable.fromJS({}),
10 | extensions: {
11 | createReducer,
12 | combineReducers,
13 | },
14 | })
15 |
16 |
17 | // 2. Model
18 | app.model(require('./models/counter.js'))
19 |
20 | // 3. View
21 | app.render(, document.getElementById('root'))
22 |
--------------------------------------------------------------------------------
/examples/counter-immutable/src/models/counter.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 |
3 | const delay = timeout => new Promise((resolve) => {
4 | setTimeout(resolve, timeout)
5 | })
6 |
7 | export default {
8 | namespace: 'counter',
9 | state: Immutable.fromJS({
10 | count: 0,
11 | loading: false,
12 | }),
13 | increment: state => state.merge({ count: state.get('count') + 1 }),
14 | decrement: state => state.merge({ count: state.get('count') - 1 }),
15 | incrementAsync: {
16 | * effect(payload, { call }, { succeed }) {
17 | yield call(delay, 2000)
18 | yield succeed()
19 | },
20 | prepare: state => state.merge({ loading: true }),
21 | succeed: state => state.merge({ count: state.get('count') + 1, loading: false }),
22 | },
23 | }
24 |
--------------------------------------------------------------------------------
/examples/counter-immutable/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const webpack = require('webpack')
3 |
4 | const config = {
5 | // in development,
6 | // 'webpack-dev-server/client' and 'webpac/hot/dev-server' will be automatically added
7 | entry: [
8 | './src/index.jsx',
9 | ],
10 | output: {
11 | path: path.resolve(__dirname, 'dist'),
12 | filename: 'app.js',
13 | publicPath: '/',
14 | },
15 | resolve: {
16 | extensions: ['*', '.js', '.jsx'],
17 | },
18 | module: {
19 | rules: [
20 | {
21 | test: /\.jsx?$/,
22 | include: [
23 | path.resolve(__dirname, 'src'),
24 | ],
25 | loader: 'babel-loader',
26 | },
27 | {
28 | test: /\.css$/,
29 | use: [
30 | {
31 | loader: 'style-loader',
32 | },
33 | {
34 | loader: 'css-loader',
35 | },
36 | ],
37 | },
38 | ],
39 | },
40 | }
41 |
42 | if (process.env.NODE_ENV === 'production') {
43 | config.devtool = 'source-map'
44 | // Exclude react and react-dom in the production bundle
45 | config.externals = {
46 | react: 'React',
47 | 'react-dom': 'ReactDOM',
48 | }
49 | } else {
50 | config.devtool = 'cheap-module-source-map'
51 | config.devServer = {
52 | contentBase: path.resolve(__dirname, 'public'),
53 | clientLogLevel: 'none',
54 | quiet: true,
55 | port: 8000,
56 | compress: true,
57 | hot: true,
58 | historyApiFallback: {
59 | disableDotRule: true,
60 | },
61 | }
62 |
63 | // HMR support
64 | config.plugins = [
65 | new webpack.HotModuleReplacementPlugin(),
66 | new webpack.NamedModulesPlugin(),
67 | ]
68 | }
69 |
70 | module.exports = config
71 |
--------------------------------------------------------------------------------
/examples/counter-persist(v5)-immutable/README.md:
--------------------------------------------------------------------------------
1 | # Counter-Persist-Immutable(v5)
2 |
3 | > Counter example work with [redux-persist(v5)](https://github.com/rt2zz/redux-persist) and [ImmutableJS](https://github.com/facebook/immutable-js/) for mickey
4 |
5 | ## Features
6 |
7 | - Integrate [Redux](https://github.com/rackt/redux) & [ImmutableJs](https://facebook.github.io/immutable-js/)
8 | - Delay render until rehydration complete
9 |
10 | ## Usage
11 |
12 | ```bash
13 | $ npm install
14 | $ npm start
15 | ```
16 |
17 | ## Resources
18 |
19 | - [Mickey Persist](https://github.com/mickeyjsx/mickey-persist)
--------------------------------------------------------------------------------
/examples/counter-persist(v5)-immutable/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "counter-persist-immuatable(v5)",
3 | "description": "Counter example work with persist-v5 and immutable for mickey",
4 | "dependencies": {
5 | "@actra-development-oss/redux-persist-transform-filter-immutable": "^0.1.7",
6 | "immutable": "^3.8.1",
7 | "mickey": "^1.1.0",
8 | "react": "^16.2.0",
9 | "redux-action-buffer": "^1.1.0",
10 | "redux-immutablejs": "^0.0.8",
11 | "redux-persist": "^5.4.0",
12 | "redux-persist-immutable": "^4.3.1",
13 | "redux-persist-transform-filter-immutable": "^0.3.0",
14 | "redux-persist-transform-immutable": "^4.3.0",
15 | "mickey-persist": "^1.0.1"
16 | },
17 | "devDependencies": {
18 | "babel-cli": "^6.26.0",
19 | "babel-core": "^6.26.0",
20 | "babel-eslint": "^7.2.3",
21 | "babel-loader": "^7.1.2",
22 | "babel-plugin-add-module-exports": "^0.2.1",
23 | "babel-plugin-transform-runtime": "^6.23.0",
24 | "babel-preset-es2015": "^6.24.1",
25 | "babel-preset-react": "^6.24.1",
26 | "babel-preset-stage-0": "^6.24.1",
27 | "cross-env": "^5.0.5",
28 | "css-loader": "^0.28.7",
29 | "eslint": "^4.5.0",
30 | "eslint-config-airbnb": "^15.1.0",
31 | "eslint-plugin-import": "^2.7.0",
32 | "eslint-plugin-jsx-a11y": "^5.1.1",
33 | "eslint-plugin-react": "^7.3.0",
34 | "husky": "^0.14.3",
35 | "rimraf": "^2.6.1",
36 | "style-loader": "^0.18.2",
37 | "webpack": "^3.5.5",
38 | "webpack-dev-server": "^2.7.1"
39 | },
40 | "scripts": {
41 | "lint": "eslint --ext .js,.jsx src",
42 | "start": "webpack-dev-server --open",
43 | "build": "cross-env NODE_ENV=production webpack -p"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/examples/counter-persist(v5)-immutable/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Counter Example - Mickey
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/examples/counter-persist(v5)-immutable/src/App.css:
--------------------------------------------------------------------------------
1 | body {
2 | height: 100vh;
3 | padding: 0;
4 | margin: 0;
5 | }
6 |
7 | #counter-app {
8 | margin: 0 auto;
9 | width: 300px;
10 | }
11 |
12 | h1 {
13 | font-size: 180px;
14 | margin: 0;
15 | text-align: center;
16 | }
17 |
18 | .btn-wrap {
19 | text-align: center;
20 | }
21 |
22 | button {
23 | background-color: #fff;
24 | border: 1px solid #a0a0a0;
25 | border-radius: 4px;
26 | font-size: 120%;
27 | outline: none;
28 | width: 100px;
29 | height: 32px;
30 | margin: 10px;
31 | cursor: pointer;
32 | }
33 |
--------------------------------------------------------------------------------
/examples/counter-persist(v5)-immutable/src/App.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 |
3 | import React from 'react'
4 | import { injectActions, connect } from 'mickey'
5 | import './App.css'
6 |
7 | const App = props => (
8 |
9 |
{props.count}
10 |
11 |
12 |
13 |
25 |
26 |
27 |
28 | )
29 |
30 | export default injectActions(
31 | connect((store) => {
32 | console.log('data:', ({ ...store['counter'].toJS() })) // eslint-disable-line
33 | return ({ ...store['counter'].toJS() })
34 | })(App),
35 | )
36 |
--------------------------------------------------------------------------------
/examples/counter-persist(v5)-immutable/src/autoMergeImmutable.js:
--------------------------------------------------------------------------------
1 |
2 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
3 |
4 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
5 |
6 | export function autoMergeImmutable(inboundState, originalState, reducedState, _ref) {
7 | var debug = _ref.debug;
8 | var newState = _extends({}, reducedState);
9 | // only rehydrate if inboundState exists and is an object
10 | if (inboundState && (typeof inboundState === 'undefined' ? 'undefined' : _typeof(inboundState)) === 'object') {
11 | Object.keys(inboundState).forEach(function (key) {
12 | // ignore _persist data
13 | if (key === '_persist') return;
14 | // if reducer modifies substate, skip auto rehydration
15 | if (originalState[key] !== reducedState[key]) {
16 | if (process.env.NODE_ENV !== 'production' && debug) console.log('redux-persist/stateReconciler: sub state for key `%s` modified, skipping.', key);
17 | return;
18 | }
19 | if (isPlainEnoughObject(reducedState[key])) {
20 | // if object is plain enough shallow merge the new values (hence "Level2")
21 | newState[key] = newState[key].merge(inboundState[key]);
22 | return;
23 | }
24 | // otherwise hard set
25 | newState[key] = inboundState[key];
26 | });
27 | }
28 |
29 | if (process.env.NODE_ENV !== 'production' && debug && inboundState && (typeof inboundState === 'undefined' ? 'undefined' : _typeof(inboundState)) === 'object') console.log('redux-persist/stateReconciler: rehydrated keys \'' + Object.keys(inboundState).join(', ') + '\'');
30 |
31 | return newState;
32 | }
33 |
34 | function isPlainEnoughObject(o) {
35 | return o !== null && !Array.isArray(o) && (typeof o === 'undefined' ? 'undefined' : _typeof(o)) === 'object';
36 | }
--------------------------------------------------------------------------------
/examples/counter-persist(v5)-immutable/src/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import createApp, { applyMiddleware } from 'mickey'
3 | import Immutable from 'immutable'
4 | import storage from 'redux-persist/lib/storage'
5 | import { persistStore, persistCombineReducers } from 'redux-persist'
6 | import immutableTransform from 'redux-persist-transform-immutable'
7 | import { PersistGate } from 'redux-persist/lib/integration/react'
8 | import createFilter from 'redux-persist-transform-filter-immutable';
9 | import MickeyPersist from 'mickey-persist'
10 | import App from './App'
11 | import { autoMergeImmutable } from './autoMergeImmutable'
12 | import { combineReducers } from 'redux-immutablejs';
13 |
14 | //specific key to persist
15 | const saveSubsetFilter = createFilter(
16 | 'counter',
17 | ['count']
18 | );
19 |
20 | const persistConfig = {
21 | transforms: [saveSubsetFilter, immutableTransform()],
22 | keyPrefix: 'mickey-immutable:',
23 | storage,
24 | whitelist: ['counter'],
25 | key: 'primary',
26 | stateReconciler: autoMergeImmutable // rehydrate immutable state
27 | }
28 | // 1. Initialize
29 | const app = createApp({
30 | initialState: {}, //must be a no-immutable object
31 | extensions: {
32 | combineReducers: MickeyPersist(persistCombineReducers.bind(this, persistConfig), combineReducers),
33 | },
34 | })
35 |
36 | // 2. Model
37 | app.model(require('./models/counter.js'))
38 | // 3. View
39 | app.render(null, document.getElementById('root'), {
40 | beforeRender: ({ store }) => new Promise(((resolve) => {
41 | resolve(
42 |
43 | );
44 | })),
45 | })
46 |
47 |
--------------------------------------------------------------------------------
/examples/counter-persist(v5)-immutable/src/models/counter.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 |
3 | const delay = timeout => new Promise((resolve) => {
4 | setTimeout(resolve, timeout)
5 | })
6 |
7 | export default {
8 | namespace: 'counter',
9 | state: Immutable.fromJS({
10 | count: 0,
11 | loading: false,
12 | }),
13 | increment: (state) => {
14 | return state.merge({ count: state.get('count') + 1 })
15 | },
16 | decrement: state => state.merge({ count: state.get('count') - 1 }),
17 | incrementAsync: {
18 | * effect(payload, { call }, { succeed }) {
19 | yield call(delay, 2000)
20 | yield succeed()
21 | },
22 | prepare: state => state.merge({ loading: true }),
23 | succeed: state => state.merge({ count: state.get('count') + 1, loading: false }),
24 | },
25 | }
26 |
--------------------------------------------------------------------------------
/examples/counter-persist(v5)-immutable/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const webpack = require('webpack')
3 |
4 | const config = {
5 | // in development,
6 | // 'webpack-dev-server/client' and 'webpac/hot/dev-server' will be automatically added
7 | entry: [
8 | './src/index.jsx',
9 | ],
10 | output: {
11 | path: path.resolve(__dirname, 'dist'),
12 | filename: 'app.js',
13 | publicPath: '/',
14 | },
15 | resolve: {
16 | extensions: ['*', '.js', '.jsx'],
17 | },
18 | module: {
19 | rules: [
20 | {
21 | test: /\.jsx?$/,
22 | include: [
23 | path.resolve(__dirname, 'src'),
24 | ],
25 | loader: 'babel-loader',
26 | },
27 | {
28 | test: /\.css$/,
29 | use: [
30 | {
31 | loader: 'style-loader',
32 | },
33 | {
34 | loader: 'css-loader',
35 | },
36 | ],
37 | },
38 | ],
39 | },
40 | }
41 |
42 | if (process.env.NODE_ENV === 'production') {
43 | config.devtool = 'source-map'
44 | // Exclude react and react-dom in the production bundle
45 | config.externals = {
46 | react: 'React',
47 | 'react-dom': 'ReactDOM',
48 | }
49 | } else {
50 | config.devtool = 'cheap-module-source-map'
51 | config.devServer = {
52 | contentBase: path.resolve(__dirname, 'public'),
53 | clientLogLevel: 'none',
54 | quiet: true,
55 | port: 8000,
56 | compress: true,
57 | hot: true,
58 | historyApiFallback: {
59 | disableDotRule: true,
60 | },
61 | }
62 |
63 | // HMR support
64 | config.plugins = [
65 | new webpack.HotModuleReplacementPlugin(),
66 | new webpack.NamedModulesPlugin(),
67 | ]
68 | }
69 |
70 | module.exports = config
71 |
--------------------------------------------------------------------------------
/examples/counter-persist(v5)/README.md:
--------------------------------------------------------------------------------
1 | # Counter-Persist(v5)
2 |
3 | > Counter example work with [redux-persist](https://github.com/rt2zz/redux-persist) for mickey
4 |
5 | ## Features
6 |
7 | - Delay render until rehydration complete
8 |
9 | ## Usage
10 |
11 | ```bash
12 | $ npm install
13 | $ npm start
14 | ```
15 |
16 | ## Resources
17 |
18 | - [Redux Action Buffer](https://github.com/rt2zz/redux-action-buffer#redux-persist-example)
19 | - [Mickey Persist](https://github.com/mickeyjsx/mickey-persist)
20 |
--------------------------------------------------------------------------------
/examples/counter-persist(v5)/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "counter-persist(v5)",
3 | "description": "Counter example work with redux-persist for mickey",
4 | "dependencies": {
5 | "mickey": "^1.1.0",
6 | "react": "^16.2.0",
7 | "react-dom": "^16.2.0",
8 | "redux-action-buffer": "^1.1.0",
9 | "redux-persist": "^5.4.0",
10 | "redux-persist-transform-filter": "0.0.16",
11 | "mickey-persist": "^1.0.1"
12 | },
13 | "devDependencies": {
14 | "babel-cli": "^6.26.0",
15 | "babel-core": "^6.26.0",
16 | "babel-eslint": "^7.2.3",
17 | "babel-loader": "^7.1.2",
18 | "babel-plugin-add-module-exports": "^0.2.1",
19 | "babel-plugin-transform-runtime": "^6.23.0",
20 | "babel-preset-es2015": "^6.24.1",
21 | "babel-preset-react": "^6.24.1",
22 | "babel-preset-stage-0": "^6.24.1",
23 | "cross-env": "^5.0.5",
24 | "css-loader": "^0.28.7",
25 | "eslint": "^4.5.0",
26 | "eslint-config-airbnb": "^15.1.0",
27 | "eslint-plugin-import": "^2.7.0",
28 | "eslint-plugin-jsx-a11y": "^5.1.1",
29 | "eslint-plugin-react": "^7.3.0",
30 | "husky": "^0.14.3",
31 | "rimraf": "^2.6.1",
32 | "style-loader": "^0.18.2",
33 | "webpack": "^3.5.5",
34 | "webpack-dev-server": "^2.7.1"
35 | },
36 | "scripts": {
37 | "lint": "eslint --ext .js,.jsx src",
38 | "start": "webpack-dev-server --open",
39 | "build": "cross-env NODE_ENV=production webpack -p"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/examples/counter-persist(v5)/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Counter Example - Mickey
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/examples/counter-persist(v5)/src/App.css:
--------------------------------------------------------------------------------
1 | body {
2 | height: 100vh;
3 | padding: 0;
4 | margin: 0;
5 | }
6 |
7 | #counter-app {
8 | margin: 0 auto;
9 | width: 300px;
10 | }
11 |
12 | h1 {
13 | font-size: 180px;
14 | margin: 0;
15 | text-align: center;
16 | }
17 |
18 | .btn-wrap {
19 | text-align: center;
20 | }
21 |
22 | button {
23 | background-color: #fff;
24 | border: 1px solid #a0a0a0;
25 | border-radius: 4px;
26 | font-size: 120%;
27 | outline: none;
28 | width: 100px;
29 | height: 32px;
30 | margin: 10px;
31 | cursor: pointer;
32 | }
33 |
--------------------------------------------------------------------------------
/examples/counter-persist(v5)/src/App.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 |
3 | import React from 'react'
4 | import { connect, injectActions } from 'mickey'
5 | import './App.css'
6 |
7 | const App = props => (
8 |
9 |
{props.count}
10 |
11 |
12 |
13 |
25 |
26 |
27 |
28 | )
29 |
30 | export default injectActions(
31 | connect(store => ({ ...store.counter }))(App),
32 | )
33 |
--------------------------------------------------------------------------------
/examples/counter-persist(v5)/src/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import createApp, { applyMiddleware } from 'mickey'
3 | import { persistStore, persistCombineReducers, REHYDRATE, PERSIST } from 'redux-persist'
4 | import { PersistGate } from 'redux-persist/lib/integration/react'
5 | import createActionBuffer from 'redux-action-buffer'
6 | import App from './App'
7 | import storage from 'redux-persist/lib/storage'
8 | import createFilter from 'redux-persist-transform-filter';
9 | import MickeyPersist from 'mickey-persist';
10 |
11 | //specific key to persist
12 | const saveSubsetFilter = createFilter(
13 | 'counter',
14 | ['count']
15 | );
16 |
17 | const persistConfig = {
18 | transforms: [saveSubsetFilter],
19 | keyPrefix: 'mickey:',
20 | storage,
21 | whitelist: ['counter'],
22 | key: 'primary'
23 | }
24 | // 1. Initialize
25 | const app = createApp({
26 | extensions: {
27 | combineReducers: MickeyPersist( persistCombineReducers.bind(this, persistConfig) ),
28 | }
29 | })
30 |
31 |
32 | // 2. Model
33 | app.model(require('./models/counter.js'))
34 | // 3. View
35 | app.render(null, document.getElementById('root'), {
36 | beforeRender: ({ store }) => new Promise(((resolve) => {
37 | resolve(
38 |
39 | );
40 | })),
41 | })
--------------------------------------------------------------------------------
/examples/counter-persist(v5)/src/models/counter.js:
--------------------------------------------------------------------------------
1 | import { REHYDRATE } from 'redux-persist'
2 |
3 | const delay = timeout => new Promise((resolve) => {
4 | setTimeout(resolve, timeout)
5 | })
6 |
7 | export default {
8 | namespace: 'counter',
9 | state: {
10 | count: 0,
11 | loading: false,
12 | },
13 | increment: state => ({ ...state, count: state.count + 1 }),
14 | decrement: state => ({ ...state, count: state.count - 1 }),
15 | incrementAsync: {
16 | * effect(payload, { call }, { succeed }) {
17 | yield call(delay, 2000)
18 | yield succeed()
19 | },
20 | prepare: state => ({ ...state, loading: true }),
21 | succeed: state => ({ ...state, count: state.count + 1, loading: false }),
22 | },
23 | }
24 |
--------------------------------------------------------------------------------
/examples/counter-persist(v5)/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const webpack = require('webpack')
3 |
4 | const config = {
5 | // in development,
6 | // 'webpack-dev-server/client' and 'webpac/hot/dev-server' will be automatically added
7 | entry: [
8 | './src/index.jsx',
9 | ],
10 | output: {
11 | path: path.resolve(__dirname, 'dist'),
12 | filename: 'app.js',
13 | publicPath: '/',
14 | },
15 | resolve: {
16 | extensions: ['*', '.js', '.jsx'],
17 | },
18 | module: {
19 | rules: [
20 | {
21 | test: /\.jsx?$/,
22 | include: [
23 | path.resolve(__dirname, 'src'),
24 | ],
25 | loader: 'babel-loader',
26 | },
27 | {
28 | test: /\.css$/,
29 | use: [
30 | {
31 | loader: 'style-loader',
32 | },
33 | {
34 | loader: 'css-loader',
35 | },
36 | ],
37 | },
38 | ],
39 | },
40 | }
41 |
42 | if (process.env.NODE_ENV === 'production') {
43 | config.devtool = 'source-map'
44 | // Exclude react and react-dom in the production bundle
45 | config.externals = {
46 | react: 'React',
47 | 'react-dom': 'ReactDOM',
48 | }
49 | } else {
50 | config.devtool = 'cheap-module-source-map'
51 | config.devServer = {
52 | contentBase: path.resolve(__dirname, 'public'),
53 | clientLogLevel: 'none',
54 | quiet: true,
55 | port: 8000,
56 | compress: true,
57 | hot: true,
58 | historyApiFallback: {
59 | disableDotRule: true,
60 | },
61 | }
62 |
63 | // HMR support
64 | config.plugins = [
65 | new webpack.HotModuleReplacementPlugin(),
66 | new webpack.NamedModulesPlugin(),
67 | ]
68 | }
69 |
70 | module.exports = config
71 |
--------------------------------------------------------------------------------
/examples/counter-persist-immutable/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "es2015",
4 | "react",
5 | "stage-0"
6 | ],
7 | "plugins": [
8 | "add-module-exports",
9 | "transform-runtime"
10 | ],
11 | "env": {
12 | "test": {
13 | "plugins": [
14 | "istanbul"
15 | ]
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/examples/counter-persist-immutable/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | extends: eslint-config-airbnb
2 | parser: babel-eslint
3 |
4 | env:
5 | es6: true
6 | node: true
7 | mocha: true
8 | browser: true
9 |
10 | rules:
11 | semi:
12 | - warn
13 | - never
14 | arrow-body-style:
15 | - error
16 | - as-needed
17 | no-param-reassign:
18 | - error
19 | - props: false
20 |
21 | parserOptions:
22 | ecmaFeatures:
23 | experimentalObjectRestSpread: true
24 |
--------------------------------------------------------------------------------
/examples/counter-persist-immutable/README.md:
--------------------------------------------------------------------------------
1 | # Counter-Persist-Immutable
2 |
3 | > Counter example work with [redux-persist](https://github.com/rt2zz/redux-persist) and [ImmutableJS](https://github.com/facebook/immutable-js/) for mickey
4 |
5 | ## Features
6 |
7 | - Integrate [Redux](https://github.com/rackt/redux) & [ImmutableJs](https://facebook.github.io/immutable-js/)
8 | - Delay render until rehydration complete
9 | - Buffer all actions into a queue until rehydration complete
10 |
11 |
12 | ## Usage
13 |
14 | ```bash
15 | $ npm install
16 | $ npm start
17 | ```
18 |
19 | ## Resources
20 |
21 | - [Redux Action Buffer](https://github.com/rt2zz/redux-action-buffer#redux-persist-example)
22 | - [Delay Render Until Rehydration Complete](https://github.com/rt2zz/redux-persist/blob/master/docs/recipes.md#delay-render-until-rehydration-complete)
23 |
--------------------------------------------------------------------------------
/examples/counter-persist-immutable/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "counter-persist-immuatable",
3 | "description": "Counter example work with persist and immutable for mickey",
4 | "dependencies": {
5 | "immutable": "^3.8.1",
6 | "mickey": "^0.2.4",
7 | "react": "^15.6.1",
8 | "redux-action-buffer": "^1.1.0",
9 | "redux-immutablejs": "^0.0.8",
10 | "redux-persist": "^4.9.1",
11 | "redux-persist-immutable": "^4.3.1",
12 | "redux-persist-transform-immutable": "^4.3.0"
13 | },
14 | "devDependencies": {
15 | "babel-cli": "^6.26.0",
16 | "babel-core": "^6.26.0",
17 | "babel-eslint": "^7.2.3",
18 | "babel-loader": "^7.1.2",
19 | "babel-plugin-add-module-exports": "^0.2.1",
20 | "babel-plugin-transform-runtime": "^6.23.0",
21 | "babel-preset-es2015": "^6.24.1",
22 | "babel-preset-react": "^6.24.1",
23 | "babel-preset-stage-0": "^6.24.1",
24 | "cross-env": "^5.0.5",
25 | "css-loader": "^0.28.7",
26 | "eslint": "^4.5.0",
27 | "eslint-config-airbnb": "^15.1.0",
28 | "eslint-plugin-import": "^2.7.0",
29 | "eslint-plugin-jsx-a11y": "^5.1.1",
30 | "eslint-plugin-react": "^7.3.0",
31 | "husky": "^0.14.3",
32 | "rimraf": "^2.6.1",
33 | "style-loader": "^0.18.2",
34 | "webpack": "^3.5.5",
35 | "webpack-dev-server": "^2.7.1"
36 | },
37 | "scripts": {
38 | "lint": "eslint --ext .js,.jsx src",
39 | "start": "webpack-dev-server --open",
40 | "build": "cross-env NODE_ENV=production webpack -p"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/examples/counter-persist-immutable/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Counter Example - Mickey
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/examples/counter-persist-immutable/src/App.css:
--------------------------------------------------------------------------------
1 | body {
2 | height: 100vh;
3 | padding: 0;
4 | margin: 0;
5 | }
6 |
7 | #counter-app {
8 | margin: 0 auto;
9 | width: 300px;
10 | }
11 |
12 | h1 {
13 | font-size: 180px;
14 | margin: 0;
15 | text-align: center;
16 | }
17 |
18 | .btn-wrap {
19 | text-align: center;
20 | }
21 |
22 | button {
23 | background-color: #fff;
24 | border: 1px solid #a0a0a0;
25 | border-radius: 4px;
26 | font-size: 120%;
27 | outline: none;
28 | width: 100px;
29 | height: 32px;
30 | margin: 10px;
31 | cursor: pointer;
32 | }
33 |
--------------------------------------------------------------------------------
/examples/counter-persist-immutable/src/App.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 |
3 | import React from 'react'
4 | import { injectActions, connect } from 'mickey'
5 | import './App.css'
6 |
7 | const App = props => (
8 |
9 |
{props.count}
10 |
11 |
12 |
13 |
25 |
26 |
27 |
28 | )
29 |
30 | export default injectActions(
31 | connect((store) => {
32 | console.log('data:', store.get('counter').toJS()) // eslint-disable-line
33 | return ({ ...store.get('counter').toJS() })
34 | })(App),
35 | )
36 |
--------------------------------------------------------------------------------
/examples/counter-persist-immutable/src/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import createApp, { applyMiddleware } from 'mickey'
3 | import Immutable from 'immutable'
4 | import { createReducer, combineReducers } from 'redux-immutablejs'
5 | import { persistStore, autoRehydrate } from 'redux-persist-immutable'
6 | import immutableTransform from 'redux-persist-transform-immutable'
7 | import { REHYDRATE } from 'redux-persist/constants'
8 | import createActionBuffer from 'redux-action-buffer'
9 | import App from './App'
10 |
11 | // 1. Initialize
12 | const app = createApp({
13 | initialState: Immutable.fromJS({}),
14 | extensions: {
15 | createReducer,
16 | combineReducers,
17 | },
18 | hooks: {
19 | extraEnhancers: [
20 | // add `autoRehydrate` as an enhancer to your store
21 | autoRehydrate(),
22 | // make sure to apply this after autoRehydrate
23 | applyMiddleware(
24 | // buffer other reducers before rehydrated
25 | createActionBuffer(REHYDRATE),
26 | ),
27 | ],
28 | },
29 | })
30 |
31 |
32 | // 2. Model
33 | app.model(require('./models/counter.js'))
34 |
35 | // 3. View
36 | app.render(, document.getElementById('root'), {
37 | beforeRender: ({ store }) => new Promise(((resolve) => {
38 | // begin periodically persisting the store
39 | persistStore(store, {
40 | debounce: 500,
41 | whitelist: ['counter'],
42 | keyPrefix: 'mickey-immutable:',
43 | transforms: [immutableTransform()],
44 | }, () => {
45 | resolve() // delay render after rehydrated
46 | })
47 | })),
48 | })
49 |
--------------------------------------------------------------------------------
/examples/counter-persist-immutable/src/models/counter.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable'
2 | import { REHYDRATE } from 'redux-persist/constants'
3 |
4 | const delay = timeout => new Promise((resolve) => {
5 | setTimeout(resolve, timeout)
6 | })
7 |
8 | export default {
9 | namespace: 'counter',
10 | state: Immutable.fromJS({
11 | count: 0,
12 | loading: false,
13 | }),
14 | enhancers: [
15 | reducer => (state, action) => {
16 | const { type, payload } = action
17 | if (type === REHYDRATE) {
18 | return payload.counter
19 | }
20 | return reducer(state, action)
21 | },
22 | ],
23 | increment: state => state.merge({ count: state.get('count') + 1 }),
24 | decrement: state => state.merge({ count: state.get('count') - 1 }),
25 | incrementAsync: {
26 | * effect(payload, { call }, { succeed }) {
27 | yield call(delay, 2000)
28 | yield succeed()
29 | },
30 | prepare: state => state.merge({ loading: true }),
31 | succeed: state => state.merge({ count: state.get('count') + 1, loading: false }),
32 | },
33 | }
34 |
--------------------------------------------------------------------------------
/examples/counter-persist-immutable/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const webpack = require('webpack')
3 |
4 | const config = {
5 | // in development,
6 | // 'webpack-dev-server/client' and 'webpac/hot/dev-server' will be automatically added
7 | entry: [
8 | './src/index.jsx',
9 | ],
10 | output: {
11 | path: path.resolve(__dirname, 'dist'),
12 | filename: 'app.js',
13 | publicPath: '/',
14 | },
15 | resolve: {
16 | extensions: ['*', '.js', '.jsx'],
17 | },
18 | module: {
19 | rules: [
20 | {
21 | test: /\.jsx?$/,
22 | include: [
23 | path.resolve(__dirname, 'src'),
24 | ],
25 | loader: 'babel-loader',
26 | },
27 | {
28 | test: /\.css$/,
29 | use: [
30 | {
31 | loader: 'style-loader',
32 | },
33 | {
34 | loader: 'css-loader',
35 | },
36 | ],
37 | },
38 | ],
39 | },
40 | }
41 |
42 | if (process.env.NODE_ENV === 'production') {
43 | config.devtool = 'source-map'
44 | // Exclude react and react-dom in the production bundle
45 | config.externals = {
46 | react: 'React',
47 | 'react-dom': 'ReactDOM',
48 | }
49 | } else {
50 | config.devtool = 'cheap-module-source-map'
51 | config.devServer = {
52 | contentBase: path.resolve(__dirname, 'public'),
53 | clientLogLevel: 'none',
54 | quiet: true,
55 | port: 8000,
56 | compress: true,
57 | hot: true,
58 | historyApiFallback: {
59 | disableDotRule: true,
60 | },
61 | }
62 |
63 | // HMR support
64 | config.plugins = [
65 | new webpack.HotModuleReplacementPlugin(),
66 | new webpack.NamedModulesPlugin(),
67 | ]
68 | }
69 |
70 | module.exports = config
71 |
--------------------------------------------------------------------------------
/examples/counter-persist/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "es2015",
4 | "react",
5 | "stage-0"
6 | ],
7 | "plugins": [
8 | "add-module-exports",
9 | "transform-runtime"
10 | ],
11 | "env": {
12 | "test": {
13 | "plugins": [
14 | "istanbul"
15 | ]
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/examples/counter-persist/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | extends: eslint-config-airbnb
2 | parser: babel-eslint
3 |
4 | env:
5 | es6: true
6 | node: true
7 | mocha: true
8 | browser: true
9 |
10 | rules:
11 | semi:
12 | - warn
13 | - never
14 | arrow-body-style:
15 | - error
16 | - as-needed
17 | no-param-reassign:
18 | - error
19 | - props: false
20 |
21 | parserOptions:
22 | ecmaFeatures:
23 | experimentalObjectRestSpread: true
24 |
--------------------------------------------------------------------------------
/examples/counter-persist/README.md:
--------------------------------------------------------------------------------
1 | # Counter-Persist
2 |
3 | > Counter example work with [redux-persist](https://github.com/rt2zz/redux-persist) for mickey
4 |
5 | ## Features
6 |
7 | - Delay render until rehydration complete
8 | - Buffer all actions into a queue until rehydration complete
9 |
10 |
11 | ## Usage
12 |
13 | ```bash
14 | $ npm install
15 | $ npm start
16 | ```
17 |
18 | ## Resources
19 |
20 | - [Redux Action Buffer](https://github.com/rt2zz/redux-action-buffer#redux-persist-example)
21 | - [Delay Render Until Rehydration Complete](https://github.com/rt2zz/redux-persist/blob/master/docs/recipes.md#delay-render-until-rehydration-complete)
22 |
--------------------------------------------------------------------------------
/examples/counter-persist/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "counter-persist",
3 | "description": "Counter example work with redux-persist for mickey",
4 | "dependencies": {
5 | "mickey": "^0.2.4",
6 | "react": "^15.6.1",
7 | "redux-action-buffer": "^1.1.0",
8 | "redux-persist": "^4.9.1"
9 | },
10 | "devDependencies": {
11 | "babel-cli": "^6.26.0",
12 | "babel-core": "^6.26.0",
13 | "babel-eslint": "^7.2.3",
14 | "babel-loader": "^7.1.2",
15 | "babel-plugin-add-module-exports": "^0.2.1",
16 | "babel-plugin-transform-runtime": "^6.23.0",
17 | "babel-preset-es2015": "^6.24.1",
18 | "babel-preset-react": "^6.24.1",
19 | "babel-preset-stage-0": "^6.24.1",
20 | "cross-env": "^5.0.5",
21 | "css-loader": "^0.28.7",
22 | "eslint": "^4.5.0",
23 | "eslint-config-airbnb": "^15.1.0",
24 | "eslint-plugin-import": "^2.7.0",
25 | "eslint-plugin-jsx-a11y": "^5.1.1",
26 | "eslint-plugin-react": "^7.3.0",
27 | "husky": "^0.14.3",
28 | "rimraf": "^2.6.1",
29 | "style-loader": "^0.18.2",
30 | "webpack": "^3.5.5",
31 | "webpack-dev-server": "^2.7.1"
32 | },
33 | "scripts": {
34 | "lint": "eslint --ext .js,.jsx src",
35 | "start": "webpack-dev-server --open",
36 | "build": "cross-env NODE_ENV=production webpack -p"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/examples/counter-persist/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Counter Example - Mickey
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/examples/counter-persist/src/App.css:
--------------------------------------------------------------------------------
1 | body {
2 | height: 100vh;
3 | padding: 0;
4 | margin: 0;
5 | }
6 |
7 | #counter-app {
8 | margin: 0 auto;
9 | width: 300px;
10 | }
11 |
12 | h1 {
13 | font-size: 180px;
14 | margin: 0;
15 | text-align: center;
16 | }
17 |
18 | .btn-wrap {
19 | text-align: center;
20 | }
21 |
22 | button {
23 | background-color: #fff;
24 | border: 1px solid #a0a0a0;
25 | border-radius: 4px;
26 | font-size: 120%;
27 | outline: none;
28 | width: 100px;
29 | height: 32px;
30 | margin: 10px;
31 | cursor: pointer;
32 | }
33 |
--------------------------------------------------------------------------------
/examples/counter-persist/src/App.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 |
3 | import React from 'react'
4 | import { connect, injectActions } from 'mickey'
5 | import './App.css'
6 |
7 | const App = props => (
8 |
9 |
{props.count}
10 |
11 |
12 |
13 |
25 |
26 |
27 |
28 | )
29 |
30 | export default injectActions(
31 | connect(store => ({ ...store.counter }))(App),
32 | )
33 |
--------------------------------------------------------------------------------
/examples/counter-persist/src/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import createApp, { applyMiddleware } from 'mickey'
3 | import { persistStore, autoRehydrate } from 'redux-persist'
4 | import { REHYDRATE } from 'redux-persist/constants'
5 | import createActionBuffer from 'redux-action-buffer'
6 | import App from './App'
7 |
8 | // 1. Initialize
9 | const app = createApp({
10 | hooks: {
11 | extraEnhancers: [
12 | // add `autoRehydrate` as an enhancer to your store
13 | autoRehydrate(),
14 | // make sure to apply this after autoRehydrate
15 | applyMiddleware(
16 | // buffer other reducers before rehydrated
17 | createActionBuffer(REHYDRATE),
18 | ),
19 | ],
20 | },
21 | })
22 |
23 |
24 | // 2. Model
25 | app.model(require('./models/counter.js'))
26 |
27 | // 3. View
28 | app.render(, document.getElementById('root'), {
29 | beforeRender: ({ store }) => new Promise(((resolve) => {
30 | // begin periodically persisting the store
31 | persistStore(store, {
32 | debounce: 500,
33 | whitelist: ['counter'],
34 | keyPrefix: 'mickey:',
35 | }, () => {
36 | resolve() // delay render after rehydrated
37 | })
38 | })),
39 | })
40 |
--------------------------------------------------------------------------------
/examples/counter-persist/src/models/counter.js:
--------------------------------------------------------------------------------
1 | import { REHYDRATE } from 'redux-persist/constants'
2 |
3 | const delay = timeout => new Promise((resolve) => {
4 | setTimeout(resolve, timeout)
5 | })
6 |
7 | export default {
8 | namespace: 'counter',
9 | state: {
10 | count: 0,
11 | loading: false,
12 | },
13 | enhancers: [
14 | reducer => (state, action) => {
15 | const { type, payload } = action
16 | if (type === REHYDRATE) {
17 | return {
18 | ...state,
19 | ...payload.counter,
20 | }
21 | }
22 | return reducer(state, action)
23 | },
24 | ],
25 | increment: state => ({ ...state, count: state.count + 1 }),
26 | decrement: state => ({ ...state, count: state.count - 1 }),
27 | incrementAsync: {
28 | * effect(payload, { call }, { succeed }) {
29 | yield call(delay, 2000)
30 | yield succeed()
31 | },
32 | prepare: state => ({ ...state, loading: true }),
33 | succeed: state => ({ ...state, count: state.count + 1, loading: false }),
34 | },
35 | }
36 |
--------------------------------------------------------------------------------
/examples/counter-persist/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const webpack = require('webpack')
3 |
4 | const config = {
5 | // in development,
6 | // 'webpack-dev-server/client' and 'webpac/hot/dev-server' will be automatically added
7 | entry: [
8 | './src/index.jsx',
9 | ],
10 | output: {
11 | path: path.resolve(__dirname, 'dist'),
12 | filename: 'app.js',
13 | publicPath: '/',
14 | },
15 | resolve: {
16 | extensions: ['*', '.js', '.jsx'],
17 | },
18 | module: {
19 | rules: [
20 | {
21 | test: /\.jsx?$/,
22 | include: [
23 | path.resolve(__dirname, 'src'),
24 | ],
25 | loader: 'babel-loader',
26 | },
27 | {
28 | test: /\.css$/,
29 | use: [
30 | {
31 | loader: 'style-loader',
32 | },
33 | {
34 | loader: 'css-loader',
35 | },
36 | ],
37 | },
38 | ],
39 | },
40 | }
41 |
42 | if (process.env.NODE_ENV === 'production') {
43 | config.devtool = 'source-map'
44 | // Exclude react and react-dom in the production bundle
45 | config.externals = {
46 | react: 'React',
47 | 'react-dom': 'ReactDOM',
48 | }
49 | } else {
50 | config.devtool = 'cheap-module-source-map'
51 | config.devServer = {
52 | contentBase: path.resolve(__dirname, 'public'),
53 | clientLogLevel: 'none',
54 | quiet: true,
55 | port: 8000,
56 | compress: true,
57 | hot: true,
58 | historyApiFallback: {
59 | disableDotRule: true,
60 | },
61 | }
62 |
63 | // HMR support
64 | config.plugins = [
65 | new webpack.HotModuleReplacementPlugin(),
66 | new webpack.NamedModulesPlugin(),
67 | ]
68 | }
69 |
70 | module.exports = config
71 |
--------------------------------------------------------------------------------
/examples/counter-undo/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "es2015",
4 | "react",
5 | "stage-0"
6 | ],
7 | "plugins": [
8 | "add-module-exports",
9 | "transform-runtime"
10 | ],
11 | "env": {
12 | "test": {
13 | "plugins": [
14 | "istanbul"
15 | ]
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/examples/counter-undo/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | extends: eslint-config-airbnb
2 | parser: babel-eslint
3 |
4 | env:
5 | es6: true
6 | node: true
7 | mocha: true
8 | browser: true
9 |
10 | rules:
11 | semi:
12 | - warn
13 | - never
14 | arrow-body-style:
15 | - error
16 | - as-needed
17 | no-param-reassign:
18 | - error
19 | - props: false
20 |
21 | parserOptions:
22 | ecmaFeatures:
23 | experimentalObjectRestSpread: true
24 |
--------------------------------------------------------------------------------
/examples/counter-undo/README.md:
--------------------------------------------------------------------------------
1 | # Counter-Undo
2 |
3 | > Counter example work with [redux-undo](https://github.com/omnidan/redux-undo) for mickey
4 |
5 | ## Usage
6 |
7 | ```bash
8 | $ npm install
9 | $ npm start
10 | ```
11 |
--------------------------------------------------------------------------------
/examples/counter-undo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "counter-undo",
3 | "description": "Counter example work with redux-undo for mickey",
4 | "dependencies": {
5 | "mickey": "^0.2.4",
6 | "react": "^15.6.1",
7 | "redux-undo": "^1.0.0-beta9-9-6"
8 | },
9 | "devDependencies": {
10 | "babel-cli": "^6.26.0",
11 | "babel-core": "^6.26.0",
12 | "babel-eslint": "^7.2.3",
13 | "babel-loader": "^7.1.2",
14 | "babel-plugin-add-module-exports": "^0.2.1",
15 | "babel-plugin-transform-runtime": "^6.23.0",
16 | "babel-preset-es2015": "^6.24.1",
17 | "babel-preset-react": "^6.24.1",
18 | "babel-preset-stage-0": "^6.24.1",
19 | "cross-env": "^5.0.5",
20 | "css-loader": "^0.28.7",
21 | "eslint": "^4.5.0",
22 | "eslint-config-airbnb": "^15.1.0",
23 | "eslint-plugin-import": "^2.7.0",
24 | "eslint-plugin-jsx-a11y": "^5.1.1",
25 | "eslint-plugin-react": "^7.3.0",
26 | "husky": "^0.14.3",
27 | "rimraf": "^2.6.1",
28 | "style-loader": "^0.18.2",
29 | "webpack": "^3.5.5",
30 | "webpack-dev-server": "^2.7.1"
31 | },
32 | "scripts": {
33 | "lint": "eslint --ext .js,.jsx src",
34 | "start": "webpack-dev-server --open",
35 | "build": "cross-env NODE_ENV=production webpack -p"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/examples/counter-undo/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Counter Example - Mickey
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/examples/counter-undo/src/App.css:
--------------------------------------------------------------------------------
1 | body {
2 | height: 100vh;
3 | padding: 0;
4 | margin: 0;
5 | }
6 |
7 | #counter-app {
8 | margin: 0 auto;
9 | width: 300px;
10 | }
11 |
12 | h1 {
13 | font-size: 180px;
14 | margin: 0;
15 | text-align: center;
16 | }
17 |
18 | .btn-wrap {
19 | text-align: center;
20 | }
21 |
22 | button {
23 | background-color: #fff;
24 | border: 1px solid #a0a0a0;
25 | border-radius: 4px;
26 | font-size: 120%;
27 | outline: none;
28 | width: 60px;
29 | height: 32px;
30 | margin: 10px;
31 | cursor: pointer;
32 | }
33 |
--------------------------------------------------------------------------------
/examples/counter-undo/src/App.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 |
3 | import React from 'react'
4 | import { injectActions, connect } from 'mickey'
5 | import { ActionCreators } from 'redux-undo'
6 | import './App.css'
7 |
8 | const App = props => (
9 |
10 |
{props.count}
11 |
12 |
13 |
14 |
26 |
27 |
28 |
29 |
30 | )
31 |
32 | export default injectActions(
33 | connect((store) => {
34 | console.log('data:', store.counter)
35 | return { ...store.counter.present }
36 | })(App),
37 | )
38 |
--------------------------------------------------------------------------------
/examples/counter-undo/src/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import createApp from 'mickey'
3 |
4 | import App from './App'
5 |
6 | // 1. Initialize
7 | const app = createApp()
8 |
9 | // 2. Model
10 | app.model(require('./models/counter.js'))
11 |
12 | // 3. View
13 | app.render(, document.getElementById('root'))
14 |
--------------------------------------------------------------------------------
/examples/counter-undo/src/models/counter.js:
--------------------------------------------------------------------------------
1 | import undoable from 'redux-undo'
2 |
3 | const delay = timeout => new Promise((resolve) => {
4 | setTimeout(resolve, timeout)
5 | })
6 |
7 | export default {
8 | namespace: 'counter',
9 | state: {
10 | count: 0,
11 | loading: false,
12 | },
13 | enhancers: [
14 | reducer => (state, action) => {
15 | const undoOpts = {}
16 | const newState = undoable(reducer, undoOpts)(state, action)
17 | return { ...newState }
18 | },
19 | ],
20 | increment: state => ({ ...state, count: state.count + 1 }),
21 | decrement: state => ({ ...state, count: state.count - 1 }),
22 | incrementAsync: {
23 | * effect(payload, { call }, { succeed }) {
24 | yield call(delay, 2000)
25 | yield succeed()
26 | },
27 | prepare: state => ({ ...state, loading: true }),
28 | succeed: state => ({ ...state, count: state.count + 1, loading: false }),
29 | },
30 | }
31 |
--------------------------------------------------------------------------------
/examples/counter-undo/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const webpack = require('webpack')
3 |
4 | const config = {
5 | // in development,
6 | // 'webpack-dev-server/client' and 'webpac/hot/dev-server' will be automatically added
7 | entry: [
8 | './src/index.jsx',
9 | ],
10 | output: {
11 | path: path.resolve(__dirname, 'dist'),
12 | filename: 'app.js',
13 | publicPath: '/',
14 | },
15 | resolve: {
16 | extensions: ['*', '.js', '.jsx'],
17 | },
18 | module: {
19 | rules: [
20 | {
21 | test: /\.jsx?$/,
22 | include: [
23 | path.resolve(__dirname, 'src'),
24 | ],
25 | loader: 'babel-loader',
26 | },
27 | {
28 | test: /\.css$/,
29 | use: [
30 | {
31 | loader: 'style-loader',
32 | },
33 | {
34 | loader: 'css-loader',
35 | },
36 | ],
37 | },
38 | ],
39 | },
40 | }
41 |
42 | if (process.env.NODE_ENV === 'production') {
43 | config.devtool = 'source-map'
44 | // Exclude react and react-dom in the production bundle
45 | config.externals = {
46 | react: 'React',
47 | 'react-dom': 'ReactDOM',
48 | }
49 | } else {
50 | config.devtool = 'cheap-module-source-map'
51 | config.devServer = {
52 | contentBase: path.resolve(__dirname, 'public'),
53 | clientLogLevel: 'none',
54 | quiet: true,
55 | port: 8000,
56 | compress: true,
57 | hot: true,
58 | historyApiFallback: {
59 | disableDotRule: true,
60 | },
61 | }
62 |
63 | // HMR support
64 | config.plugins = [
65 | new webpack.HotModuleReplacementPlugin(),
66 | new webpack.NamedModulesPlugin(),
67 | ]
68 | }
69 |
70 | module.exports = config
71 |
--------------------------------------------------------------------------------
/examples/counter/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "es2015",
4 | "react",
5 | "stage-0"
6 | ],
7 | "plugins": [
8 | "add-module-exports",
9 | "transform-runtime"
10 | ],
11 | "env": {
12 | "test": {
13 | "plugins": [
14 | "istanbul"
15 | ]
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/examples/counter/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | extends: eslint-config-airbnb
2 | parser: babel-eslint
3 |
4 | env:
5 | es6: true
6 | node: true
7 | mocha: true
8 | browser: true
9 |
10 | rules:
11 | semi:
12 | - warn
13 | - never
14 | arrow-body-style:
15 | - error
16 | - as-needed
17 | no-param-reassign:
18 | - error
19 | - props: false
20 |
21 | parserOptions:
22 | ecmaFeatures:
23 | experimentalObjectRestSpread: true
24 |
--------------------------------------------------------------------------------
/examples/counter/README.md:
--------------------------------------------------------------------------------
1 | # Counter
2 |
3 | > Counter example for mickey
4 |
5 | ## Usage
6 |
7 | ```bash
8 | $ npm install
9 | $ npm start
10 | ```
11 |
--------------------------------------------------------------------------------
/examples/counter/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "counter",
3 | "description": "Counter example for mickey",
4 | "dependencies": {
5 | "mickey": "^1.3.2",
6 | "react": "^15.6.1"
7 | },
8 | "devDependencies": {
9 | "babel-cli": "^6.26.0",
10 | "babel-core": "^6.26.0",
11 | "babel-eslint": "^7.2.3",
12 | "babel-loader": "^7.1.2",
13 | "babel-plugin-add-module-exports": "^0.2.1",
14 | "babel-plugin-transform-runtime": "^6.23.0",
15 | "babel-preset-es2015": "^6.24.1",
16 | "babel-preset-react": "^6.24.1",
17 | "babel-preset-stage-0": "^6.24.1",
18 | "cross-env": "^5.0.5",
19 | "css-loader": "^0.28.7",
20 | "eslint": "^4.5.0",
21 | "eslint-config-airbnb": "^15.1.0",
22 | "eslint-plugin-import": "^2.7.0",
23 | "eslint-plugin-jsx-a11y": "^5.1.1",
24 | "eslint-plugin-react": "^7.3.0",
25 | "husky": "^0.14.3",
26 | "rimraf": "^2.6.1",
27 | "style-loader": "^0.18.2",
28 | "webpack": "^3.5.5",
29 | "webpack-dev-server": "^2.7.1"
30 | },
31 | "scripts": {
32 | "lint": "eslint --ext .js,.jsx src",
33 | "start": "webpack-dev-server --open",
34 | "build": "cross-env NODE_ENV=production webpack -p"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/examples/counter/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Counter Example - Mickey
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/examples/counter/src/App.css:
--------------------------------------------------------------------------------
1 | body {
2 | height: 100vh;
3 | padding: 0;
4 | margin: 0;
5 | }
6 |
7 | #counter-app {
8 | margin: 0 auto;
9 | width: 300px;
10 | }
11 |
12 | h1 {
13 | font-size: 180px;
14 | margin: 0;
15 | text-align: center;
16 | }
17 |
18 | .btn-wrap {
19 | text-align: center;
20 | }
21 |
22 | button {
23 | background-color: #fff;
24 | border: 1px solid #a0a0a0;
25 | border-radius: 4px;
26 | font-size: 120%;
27 | outline: none;
28 | width: 60px;
29 | height: 32px;
30 | margin: 0 10px;
31 | cursor: pointer;
32 | }
33 |
--------------------------------------------------------------------------------
/examples/counter/src/App.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 |
3 | import React from 'react'
4 | import { connect, injectActions } from 'mickey'
5 | import './App.css'
6 |
7 | const App = props => (
8 |
9 |
{props.count}
10 |
11 |
12 |
13 |
25 |
26 |
27 | )
28 |
29 | export default injectActions(connect(store => ({ ...store.counter }))(App))
30 |
--------------------------------------------------------------------------------
/examples/counter/src/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import createApp from 'mickey'
3 | import App from './App'
4 |
5 | // 1. Initialize
6 | const app = createApp()
7 |
8 | // 2. Model
9 | app.model(require('./models/counter.js'))
10 |
11 | // 3. View
12 | app.render(, document.getElementById('root'))
13 |
--------------------------------------------------------------------------------
/examples/counter/src/models/counter.js:
--------------------------------------------------------------------------------
1 | const delay = timeout => new Promise((resolve) => {
2 | setTimeout(resolve, timeout)
3 | })
4 |
5 | export default {
6 | namespace: 'counter',
7 | state: {
8 | count: 0,
9 | loading: false,
10 | },
11 | increment: state => ({ ...state, count: state.count + 1 }),
12 | decrement: state => ({ ...state, count: state.count - 1 }),
13 | incrementAsync: {
14 | * effect(payload, { call }, { succeed }) {
15 | yield call(delay, 2000)
16 | yield succeed()
17 | },
18 | prepare: state => ({ ...state, loading: true }),
19 | succeed: state => ({ ...state, count: state.count + 1, loading: false }),
20 | },
21 | }
22 |
--------------------------------------------------------------------------------
/examples/counter/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const webpack = require('webpack')
3 |
4 | const config = {
5 | // in development,
6 | // 'webpack-dev-server/client' and 'webpac/hot/dev-server' will be automatically added
7 | entry: [
8 | './src/index.jsx',
9 | ],
10 | output: {
11 | path: path.resolve(__dirname, 'dist'),
12 | filename: 'app.js',
13 | publicPath: '/',
14 | },
15 | resolve: {
16 | extensions: ['*', '.js', '.jsx'],
17 | },
18 | module: {
19 | rules: [
20 | {
21 | test: /\.jsx?$/,
22 | include: [
23 | path.resolve(__dirname, 'src'),
24 | ],
25 | loader: 'babel-loader',
26 | },
27 | {
28 | test: /\.css$/,
29 | use: [
30 | {
31 | loader: 'style-loader',
32 | },
33 | {
34 | loader: 'css-loader',
35 | },
36 | ],
37 | },
38 | ],
39 | },
40 | }
41 |
42 | if (process.env.NODE_ENV === 'production') {
43 | config.devtool = 'source-map'
44 | // Exclude react and react-dom in the production bundle
45 | config.externals = {
46 | react: 'React',
47 | 'react-dom': 'ReactDOM',
48 | }
49 | } else {
50 | config.devtool = 'cheap-module-source-map'
51 | config.devServer = {
52 | contentBase: path.resolve(__dirname, 'public'),
53 | clientLogLevel: 'none',
54 | quiet: true,
55 | port: 8000,
56 | compress: true,
57 | hot: true,
58 | historyApiFallback: {
59 | disableDotRule: true,
60 | },
61 | }
62 |
63 | // HMR support
64 | config.plugins = [
65 | new webpack.HotModuleReplacementPlugin(),
66 | new webpack.NamedModulesPlugin(),
67 | ]
68 | }
69 |
70 | module.exports = config
71 |
--------------------------------------------------------------------------------
/examples/simple-router/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "es2015",
4 | "react",
5 | "stage-0"
6 | ],
7 | "plugins": [
8 | "add-module-exports",
9 | "transform-runtime"
10 | ],
11 | "env": {
12 | "test": {
13 | "plugins": [
14 | "istanbul"
15 | ]
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/examples/simple-router/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | extends: eslint-config-airbnb
2 | parser: babel-eslint
3 |
4 | env:
5 | es6: true
6 | node: true
7 | mocha: true
8 | browser: true
9 |
10 | rules:
11 | semi:
12 | - warn
13 | - never
14 | arrow-body-style:
15 | - error
16 | - as-needed
17 | no-param-reassign:
18 | - error
19 | - props: false
20 |
21 | parserOptions:
22 | ecmaFeatures:
23 | experimentalObjectRestSpread: true
24 |
--------------------------------------------------------------------------------
/examples/simple-router/README.md:
--------------------------------------------------------------------------------
1 | # Counter
2 |
3 | > Counter example for mickey
4 |
5 | ## Usage
6 |
7 | ```bash
8 | $ npm install
9 | $ npm start
10 | ```
11 |
--------------------------------------------------------------------------------
/examples/simple-router/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "counter",
3 | "description": "Counter example for mickey",
4 | "dependencies": {
5 | "react": "^15.6.1"
6 | },
7 | "devDependencies": {
8 | "babel-cli": "^6.26.0",
9 | "babel-core": "^6.26.0",
10 | "babel-eslint": "^7.2.3",
11 | "babel-loader": "^7.1.2",
12 | "babel-plugin-add-module-exports": "^0.2.1",
13 | "babel-plugin-transform-runtime": "^6.23.0",
14 | "babel-preset-es2015": "^6.24.1",
15 | "babel-preset-react": "^6.24.1",
16 | "babel-preset-stage-0": "^6.24.1",
17 | "cross-env": "^5.0.5",
18 | "css-loader": "^0.28.7",
19 | "eslint": "^4.5.0",
20 | "eslint-config-airbnb": "^15.1.0",
21 | "eslint-plugin-import": "^2.7.0",
22 | "eslint-plugin-jsx-a11y": "^5.1.1",
23 | "eslint-plugin-react": "^7.3.0",
24 | "husky": "^0.14.3",
25 | "rimraf": "^2.6.1",
26 | "style-loader": "^0.18.2",
27 | "webpack": "^3.5.5",
28 | "webpack-dev-server": "^2.7.1"
29 | },
30 | "scripts": {
31 | "lint": "eslint --ext .js,.jsx src",
32 | "start": "webpack-dev-server --open",
33 | "build": "cross-env NODE_ENV=production webpack -p"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/examples/simple-router/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Counter Example - Mickey
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/examples/simple-router/src/Router.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 |
3 | import React from 'react'
4 | import { HashRouter as Router, Route, Switch } from '../../../lib'
5 | import Header from './components/Header'
6 | import Home from './components/Home'
7 | import About from './components/About'
8 | import Topics from './components/Topics'
9 |
10 | const Routers = () => (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | )
25 |
26 | export default Routers
27 |
--------------------------------------------------------------------------------
/examples/simple-router/src/components/About.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const About = () => (
4 |
5 |
About
6 |
7 | )
8 |
9 | export default About
10 |
--------------------------------------------------------------------------------
/examples/simple-router/src/components/AddTopic.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 |
3 | import React from 'react'
4 | import { injectActions } from '../../../../lib'
5 |
6 | const AddTopic = ({ actions }) => {
7 | let input
8 |
9 | const submit = () => {
10 | if (input.value.trim()) {
11 | actions.topics.add(input.value)
12 | input.value = ''
13 | }
14 | }
15 |
16 | return (
17 |
18 | { input = elem }} />
19 |
20 |
21 | )
22 | }
23 |
24 | export default injectActions(AddTopic)
25 |
--------------------------------------------------------------------------------
/examples/simple-router/src/components/Header.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Link } from '../../../../lib'
3 |
4 | const Header = () => (
5 |
6 |
13 |
14 | )
15 |
16 | export default Header
17 |
--------------------------------------------------------------------------------
/examples/simple-router/src/components/Home.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Home = () => (
4 |
5 |
Home
6 |
7 | )
8 |
9 | export default Home
10 |
--------------------------------------------------------------------------------
/examples/simple-router/src/components/Topic.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 |
3 | import React from 'react'
4 | import { injectActions } from '../../../../lib'
5 |
6 | const Topic = ({ topic, actions }) => (
7 |
8 |
{topic || 'Topic not found'}
9 |
10 |
11 | )
12 |
13 | export default injectActions(Topic)
14 |
--------------------------------------------------------------------------------
/examples/simple-router/src/components/Topics.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 |
3 | import React from 'react'
4 | import { Route, Link, connect } from '../../../../lib'
5 | import Topic from './Topic'
6 | import AddTopic from './AddTopic'
7 |
8 | const Topics = ({ topics }) => (
9 |
10 |
Topics
11 |
12 | {
13 | topics.map((topic, idx) => (
14 | -
15 | {topic}
16 |
17 | ))
18 | }
19 |
20 |
(
23 | idx === match.params.topicId)} />
24 | )}
25 | />
26 |
27 |
28 | )
29 |
30 | export default connect(({ topics }) => ({ topics }))(Topics)
31 |
--------------------------------------------------------------------------------
/examples/simple-router/src/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import createApp from '../../../lib'
3 | import Router from './Router'
4 |
5 | // 1. Initialize
6 | const app = createApp({ historyMode: 'hash' })
7 |
8 | // 2. Model
9 | app.model({
10 | namespace: 'topics',
11 | state: [
12 | 'foo',
13 | ],
14 | add: (state, topic) => [...state, topic],
15 | })
16 |
17 | // 3. View
18 | app.render(, document.getElementById('root'))
19 |
20 | console.log(app) // eslint-disable-line
21 |
--------------------------------------------------------------------------------
/examples/simple-router/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const webpack = require('webpack')
3 |
4 | const config = {
5 | // in development,
6 | // 'webpack-dev-server/client' and 'webpac/hot/dev-server' will be automatically added
7 | entry: [
8 | './src/index.jsx',
9 | ],
10 | output: {
11 | path: path.resolve(__dirname, 'dist'),
12 | filename: 'app.js',
13 | publicPath: '/',
14 | },
15 | resolve: {
16 | extensions: ['*', '.js', '.jsx'],
17 | },
18 | module: {
19 | rules: [
20 | {
21 | test: /\.jsx?$/,
22 | include: [
23 | path.resolve(__dirname, 'src'),
24 | ],
25 | loader: 'babel-loader',
26 | },
27 | {
28 | test: /\.css$/,
29 | use: [
30 | {
31 | loader: 'style-loader',
32 | },
33 | {
34 | loader: 'css-loader',
35 | },
36 | ],
37 | },
38 | ],
39 | },
40 | }
41 |
42 | if (process.env.NODE_ENV === 'production') {
43 | config.devtool = 'source-map'
44 | // Exclude react and react-dom in the production bundle
45 | config.externals = {
46 | react: 'React',
47 | 'react-dom': 'ReactDOM',
48 | }
49 | } else {
50 | config.devtool = 'cheap-module-source-map'
51 | config.devServer = {
52 | contentBase: path.resolve(__dirname, 'public'),
53 | clientLogLevel: 'none',
54 | quiet: true,
55 | port: 8000,
56 | compress: true,
57 | hot: true,
58 | historyApiFallback: {
59 | disableDotRule: true,
60 | },
61 | }
62 |
63 | // HMR support
64 | config.plugins = [
65 | new webpack.HotModuleReplacementPlugin(),
66 | new webpack.NamedModulesPlugin(),
67 | ]
68 | }
69 |
70 | module.exports = config
71 |
--------------------------------------------------------------------------------
/mickey.svg:
--------------------------------------------------------------------------------
1 |
2 |
43 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mickey",
3 | "version": "2.0.0-alpha.1",
4 | "description": "Lightweight front-end framework for creating React and Redux based app painlessly.",
5 | "main": "./lib/index.js",
6 | "keywords": [
7 | "mickey",
8 | "react",
9 | "redux",
10 | "redux-saga",
11 | "elm",
12 | "framework",
13 | "frontend"
14 | ],
15 | "author": {
16 | "name": "bubkoo",
17 | "email": "bubkoo.wy@gmail.com"
18 | },
19 | "license": "MIT",
20 | "repository": {
21 | "type": "git",
22 | "url": "git+https://github.com/bubkoo/mickey.git"
23 | },
24 | "bugs": {
25 | "url": "https://github.com/bubkoo/mickey/issues"
26 | },
27 | "homepage": "https://github.com/bubkoo/mickey#readme",
28 | "peerDependencies": {
29 | "react": "*",
30 | "prop-types": "*",
31 | "react-dom": "*"
32 | },
33 | "dependencies": {
34 | "connected-react-router": "^4.4.1",
35 | "global": "^4.3.2",
36 | "history": "^4.7.2",
37 | "invariant": "^2.2.4",
38 | "is-plain-object": "^2.0.4",
39 | "lodash.flatten": "^4.4.0",
40 | "lodash.get": "^4.4.2",
41 | "lodash.isfunction": "^3.0.9",
42 | "minimatch": "^3.0.4",
43 | "object-assign": "^4.1.1",
44 | "react-redux": "^5.0.7",
45 | "react-router": "^4.3.1",
46 | "react-router-dom": "^4.3.1",
47 | "redux": "^4.0.0",
48 | "redux-actions": "^2.6.1",
49 | "redux-devtools-extension": "^2.13.7",
50 | "redux-saga": "^0.16.0",
51 | "warning": "^4.0.2"
52 | },
53 | "devDependencies": {
54 | "babel-cli": "^6.26.0",
55 | "babel-eslint": "^7.2.3",
56 | "babel-plugin-add-module-exports": "^0.2.1",
57 | "babel-plugin-istanbul": "^4.1.4",
58 | "babel-plugin-transform-decorators-legacy": "^1.3.4",
59 | "babel-plugin-transform-runtime": "^6.23.0",
60 | "babel-preset-env": "^1.6.1",
61 | "babel-preset-react": "^6.24.1",
62 | "babel-preset-stage-0": "^6.24.1",
63 | "babel-register": "^6.26.0",
64 | "chai": "^4.1.2",
65 | "coveralls": "^2.13.1",
66 | "cross-env": "^5.0.5",
67 | "eslint": "^4.11.0",
68 | "eslint-config-airbnb": "^15.1.0",
69 | "eslint-plugin-import": "^2.7.0",
70 | "eslint-plugin-jsx-a11y": "^5.1.1",
71 | "eslint-plugin-react": "^7.3.0",
72 | "husky": "^0.14.3",
73 | "jsdom": "^11.2.0",
74 | "mocha": "^3.5.0",
75 | "nyc": "^11.1.0",
76 | "react": "^16.3.2",
77 | "react-dom": "^16.3.2",
78 | "rimraf": "^2.6.1",
79 | "sinon": "^4.1.2"
80 | },
81 | "files": [
82 | "lib",
83 | "src",
84 | "*.d.ts"
85 | ],
86 | "nyc": {
87 | "include": [
88 | "src/**/*.js"
89 | ],
90 | "reporter": [
91 | "html",
92 | "text"
93 | ],
94 | "require": [
95 | "babel-register",
96 | "./test/setup.js"
97 | ],
98 | "report-dir": "./test/coverage",
99 | "sourceMap": false,
100 | "instrument": false
101 | },
102 | "scripts": {
103 | "test": "cross-env NODE_ENV=test nyc mocha ./test/index.js",
104 | "coveralls": "nyc report --reporter=text-lcov | coveralls",
105 | "lint": "eslint --ext .js,.jsx src",
106 | "build": "rimraf lib && babel src --out-dir lib",
107 | "prebuild": "npm run lint",
108 | "prepare": "npm run lint && npm run build && npm run test",
109 | "precommit": "npm run lint"
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/ActionsProvider.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | export function createProvider(actionsKey = 'actions') {
5 | class Provider extends React.Component {
6 | constructor(props, context) {
7 | super(props, context)
8 | this[actionsKey] = props.actions
9 | }
10 |
11 | getChildContext() {
12 | return { [actionsKey]: this[actionsKey] }
13 | }
14 |
15 | render() {
16 | return React.Children.only(this.props.children)
17 | }
18 | }
19 |
20 | Provider.propTypes = {
21 | actions: PropTypes.object.isRequired, // eslint-disable-line
22 | children: PropTypes.element.isRequired,
23 | }
24 |
25 | Provider.childContextTypes = {
26 | [actionsKey]: PropTypes.object.isRequired,
27 | }
28 |
29 | return Provider
30 | }
31 |
32 | export default createProvider()
33 |
--------------------------------------------------------------------------------
/src/Plugin.js:
--------------------------------------------------------------------------------
1 | import invariant from 'invariant'
2 | import { isPlainObject, getEnhancer } from './utils'
3 |
4 |
5 | export default class Plugin {
6 | static hooks = [
7 | 'onError',
8 | 'onEffect',
9 | 'onStateChange',
10 |
11 | 'onAction',
12 | 'onReducer',
13 | 'extraReducers',
14 | 'extraEnhancers',
15 | ]
16 |
17 | constructor() {
18 | this.hooks = Plugin.hooks.reduce((memo, name) => {
19 | memo[name] = []
20 | return memo
21 | }, {})
22 | }
23 |
24 | use(plugins) {
25 | invariant(isPlainObject(plugins), 'hook should be plain object')
26 |
27 | Object.keys(plugins).forEach((key) => {
28 | invariant(this.hooks[key], `unknown hook property: "${key}"`)
29 | const plugin = plugins[key]
30 | if (plugin) {
31 | if (key === 'extraEnhancers') {
32 | this.hooks[key] = plugin
33 | } else if (Array.isArray(plugin)) {
34 | this.hooks[key].push(...plugin)
35 | } else {
36 | this.hooks[key].push(plugin)
37 | }
38 | }
39 | })
40 |
41 | return this
42 | }
43 |
44 | apply(name, defaultHandler) {
45 | const handlers = this.hooks[name]
46 | const validHooks = ['onError']
47 |
48 | invariant(validHooks.includes(name), `plugin.apply: hook "${name}" cannot be applied`)
49 |
50 | return (...args) => {
51 | if (handlers.length) {
52 | handlers.forEach(fn => fn(...args))
53 | } else if (defaultHandler) {
54 | defaultHandler(...args)
55 | }
56 | }
57 | }
58 |
59 | get(name) {
60 | const handlers = this.hooks[name]
61 | invariant(handlers, `hook "${name}" cannot be got`)
62 |
63 | if (name === 'extraReducers') {
64 | return handlers.reduce((memo, reducer) => ({ ...memo, ...reducer }), {})
65 | } else if (name === 'onReducer') {
66 | return getEnhancer(handlers)
67 | }
68 |
69 | return handlers
70 | }
71 | }
72 |
73 |
--------------------------------------------------------------------------------
/src/Provider.js:
--------------------------------------------------------------------------------
1 | import warning from 'warning'
2 | import React, { createElement } from 'react'
3 | import PropTypes from 'prop-types'
4 | import { Provider as StoreProvider } from 'react-redux'
5 | import { ConnectedRouter, routerActions } from 'connected-react-router'
6 | import ActionsProvider from './ActionsProvider'
7 |
8 |
9 | export default class Provider extends React.Component {
10 | static propTypes = {
11 | children: PropTypes.element.isRequired,
12 | app: PropTypes.shape({
13 | store: PropTypes.object.isRequired,
14 | actions: PropTypes.object.isRequired,
15 | history: PropTypes.object,
16 | }).isRequired,
17 | }
18 |
19 | addRouterActions() {
20 | const { app } = this.props
21 | if (app.history) {
22 | // Add `push`, `replace`, `go`, `goForward` and `goBack` methods to actions.router,
23 | // when called, will dispatch the crresponding action provided by connected-react-router.
24 | app.actions.router = Object.keys(routerActions).reduce((memo, action) => ({
25 | ...memo,
26 | [action]: (...args) => {
27 | app.store.dispatch(routerActions[action](...args))
28 | },
29 | }), {})
30 |
31 | app.actions.routing = Object.keys(routerActions).reduce((memo, action) => ({
32 | ...memo,
33 | [action]: (...args) => {
34 | if (process.env.NODE_ENV !== 'production') {
35 | warning(
36 | false,
37 | '\'app.actions.routing\' is deprecated, use \'app.actions.router\' instead.',
38 | )
39 | }
40 |
41 | app.store.dispatch(routerActions[action](...args))
42 | },
43 | }), {})
44 | }
45 | }
46 |
47 | renderProvider(Component) {
48 | const { app } = this.props
49 |
50 | return createElement(ActionsProvider, { actions: app.actions },
51 | createElement(StoreProvider, { store: app.store },
52 | Component,
53 | ),
54 | )
55 | }
56 |
57 | render() {
58 | const { children, app } = this.props
59 | const child = React.Children.only(children)
60 | this.addRouterActions()
61 | return this.renderProvider(
62 | app.history
63 | ? createElement(ConnectedRouter, { history: app.history }, child)
64 | : child,
65 | )
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/actions.js:
--------------------------------------------------------------------------------
1 | import { NAMESPACE_SEP } from './constants'
2 | import { prefixType } from './utils'
3 |
4 | export function getModelActions(model, dispatch) {
5 | const { actions, namespace } = model
6 | return Object.keys(actions).reduce((memo, methodName) => ({
7 | ...memo,
8 | [methodName]: payload => dispatch({
9 | payload,
10 | type: prefixType(namespace, actions[methodName]),
11 | }),
12 | }), {})
13 | }
14 |
15 | function createActions(app, model) {
16 | const { actions, namespace } = model
17 | return Object.keys(actions).reduce((memo, methodName) => ({
18 | ...memo,
19 | // dispatch should be ready after the app started
20 | [methodName]: payload => app.store.dispatch({
21 | payload,
22 | type: prefixType(namespace, actions[methodName]),
23 | }),
24 | }), {})
25 | }
26 |
27 | export function addActions(app, model) {
28 | const { actions } = app
29 | const { namespace } = model
30 | const nss = namespace.split(NAMESPACE_SEP)
31 | let temp = actions
32 | nss.forEach((ns) => {
33 | if (!temp[ns]) {
34 | temp[ns] = {}
35 | }
36 | temp = temp[ns]
37 | })
38 |
39 | Object.assign(temp, createActions(app, model))
40 |
41 | return actions
42 | }
43 |
44 | export function removeActions(app, namespace) {
45 | const { actions } = app
46 | const nss = namespace.split(NAMESPACE_SEP)
47 | const lastIndex = nss.length - 1
48 |
49 | let temp = actions
50 | let removed = null
51 |
52 | nss.forEach((ns, index) => {
53 | if (index === lastIndex) {
54 | removed = temp[ns]
55 | delete temp[ns]
56 | } else if (temp) {
57 | temp = temp[ns]
58 | }
59 | })
60 |
61 | // clean sub tree
62 | for (let i = 0; i < lastIndex; i += 1) {
63 | temp = actions
64 | nss.some((ns) => { // eslint-disable-line
65 | if (temp && temp[ns]) {
66 | if (Object.keys(temp[ns]).length === 0) {
67 | delete temp[ns]
68 | return true
69 | }
70 | temp = temp[ns]
71 | return false
72 | }
73 |
74 | return true
75 | })
76 | }
77 |
78 | return removed
79 | }
80 |
--------------------------------------------------------------------------------
/src/baseModel.js:
--------------------------------------------------------------------------------
1 | import { MUTATE } from './constants'
2 |
3 | export default {
4 | [MUTATE]: (state, payload) => ({
5 | ...state,
6 | ...payload,
7 | }),
8 | }
9 |
--------------------------------------------------------------------------------
/src/checkModel.js:
--------------------------------------------------------------------------------
1 | import invariant from 'invariant'
2 |
3 | export default function checkModel(model, models) {
4 | const { namespace } = model
5 |
6 | invariant(
7 | namespace,
8 | 'app.model: namespace should be specified',
9 | )
10 |
11 | invariant(
12 | typeof namespace === 'string',
13 | `app.model: namespace should be string, but got ${typeof namespace}`,
14 | )
15 |
16 | invariant(
17 | !models.some(m => m.namespace === namespace),
18 | 'app.model: namespace should be unique',
19 | )
20 | }
21 |
22 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | export const MUTATE = 'mutate'
2 | export const NAMESPACE_SEP = '/'
3 | export const CANCEL_EFFECTS = '@@CANCEL_EFFECTS'
4 | export const MICKEY_UPDATE = '@@MICKEY/UPDATE'
5 |
--------------------------------------------------------------------------------
/src/createApp.js:
--------------------------------------------------------------------------------
1 | import Plugin from './Plugin'
2 | import getSaga from './getSaga'
3 | import getReducer from './getReducer'
4 | import createStore from './createStore'
5 | import createRender from './createRender'
6 | import createModel from './createModel'
7 | import registerModel from './registerModel'
8 | import internalModel from './internalModel'
9 | import createReducer from './createReducer'
10 | import createHistory from './createHistory'
11 | import { removeActions } from './actions'
12 | import createErrorHandler from './createErrorHandler'
13 | import createPromiseMiddleware from './createPromiseMiddleware'
14 | import { startWatchers, stopWatchers } from './watcher'
15 | import { CANCEL_EFFECTS, MICKEY_UPDATE } from './constants'
16 | import { prefixType, fixNamespace } from './utils'
17 |
18 |
19 | export default function createApp(options = {}) {
20 | const {
21 | hooks = {},
22 | historyMode,
23 | initialState = {},
24 | initialReducer = {},
25 | extensions = {},
26 | } = options
27 |
28 | // supportted extensions
29 | const { createReducer: reducerCreator, combineReducers } = extensions
30 | // use `options.history` or create one with `historyMode`
31 | const history = options.history || createHistory(historyMode)
32 |
33 | const app = {}
34 | const plugin = (new Plugin()).use(hooks)
35 | const regModel = m => (registerModel(app, m))
36 |
37 | return Object.assign(app, {
38 | plugin,
39 | history,
40 | actions: {},
41 | models: [createModel({ ...internalModel })],
42 |
43 | // check the namespace available or not
44 | has(namespace) {
45 | namespace = fixNamespace(namespace) // eslint-disable-line
46 | return app.models.some(m => m.namespace === namespace)
47 | },
48 |
49 | // register model before app is started
50 | model(raw) {
51 | regModel(raw)
52 | return app
53 | },
54 |
55 | eject(namespace) {
56 | namespace = fixNamespace(namespace) // eslint-disable-line
57 | app.models = app.models.filter(m => m.namespace !== namespace)
58 | removeActions(app, namespace)
59 |
60 | return app
61 | },
62 |
63 | load() {
64 | throw new Error('The method `load(pattern)` is unavailable. This method depend on `babel-plugin-mickey-model-loader`. For more information, see: https://github.com/mickeyjsx/babel-plugin-mickey-model-loader')
65 | },
66 |
67 | // create store, steup reducer, start the app
68 | render(component, container, callback) {
69 | const onError = createErrorHandler(app)
70 | const onEffect = plugin.get('onEffect')
71 | const extraReducers = plugin.get('extraReducers')
72 | const reducerEnhancer = plugin.get('onReducer')
73 |
74 | const sagas = []
75 | const reducers = { ...initialReducer }
76 |
77 | const innerGetSaga = m => getSaga(onError, onEffect, app, m)
78 | const innerGetReducer = m => getReducer(reducerCreator, m)
79 | const innerCreateReducer = asyncReducers => createReducer({
80 | reducers,
81 | asyncReducers,
82 | extraReducers,
83 | reducerEnhancer,
84 | combineReducers,
85 | history: app.history,
86 | })
87 |
88 | // handle reducers and sagas in model
89 | app.models.forEach((model) => {
90 | reducers[model.namespace] = innerGetReducer(model)
91 | sagas.push(innerGetSaga(model))
92 | })
93 |
94 | // create store
95 | const store = app.store = createStore({ // eslint-disable-line
96 | plugin,
97 | options,
98 | initialState,
99 | history: app.history,
100 | reducers: innerCreateReducer(),
101 | promiseMiddleware: createPromiseMiddleware(app),
102 | extraMiddlewares: plugin.get('onAction'),
103 | onStateChange: plugin.get('onStateChange'),
104 | extraEnhancers: plugin.get('extraEnhancers'),
105 | })
106 |
107 | // run sagas
108 | sagas.forEach(store.runSaga)
109 |
110 | const render = createRender(app, component, container, callback)
111 | render(component, container, callback)
112 |
113 | // run watcher after render
114 | const unlisteners = {}
115 | app.models.forEach((model) => {
116 | unlisteners[model.namespace] = startWatchers(model, app, onError)
117 | })
118 |
119 | // replace and inject some methods after the app started
120 | return Object.assign(app, {
121 | // after the first call fo render, the render function
122 | // should only do pure-render with any other actions
123 | render,
124 | onError,
125 |
126 | // inject model after app is started
127 | model(raw) {
128 | const namespace = fixNamespace(raw.namespace)
129 |
130 | // clean the old one when exists
131 | if (app.has(namespace)) {
132 | stopWatchers(unlisteners, namespace)
133 | removeActions(app, namespace)
134 | app.models = app.models.filter(m => m.namespace !== namespace)
135 | store.dispatch({ type: prefixType(namespace, CANCEL_EFFECTS) })
136 | store.dispatch({ type: MICKEY_UPDATE })
137 | }
138 |
139 | const model = regModel(raw)
140 |
141 | store.asyncReducers[namespace] = innerGetReducer(model)
142 | store.replaceReducer(innerCreateReducer(store.asyncReducers))
143 | store.runSaga(innerGetSaga(model))
144 |
145 | unlisteners[namespace] = startWatchers(model, app, onError)
146 |
147 | return app
148 | },
149 |
150 | // remove model
151 | eject(namespace) {
152 | namespace = fixNamespace(namespace) // eslint-disable-line
153 | delete store.asyncReducers[namespace]
154 | delete reducers[namespace]
155 |
156 | // The pattern we recommend is to keep the old reducers around, so there's a warning
157 | // ref: https://stackoverflow.com/questions/34095804/replacereducer-causing-unexpected-key-error
158 | store.replaceReducer(innerCreateReducer(store.asyncReducers))
159 | store.dispatch({ type: MICKEY_UPDATE })
160 | store.dispatch({ type: prefixType(namespace, CANCEL_EFFECTS) })
161 |
162 | stopWatchers(unlisteners, namespace)
163 |
164 | app.models = app.models.filter(m => m.namespace !== namespace)
165 | removeActions(app, namespace)
166 |
167 | return app
168 | },
169 | })
170 | },
171 | })
172 | }
173 |
--------------------------------------------------------------------------------
/src/createErrorHandler.js:
--------------------------------------------------------------------------------
1 | const normalize = (err) => {
2 | if (typeof err === 'string') {
3 | return new Error(err)
4 | }
5 | return err
6 | }
7 |
8 | const defaultErrorHandler = (err) => { throw err }
9 |
10 | export default function createErrorHandler(app) {
11 | return (err) => {
12 | if (err) {
13 | const handler = app.plugin.apply('onError', defaultErrorHandler)
14 | handler(normalize(err), app)
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/createHistory.js:
--------------------------------------------------------------------------------
1 | import invariant from 'invariant'
2 | import {
3 | createHashHistory,
4 | createMemoryHistory,
5 | createBrowserHistory,
6 | } from 'history'
7 |
8 | const historyModes = ['browser', 'hash', 'memory']
9 |
10 | export default function createHistory(historyMode) {
11 | if (historyMode) {
12 | if (process.env.NODE_ENV !== 'production') {
13 | invariant(
14 | historyModes.includes(historyMode),
15 | `historyMode "${historyMode}" is invalid, must be one of ${historyModes.join(', ')}!`,
16 | )
17 | }
18 |
19 | if (historyMode === 'hash') {
20 | return createHashHistory()
21 | } else if (historyMode === 'browser') {
22 | return createBrowserHistory()
23 | } else if (historyMode === 'memory') {
24 | return createMemoryHistory()
25 | }
26 | }
27 |
28 | return null
29 | }
30 |
--------------------------------------------------------------------------------
/src/createModel.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-underscore-dangle */
2 | import invariant from 'invariant'
3 | import baseModel from './baseModel'
4 | import { MUTATE } from './constants'
5 | import {
6 | ucfirst,
7 | prefixType,
8 | prefixObject,
9 | isArray,
10 | isFunction,
11 | isGeneratorFn,
12 | fixNamespace,
13 | } from './utils'
14 |
15 |
16 | function isEffect(method) {
17 | // if a method is generator function then it should be an effect.
18 | return isGeneratorFn(method)
19 | || (isArray(method) && isGeneratorFn(method[0])) // [ *effect(){}, type ]
20 | }
21 |
22 | function createEmptyGroup(type) {
23 | return {
24 | type,
25 | actions: {},
26 | effects: {},
27 | reducers: {},
28 | callbacks: [],
29 | effectCount: 0,
30 | }
31 | }
32 |
33 | function fillGroup(group, type, method, callback) {
34 | const { actions, effects, reducers, callbacks } = group
35 | if (typeof method === 'object' && !isArray(method)) {
36 | return false
37 | }
38 |
39 | if (isEffect(method)) {
40 | actions[type] = type
41 | effects[type] = method
42 | group.effectCount += 1
43 | return true
44 | } else if (isFunction(method)) {
45 | if (callback && callback !== 'prepare') {
46 | const prefixed = prefixType(type, callback) // query/succeed
47 | actions[type + ucfirst(callback)] = prefixed // { querySucceed: 'query/scuueed' }
48 | reducers[prefixed] = method
49 | callbacks.push(callback)
50 | } else {
51 | actions[type] = type
52 | reducers[type] = method
53 | }
54 | return true
55 | }
56 |
57 | return false
58 | }
59 |
60 | function parseGroups(raw, namespace) {
61 | const keys = Object.keys(raw)
62 | const groups = keys.map((type) => {
63 | const section = raw[type]
64 | const group = createEmptyGroup(type)
65 |
66 | if (!fillGroup(group, type, section)) {
67 | Object.keys(section).forEach(name => fillGroup(group, type, section[name], name))
68 | }
69 |
70 | if (process.env.NODE_ENV !== 'production') {
71 | invariant(
72 | group.effectCount <= 1,
73 | `Less than one effect function should be specified in model "${namespace}" with action name "${type}".`,
74 | )
75 | }
76 |
77 | return group
78 | })
79 |
80 | if (process.env.NODE_ENV !== 'production') {
81 | const exist = keys.some(key => (key === MUTATE))
82 | invariant(
83 | !exist,
84 | `The \`mutate\` is a reserved action for mutate the state. You should change \`mutate\` to other action names in model "${namespace}".`,
85 | )
86 | }
87 |
88 | // extend model with `mutate` reducer
89 | groups.push({
90 | type: MUTATE,
91 | actions: { [MUTATE]: MUTATE },
92 | effects: {},
93 | reducers: { [MUTATE]: baseModel[MUTATE] },
94 | callbacks: [],
95 | effectCount: 0,
96 | })
97 |
98 | return groups
99 | }
100 |
101 | function parseWatcher(watcher) {
102 | if (watcher) {
103 | const watchers = isArray(watcher) // eslint-disable-line
104 | ? [...watcher] : isFunction(watcher)
105 | ? [watcher]
106 | : Object.keys(watcher).map(key => watcher[key])
107 |
108 | return watchers.filter(fn => isFunction(fn))
109 | }
110 |
111 | return null
112 | }
113 |
114 | function getWatchers(m) {
115 | const { watcher, subscriptions } = m
116 | return parseWatcher(watcher) || parseWatcher(subscriptions) || []
117 | }
118 |
119 | export default function createModel(m) {
120 | const {
121 | namespace,
122 | state,
123 | effects,
124 | reducers,
125 | enhancers,
126 | createReducer,
127 | ...others
128 | } = m
129 |
130 | const actions = {}
131 | const _effects = {}
132 | const _reducers = {}
133 | const _callbacks = {}
134 |
135 | if (effects) {
136 | Object.keys(effects).forEach((type) => {
137 | const method = effects[type]
138 | if (isEffect(method)) {
139 | _effects[type] = method
140 | actions[type] = type
141 | }
142 | })
143 | }
144 |
145 | if (reducers) {
146 | Object.keys(reducers).forEach((type) => {
147 | const method = reducers[type]
148 | if (isFunction(method)) {
149 | _reducers[type] = method
150 | actions[type] = type
151 | }
152 | })
153 | }
154 |
155 | const groups = parseGroups(others, namespace)
156 | groups.forEach((group) => {
157 | Object.assign(actions, group.actions)
158 | Object.assign(_effects, group.effects)
159 | Object.assign(_reducers, group.reducers)
160 | if (group.callbacks.length) {
161 | _callbacks[group.type] = group.callbacks
162 | }
163 | })
164 |
165 | const ns = fixNamespace(namespace)
166 | return {
167 | namespace: ns,
168 | state,
169 | enhancers,
170 | createReducer,
171 | actions,
172 | effects: prefixObject(ns, _effects),
173 | reducers: prefixObject(ns, _reducers),
174 | callbacks: prefixObject(ns, _callbacks),
175 | watchers: getWatchers(m),
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/src/createPromiseMiddleware.js:
--------------------------------------------------------------------------------
1 | import { NAMESPACE_SEP } from './constants'
2 |
3 | export default function createPromiseMiddleware(app) {
4 | const caches = {}
5 |
6 | function isEffect(type) {
7 | const cache = caches[type]
8 | if (cache) {
9 | return cache === 1
10 | }
11 |
12 | const parts = type.split(NAMESPACE_SEP)
13 | const namespace = parts.slice(0, -1).join(NAMESPACE_SEP)
14 |
15 | const ret = app.models.some((m) => {
16 | if (m.namespace === namespace && m.effects[type]) {
17 | return true
18 | }
19 | return false
20 | })
21 |
22 | caches[type] = ret ? 1 : 2
23 |
24 | return ret
25 | }
26 |
27 | const middleware = () => next => (action) => {
28 | const { type } = action
29 | if (isEffect(type)) {
30 | // action should be a plain-object
31 | action.resolver = {}
32 | const promise = new Promise((resolve, reject) => {
33 | action.resolver.resolve = resolve
34 | action.resolver.reject = reject
35 | })
36 |
37 | action.then = promise.then.bind(promise)
38 | action.catch = promise.catch.bind(promise)
39 |
40 | return next(action)
41 | }
42 |
43 | return next(action)
44 | }
45 |
46 | return middleware
47 | }
48 |
--------------------------------------------------------------------------------
/src/createReducer.js:
--------------------------------------------------------------------------------
1 | import invariant from 'invariant'
2 | import { combineReducers } from 'redux'
3 | import { connectRouter } from 'connected-react-router'
4 | import { NAMESPACE_SEP } from './constants'
5 | import { isFunction } from './utils'
6 |
7 | function parseNamespace(reducers) {
8 | return Object.keys(reducers).reduce((memo, key) => {
9 | const nss = key.split(NAMESPACE_SEP)
10 | let temp = memo
11 |
12 | nss.forEach((ns, index) => {
13 | if (!temp[ns]) {
14 | temp[ns] = index === nss.length - 1 ? reducers[key] : {}
15 | }
16 | temp = temp[ns]
17 | })
18 |
19 | return memo
20 | }, {})
21 | }
22 |
23 | export default function createReducer({
24 | history,
25 | reducers,
26 | asyncReducers = {},
27 | extraReducers,
28 | reducerEnhancer,
29 | combineReducers: globalCombineReducers,
30 | }) {
31 | const combineMethod = globalCombineReducers || combineReducers
32 | const merged = { ...reducers, ...asyncReducers }
33 | const combine = section => Object.keys(section).reduce((memo, key) => ({
34 | ...memo,
35 | [key]: isFunction(section[key])
36 | ? section[key]
37 | : combineMethod(combine(section[key])),
38 | }), {})
39 |
40 | if (process.env.NODE_ENV !== 'production') {
41 | invariant(
42 | Object.keys(extraReducers).every(key => !(key in merged)),
43 | `createReducer: extraReducers is conflict with other reducers, reducers list: ${Object.keys(merged).join(', ')}`,
44 | )
45 | }
46 |
47 | const parsed = parseNamespace(merged)
48 | const combined = combine(parsed)
49 | const rootReducer = combineMethod({
50 | ...combined,
51 | ...extraReducers,
52 | })
53 |
54 | return reducerEnhancer(
55 | history
56 | ? connectRouter(history)(rootReducer)
57 | : rootReducer,
58 | )
59 | }
60 |
--------------------------------------------------------------------------------
/src/createRender.js:
--------------------------------------------------------------------------------
1 | import invariant from 'invariant'
2 | import React from 'react'
3 | import ReactDOM from 'react-dom'
4 | import Provider from './Provider'
5 | import { isFunction, isString, isHTMLElement } from './utils'
6 |
7 |
8 | function getCallbacks(options) {
9 | if (isFunction(options)) {
10 | return { afterRender: options }
11 | }
12 | return options || {}
13 | }
14 |
15 |
16 | export default function createRender(app, component, container, callback) {
17 | // return a render function
18 | return (_component, _container, _callback) => {
19 | const { beforeRender, afterRender } = getCallbacks(_callback || callback)
20 | // real render function
21 | const innerRender = (componentFromPromise, containerFromPromise) => {
22 | const innerComponent = componentFromPromise || component
23 | let innerContainer = containerFromPromise || container
24 |
25 | if (innerContainer) {
26 | if (isString(innerContainer)) {
27 | innerContainer = document.querySelector(innerContainer)
28 | invariant(innerContainer, `container with selector "${container}" not exist`)
29 | }
30 |
31 | invariant(isHTMLElement(innerContainer), 'container should be HTMLElement')
32 | }
33 |
34 | const canRender = innerComponent && innerContainer
35 | if (canRender) {
36 | ReactDOM.render(
37 | React.createElement(Provider, { app }, innerComponent),
38 | innerContainer,
39 | )
40 |
41 | if (afterRender) {
42 | afterRender(app)
43 | }
44 | }
45 | }
46 |
47 | let result = true
48 | if (beforeRender) {
49 | result = beforeRender(app)
50 | }
51 |
52 | if (result && result.then) {
53 | result.then((val) => {
54 | if (val) {
55 | if (Array.isArray(val)) {
56 | innerRender(...val)
57 | } else {
58 | innerRender(val, _container)
59 | }
60 | } else {
61 | innerRender(_component, _container)
62 | }
63 | }, () => { })
64 | } else if (result !== false) {
65 | innerRender(_component, _container)
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/createStore.js:
--------------------------------------------------------------------------------
1 | import invariant from 'invariant'
2 | import { createStore, applyMiddleware, compose } from 'redux'
3 | import { routerMiddleware } from 'connected-react-router'
4 | import { composeWithDevTools } from 'redux-devtools-extension'
5 | import createSagaMiddleware from 'redux-saga/lib/internal/middleware'
6 | import { flatten } from './utils'
7 |
8 | export default function ({
9 | history,
10 | reducers,
11 | initialState,
12 | promiseMiddleware,
13 | extraMiddlewares,
14 | extraEnhancers,
15 | onStateChange,
16 | options: {
17 | setupMiddlewares = f => f,
18 | },
19 | }) {
20 | invariant(
21 | Array.isArray(extraEnhancers),
22 | `createStore: extraEnhancers should be array, but got ${typeof extraEnhancers}`,
23 | )
24 |
25 | const sagaMiddleware = createSagaMiddleware()
26 | const middlewares = setupMiddlewares([
27 | sagaMiddleware,
28 | promiseMiddleware,
29 | ...flatten(extraMiddlewares, true),
30 | ])
31 |
32 | if (history) {
33 | middlewares.push(routerMiddleware(history))
34 | }
35 |
36 | const composeEnhancers = process.env.NODE_ENV !== 'production'
37 | ? composeWithDevTools()
38 | : compose
39 |
40 | const enhancers = [
41 | applyMiddleware(...middlewares),
42 | ...extraEnhancers,
43 | ]
44 |
45 | const store = createStore(reducers, initialState, composeEnhancers(...enhancers))
46 |
47 | // extend store
48 | store.runSaga = sagaMiddleware.run
49 | store.asyncReducers = {}
50 |
51 | // execute listeners when state is changed
52 | onStateChange.forEach((listener) => {
53 | store.subscribe(() => {
54 | listener(store.getState())
55 | })
56 | })
57 |
58 | return store
59 | }
60 |
--------------------------------------------------------------------------------
/src/getReducer.js:
--------------------------------------------------------------------------------
1 | import { handleActions } from 'redux-actions'
2 | import { getEnhancer } from './utils'
3 |
4 |
5 | const defaultReducerCreator = (state, handlers) => handleActions(handlers, state)
6 |
7 | function wrap(reducers) {
8 | return Object.keys(reducers).reduce((memo, type) => ({
9 | ...memo,
10 | // call reducer with `state` and `payload`
11 | [type]: (state, action) => reducers[type](state, action.payload),
12 | }), {})
13 | }
14 |
15 | export default function getReducer(globalReducerCreator, model) {
16 | const { state, reducers, enhancers, createReducer } = model
17 | const enhancer = getEnhancer(enhancers)
18 | const creator = createReducer || globalReducerCreator || defaultReducerCreator
19 | return enhancer(creator(state, wrap(reducers)))
20 | }
21 |
--------------------------------------------------------------------------------
/src/getSaga.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable func-names */
2 | import warning from 'warning'
3 | import invariant from 'invariant'
4 | import {
5 | takeEveryHelper as takeEvery,
6 | takeLatestHelper as takeLatest,
7 | throttleHelper as throttle,
8 | } from 'redux-saga/lib/internal/sagaHelpers'
9 | import { delay, CANCEL as CANCEL_DELAY } from 'redux-saga'
10 | import * as sagaEffects from 'redux-saga/effects'
11 | import { CANCEL_EFFECTS, MUTATE } from './constants'
12 | import { prefixType, unfixType, prefixAndValidate } from './utils'
13 | import { getModelActions } from './actions'
14 |
15 |
16 | function applyOnEffect(handlers, effect, actionType, metadata) {
17 | return handlers.reduce(
18 | (_effect, handler) => handler(_effect, sagaEffects, actionType, metadata),
19 | effect,
20 | )
21 | }
22 |
23 | // signature: *effect(payload, {call, digest}, callbacks, innerActions, actions) { }
24 | // usage: const result = yield digest(generator, arg1, arg2, ...)
25 | function* callGenerator(fn, ...args) {
26 | const ret = yield fn(...args)
27 | if (ret.then) {
28 | return yield new Promise((resolve) => {
29 | ret.then(data => resolve(data))
30 | })
31 | }
32 |
33 | return ret
34 | }
35 |
36 | function getEffects(model) {
37 | const { put, take } = sagaEffects
38 | const { namespace } = model
39 |
40 | function assertAction(type, name) {
41 | invariant(type, `${name}: action should be a plain Object with type`)
42 | warning(
43 | type.indexOf(prefixType(namespace, '')) !== 0,
44 | `${name}: ${type} should not be prefixed with namespace "${namespace}"`,
45 | )
46 | }
47 |
48 | function innerPut(action) {
49 | const { type } = action
50 | assertAction(type, 'innerPut')
51 | return put({ ...action, type: prefixAndValidate(type, model) })
52 | }
53 |
54 | function innerTake(type) {
55 | if (typeof type === 'string') {
56 | assertAction(type, 'innerTake')
57 | return take(prefixAndValidate(type, model))
58 | }
59 | return take(type)
60 | }
61 |
62 | function mutate(payload, debug) {
63 | const action = {
64 | payload,
65 | type: prefixType(namespace, model.actions[MUTATE]),
66 | }
67 |
68 | if (process.env.NODE_ENV !== 'production' && debug) {
69 | action.debug = debug
70 | }
71 |
72 | return put(action)
73 | }
74 |
75 | return {
76 | ...sagaEffects,
77 | delay,
78 | CANCEL_DELAY,
79 | innerPut,
80 | innerTake,
81 | [MUTATE]: mutate,
82 | digest: callGenerator,
83 | }
84 | }
85 |
86 | function getCallbacks(model, actionType) {
87 | const { put } = sagaEffects
88 | const { namespace, effects, callbacks } = model
89 | const callbackEffects = {}
90 | if (effects[actionType] && callbacks[actionType]) {
91 | callbacks[actionType].forEach((callback) => {
92 | callbackEffects[callback] = (payload) => {
93 | const actionName = unfixType(namespace, actionType)
94 | const prefixed = prefixType(actionName, callback)
95 | const fullType = prefixAndValidate(prefixed, model)
96 | return put({ payload, type: fullType })
97 | }
98 | })
99 | }
100 |
101 | return callbackEffects
102 | }
103 |
104 | function getWatcher({ onError, onEffect, app, model, type, effect }) {
105 | let effectFn = effect
106 | let effectType = 'takeEvery'
107 | let ms
108 |
109 | if (Array.isArray(effect)) {
110 | effectFn = effect[0]
111 | const options = effect[1]
112 | if (options && options.type) {
113 | effectType = options.type
114 | if (effectType === 'throttle') {
115 | invariant(
116 | options.ms,
117 | 'options.ms should be defined if type is throttle',
118 | )
119 | ms = options.ms
120 | }
121 | }
122 | invariant(
123 | ['watcher', 'takeEvery', 'takeLatest', 'throttle'].includes(effectType),
124 | 'effect type should be takeEvery, takeLatest, throttle or watcher',
125 | )
126 | }
127 |
128 | const actions = app.actions
129 | const effects = getEffects(model)
130 | const callbacks = getCallbacks(model, type)
131 | const innerActions = getModelActions(model, sagaEffects.put)
132 |
133 | function* sagaWithCatch(...args) {
134 | const { payload, resolver: { resolve, reject } } = args[0]
135 | try {
136 | const ret = yield effectFn(
137 | payload,
138 | { ...effects, resolve, reject }, // 支持在业务逻辑中调用 resolve 和 reject
139 | callbacks,
140 | innerActions,
141 | actions,
142 | )
143 | // 默认调用 resolve,ret 是在 effect 函数的返回值(return ret)
144 | // 如果在业务逻辑中已经触发 resolve,那么此处的 resolve 将不会生效
145 | resolve(ret)
146 | } catch (err) {
147 | onError(err)
148 | reject(err)
149 | }
150 | }
151 |
152 | const sagaWithOnEffect = applyOnEffect(onEffect, sagaWithCatch, type, {
153 | app,
154 | model: model.namespace,
155 | actions,
156 | effects,
157 | })
158 |
159 | switch (effectType) {
160 | case 'watcher':
161 | return sagaWithCatch
162 | case 'takeLatest':
163 | return function* () {
164 | yield takeLatest(type, sagaWithOnEffect)
165 | }
166 | case 'throttle':
167 | return function* () {
168 | yield throttle(ms, type, sagaWithOnEffect)
169 | }
170 | default:
171 | return function* () {
172 | yield takeEvery(type, sagaWithOnEffect)
173 | }
174 | }
175 | }
176 |
177 | function getTask(model, type, watcher) {
178 | const { namespace } = model
179 | return function* run() {
180 | const task = yield sagaEffects.fork(watcher)
181 | yield sagaEffects.fork(function* () {
182 | const { cancel } = yield sagaEffects.race({
183 | cancel: sagaEffects.take(prefixType(type, CANCEL_EFFECTS)),
184 | eject: sagaEffects.take(prefixType(namespace, CANCEL_EFFECTS)),
185 | })
186 |
187 | yield sagaEffects.cancel(task)
188 |
189 | if (cancel) {
190 | yield sagaEffects.fork(run)
191 | }
192 | })
193 | }
194 | }
195 |
196 | export default function getSaga(onError, onEffect, app, model) {
197 | return function* () {
198 | const { effects } = model
199 | const keys = Object.keys(effects)
200 | for (let i = 0, l = keys.length; i < l; i += 1) {
201 | const type = keys[i]
202 | const watcher = getWatcher({ onError, onEffect, app, model, type, effect: effects[type] })
203 | const task = getTask(model, type, watcher)
204 | yield sagaEffects.fork(task)
205 | }
206 | }
207 | }
208 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | // native exports
2 | export { connect } from 'react-redux'
3 | export { applyMiddleware, compose, combineReducers } from 'redux'
4 | export {
5 | Route,
6 | Router,
7 | MemoryRouter,
8 | StaticRouter,
9 | Switch,
10 | Prompt,
11 | Redirect,
12 | matchPath,
13 | withRouter,
14 | } from 'react-router'
15 | export { Link, NavLink, BrowserRouter, HashRouter } from 'react-router-dom'
16 |
17 | // mickey exports
18 | export * as utils from './utils'
19 | export injectActions from './injectActions'
20 | export ActionsProvider from './ActionsProvider'
21 | export default from './createApp'
22 |
--------------------------------------------------------------------------------
/src/injectActions.js:
--------------------------------------------------------------------------------
1 | import invariant from 'invariant'
2 | import React from 'react'
3 | import PropTypes from 'prop-types'
4 |
5 | function getDisplayName(Component) {
6 | const name = (Component && (Component.displayName || Component.name)) || 'Component'
7 | return `injectActions(${name})`
8 | }
9 |
10 | export default function injectActions(WrappedComponent, options = {}) {
11 | const {
12 | propName = 'actions',
13 | withRef = false,
14 | } = options
15 |
16 | class InjectActions extends React.Component {
17 | constructor(props, context) {
18 | super(props, context)
19 | const { actions } = context
20 | if (process.env.NODE_ENV !== 'production') {
21 | invariant(
22 | actions,
23 | '[injectActions] Could not find required `actions` object. ' +
24 | ' needs to exist in the component ancestry.',
25 | )
26 | }
27 | }
28 |
29 | getWrappedInstance() {
30 | if (process.env.NODE_ENV !== 'production') {
31 | invariant(
32 | withRef,
33 | 'To access the wrapped instance, you need to specify { withRef: true } in the options argument of the injectActions() call.',
34 | )
35 | }
36 | return this.wrappedInstance
37 | }
38 |
39 | render() {
40 | const props = {
41 | ...this.props,
42 | [propName]: this.context.actions,
43 | }
44 |
45 | if (withRef) {
46 | props.ref = (wrappedInstance) => { this.wrappedInstance = wrappedInstance }
47 | }
48 |
49 | return React.createElement(WrappedComponent, props)
50 | }
51 | }
52 |
53 | InjectActions.WrappedComponent = WrappedComponent
54 | InjectActions.displayName = getDisplayName(WrappedComponent)
55 | InjectActions.contextTypes = {
56 | actions: PropTypes.object.isRequired,
57 | }
58 |
59 | return InjectActions
60 | }
61 |
--------------------------------------------------------------------------------
/src/internalModel.js:
--------------------------------------------------------------------------------
1 | // internal model to update global state when do unmodel
2 | export default {
3 | namespace: '@@MICKEY',
4 | state: 0,
5 | reducers: {
6 | UPDATE(state) { return state + 1 },
7 | },
8 | }
9 |
--------------------------------------------------------------------------------
/src/prefixDispatch.js:
--------------------------------------------------------------------------------
1 | import warning from 'warning'
2 | import invariant from 'invariant'
3 | import { prefixAndValidate, prefixType } from './utils'
4 |
5 | export default function prefixDispatch(dispatch, model) {
6 | return (action) => {
7 | const { type } = action
8 |
9 | if (process.env.NODE_ENV !== 'production') {
10 | const { namespace } = model
11 | invariant(type, 'innerDispatch: action should be a plain Object with type')
12 | warning(
13 | type.indexOf(prefixType(namespace, '')) !== 0,
14 | `innerDispatch: ${type} should not be prefixed with namespace ${namespace}`,
15 | )
16 | }
17 |
18 | return dispatch({ ...action, type: prefixAndValidate(type, model) })
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/registerModel.js:
--------------------------------------------------------------------------------
1 | import checkModel from './checkModel'
2 | import createModel from './createModel'
3 | import { addActions } from './actions'
4 |
5 | export default function registerModel(app, raw) {
6 | const model = createModel(raw)
7 | if (process.env.NODE_ENV !== 'production') {
8 | checkModel(model, app.models)
9 | }
10 | app.models.push(model)
11 | addActions(app, model)
12 | return model
13 | }
14 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | import isFunction from 'lodash.isfunction'
2 | import { NAMESPACE_SEP } from './constants'
3 |
4 | export asign from 'object-assign'
5 | export getByPath from 'lodash.get'
6 | export flatten from 'lodash.flatten'
7 | export minimatch from 'minimatch'
8 | export isPlainObject from 'is-plain-object'
9 | export isFunction from 'lodash.isfunction'
10 |
11 | // Test generator function that compilied with babel(babel-profill)
12 | export const isGeneratorFn = fn => (
13 | typeof fn === 'function' &&
14 | fn.constructor &&
15 | (
16 | fn.constructor.name === 'GeneratorFunction' ||
17 | fn.constructor.displayName === 'GeneratorFunction'
18 | )
19 | )
20 |
21 | export const isArray = Array.isArray.bind(Array)
22 | export const isString = str => typeof str === 'string'
23 | export const ucfirst = s => s.charAt(0).toUpperCase() + s.substr(1)
24 | export const filename = file => file.match(/([^/]+)\.(js|ts)$/)[1]
25 | export const isHTMLElement = node => !!(node && typeof node === 'object' && node.nodeType && node.nodeName)
26 |
27 | export const unfixType = (namespace, type) => (type.replace(`${namespace}${NAMESPACE_SEP}`, ''))
28 | export const prefixType = (namespace, type) => (`${namespace}${NAMESPACE_SEP}${type}`)
29 |
30 | export const prefixObject = (namespace, obj) => Object.keys(obj).reduce((memo, type) => {
31 | memo[prefixType(namespace, type)] = obj[type]
32 | return memo
33 | }, {})
34 |
35 | export const prefixAndValidate = (type, model) => {
36 | const { effects, reducers, namespace } = model
37 |
38 | const prefixed = prefixType(namespace, type)
39 | const final = prefixed.replace(/\/@@[^/]+?$/, '')
40 | if (reducers[final] || effects[final]) {
41 | return final
42 | }
43 | return type
44 | }
45 |
46 | export const getEnhancer = (enhancers) => {
47 | if (enhancers) {
48 | if (isArray(enhancers)) {
49 | return reducer => enhancers.reduce((memo, reducerEnhancer) => reducerEnhancer(memo), reducer)
50 | } else if (isFunction(enhancers)) {
51 | return reducer => enhancers(reducer)
52 | }
53 | }
54 |
55 | return f => f
56 | }
57 |
58 | export const getNamespaceFromPath = (path) => {
59 | const parts = path.split('/')
60 | const file = parts.pop()
61 |
62 | if (path[0] === '.' || path[0] === '/') {
63 | parts.shift()
64 | }
65 |
66 | parts.push(filename(file))
67 |
68 | return parts.join('.')
69 | }
70 |
71 | export const fixNamespace = namespace => namespace.replace(/\//g, '_').replace(/\./g, '/')
72 |
--------------------------------------------------------------------------------
/src/watcher.js:
--------------------------------------------------------------------------------
1 | import warning from 'warning'
2 | import { isFunction, getByPath } from './utils'
3 | import { getModelActions } from './actions'
4 | import prefixDispatch from './prefixDispatch'
5 |
6 | function getState(app, path, defaultValue) {
7 | const state = app.store.getState()
8 | return path ? getByPath(state, path, defaultValue) : state
9 | }
10 |
11 | export function startWatchers(model, app, onError) {
12 | const history = app.history
13 | const dispatch = app.store.dispatch
14 | const innerActions = getModelActions(model, dispatch)
15 |
16 | const funcs = []
17 | const nonFuncs = []
18 |
19 | if (model.watchers) {
20 | model.watchers.forEach((watcher, index) => {
21 | const unlistener = watcher({
22 | history,
23 | getState: (path, defaultValue) => getState(app, path, defaultValue),
24 | // never need to call the following methods, just export for debug
25 | dispatch,
26 | innerDispatch: prefixDispatch(dispatch, model),
27 | }, innerActions, app.actions, onError)
28 |
29 | if (isFunction(unlistener)) {
30 | funcs.push(unlistener)
31 | } else {
32 | nonFuncs.push([model.namespace, index])
33 | }
34 | })
35 | }
36 |
37 | return { funcs, nonFuncs }
38 | }
39 |
40 | export function stopWatchers(unlisteners, namespace) {
41 | if (!unlisteners[namespace]) return
42 |
43 | const { funcs, nonFuncs } = unlisteners[namespace]
44 | warning(
45 | nonFuncs.length === 0,
46 | `watcher should return unlistener function, check watchers in these models: ${nonFuncs.map(item => item[0]).join(', ')}`,
47 | )
48 |
49 | funcs.forEach(unlistener => unlistener())
50 | delete unlisteners[namespace]
51 | }
52 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | import './spec/utils.spec'
2 | import './spec/watcher.spec'
3 | import './spec/createHistory.spec'
4 | // import './spec/steupHistoryHooks.spec'
5 | import './spec/Plugin.spec'
6 | import './spec/ActionsProvider.spec'
7 | import './spec/createModel.spec'
8 | import './spec/checkModel.spec'
9 | import './spec/createErrorHandler.spec'
10 | import './spec/injectActions.spec'
11 | import './spec/getSaga.spec'
12 | import './spec/createApp.spec'
13 |
--------------------------------------------------------------------------------
/test/setup.js:
--------------------------------------------------------------------------------
1 | import { JSDOM } from 'jsdom'
2 |
3 | const { window } = new JSDOM('', { url: 'http://localhost' })
4 |
5 | const copyProps = (source, target) => {
6 | Object.getOwnPropertyNames(source)
7 | .filter(prop => typeof target[prop] === 'undefined')
8 | .forEach((prop) => { target[prop] = source[prop] })
9 | }
10 |
11 | global.window = window
12 | global.document = window.document
13 | global.navigator = { userAgent: 'node.js' }
14 |
15 | copyProps(window, global)
16 |
17 |
--------------------------------------------------------------------------------
/test/spec/ActionsProvider.spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/no-multi-comp */
2 | /* eslint-disable react/jsx-filename-extension */
3 | /* eslint-disable react/prefer-stateless-function */
4 |
5 | import { expect } from 'chai'
6 | import sinon from 'sinon'
7 | import React from 'react'
8 | import PropTypes from 'prop-types'
9 | import TestUtils from 'react-dom/test-utils'
10 | import ActionsProvider, { createProvider } from '../../src/ActionsProvider'
11 |
12 | describe('ActionsProvider', () => {
13 | const createChild = (actionsKey = 'actions') => {
14 | class Child extends React.Component {
15 | render() {
16 | return
17 | }
18 | }
19 |
20 | Child.contextTypes = {
21 | [actionsKey]: PropTypes.object.isRequired,
22 | }
23 |
24 | return Child
25 | }
26 |
27 | const Child = createChild()
28 |
29 | it('should enforce a single child', () => {
30 | const actions = {}
31 |
32 | // Ignore propTypes warnings
33 | const propTypes = ActionsProvider.propTypes
34 | ActionsProvider.propTypes = {}
35 |
36 | try {
37 | expect(
38 | () => TestUtils.renderIntoDocument(
39 |
40 |
41 | ,
42 | ),
43 | ).to.not.throw()
44 |
45 | expect(
46 | () => TestUtils.renderIntoDocument(
47 | ,
48 | ),
49 | ).to.throw(/a single React element child/)
50 |
51 | expect(
52 | () => TestUtils.renderIntoDocument(
53 |
54 |
55 |
56 | ,
57 | ),
58 | ).to.throw(/a single React element child/)
59 | } finally {
60 | ActionsProvider.propTypes = propTypes
61 | }
62 | })
63 |
64 | it('should add the actions to the child context', () => {
65 | const actions = { foo: 1 }
66 | const spy = sinon.spy(console, 'error')
67 | const tree = TestUtils.renderIntoDocument(
68 |
69 |
70 | ,
71 | )
72 | spy.restore()
73 | expect(spy.callCount).to.be.equal(0)
74 |
75 | const child = TestUtils.findRenderedComponentWithType(tree, Child)
76 | expect(child.context.actions).to.be.eql(actions)
77 | })
78 |
79 | it('should add the actions to the child context using a custom actions key', () => {
80 | const actions = { foo: 1 }
81 | const CustomProvider = createProvider('customActionsKey')
82 | const CustomChild = createChild('customActionsKey')
83 |
84 | const spy = sinon.spy(console, 'error')
85 | const tree = TestUtils.renderIntoDocument(
86 |
87 |
88 | ,
89 | )
90 | spy.restore()
91 | expect(spy.callCount).to.be.equal(0)
92 |
93 | const child = TestUtils.findRenderedComponentWithType(tree, CustomChild)
94 | expect(child.context.customActionsKey).to.be.eql(actions)
95 | })
96 | })
97 |
--------------------------------------------------------------------------------
/test/spec/Plugin.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import Plugin from '../../src/Plugin'
3 |
4 | describe('Plugin', () => {
5 | it('should return a plugin instance', () => {
6 | const plugin = new Plugin()
7 | expect(plugin).to.be.an.instanceof(Plugin)
8 | })
9 |
10 | it('should have empty plugins on the property `hooks` after initialize', () => {
11 | const plugin = new Plugin()
12 | Object.keys(plugin.hooks).forEach((key) => {
13 | const plugins = plugin.hooks[key]
14 | expect(plugins).to.be.eql([])
15 | })
16 | })
17 |
18 | describe('plugin.use(hooks)', () => {
19 | it('should throw an error if `hooks` is not a plain-object', () => {
20 | const plugin = new Plugin()
21 | const badFn = () => { plugin.use(1) }
22 | expect(badFn).to.throw(/should be plain object/)
23 | })
24 |
25 | it('should throw an error if given an unknown hook name', () => {
26 | const plugin = new Plugin()
27 | const badFn = () => { plugin.use({ foo: 1 }) }
28 | expect(badFn).to.throw(/unknown hook property/)
29 | })
30 |
31 | it('should replace `extraEnhancers` when apply a new one', () => {
32 | const plugin = new Plugin()
33 | plugin.use({ extraEnhancers: 1 })
34 | plugin.use({ extraEnhancers: 2 })
35 | expect(plugin.hooks.extraEnhancers).to.be.eql(2)
36 | })
37 |
38 | it('should append to plugins set except `extraEnhancers`', () => {
39 | const hooks = Plugin.hooks
40 | const plugin = new Plugin()
41 | hooks.forEach((name) => {
42 | plugin.use({ [name]: 1 })
43 | })
44 | hooks.forEach((name) => {
45 | plugin.use({ [name]: [2, 3] })
46 | })
47 | hooks.forEach((name) => {
48 | plugin.use({ [name]: 4 })
49 | })
50 |
51 | hooks.forEach((name) => {
52 | if (name === 'extraEnhancers') {
53 | expect(plugin.hooks[name]).to.be.eql(4)
54 | } else {
55 | expect(plugin.hooks[name]).to.be.eql([1, 2, 3, 4])
56 | }
57 | })
58 | })
59 | })
60 |
61 | describe('plugin.get(name)', () => {
62 | it('should throw error when trying to get hooks with illegal name', () => {
63 | const plugin = new Plugin()
64 | const badFn = () => { plugin.get('foo') }
65 | expect(badFn).to.be.throw(/cannot be got/)
66 | })
67 |
68 | it('should return an reducer enhance function when get `onReducer`', () => {
69 | const plugin1 = new Plugin()
70 | const enhancer1 = plugin1.get('onReducer')
71 |
72 | expect(enhancer1(1)).to.be.equal(1)
73 |
74 | const plugin2 = new Plugin()
75 | plugin2.use({
76 | onReducer: [
77 | state => (state + 1),
78 | state => (state + 2),
79 | ],
80 | })
81 | const enhancer2 = plugin2.get('onReducer')
82 | expect(enhancer2(1)).to.be.equal(4)
83 | })
84 |
85 | it('should return a merge reducer when get `extraReducers`', () => {
86 | const plugin1 = new Plugin()
87 | expect(plugin1.get('extraReducers')).to.be.eql({})
88 |
89 | const plugin2 = new Plugin()
90 | plugin2.use({
91 | extraReducers: [
92 | { foo: 'bar' },
93 | { baz: 1 },
94 | ],
95 | })
96 | expect(plugin2.get('extraReducers')).to.be.eql({ foo: 'bar', baz: 1 })
97 | })
98 |
99 | it('should return an array called with name except `onReducer` and `extraReducers`', () => {
100 | const hooks = Plugin.hooks
101 | const plugin = new Plugin()
102 | hooks.forEach((name) => {
103 | plugin.use({ [name]: 1 })
104 | })
105 | hooks.forEach((name) => {
106 | plugin.use({ [name]: [2, 3] })
107 | })
108 | hooks.forEach((name) => {
109 | plugin.use({ [name]: 4 })
110 | })
111 |
112 | hooks.forEach((name) => {
113 | if (name !== 'extraEnhancers' && name !== 'onReducer') {
114 | expect(plugin.hooks[name]).to.be.eql([1, 2, 3, 4])
115 | }
116 | })
117 | })
118 | })
119 |
120 | describe('plugin.apply(name, defaultHandler)', () => {
121 | it('should throw error when trying to apply hooks except `onError`', () => {
122 | const plugin = new Plugin()
123 | const badFn1 = () => { plugin.apply('foo') }
124 | const badFn2 = () => { plugin.apply('onAction') }
125 |
126 | expect(badFn1).to.be.throw(/cannot be applied/)
127 | expect(badFn2).to.be.throw(/cannot be applied/)
128 | })
129 |
130 | it('should return a handler function', () => {
131 | const plugin = new Plugin()
132 | const handler = plugin.apply('onError')
133 | expect(handler).to.be.an.instanceof(Function)
134 | })
135 |
136 | it('should work with the specified handlers', () => {
137 | let errorMessage = ''
138 |
139 | function onError(err) {
140 | errorMessage = err.message
141 | }
142 |
143 | const plugin = new Plugin()
144 | plugin.use({
145 | onError,
146 | })
147 | plugin.apply('onError')({ message: 'Hello Mickey' })
148 | expect(errorMessage).to.eql('Hello Mickey')
149 | })
150 |
151 | it('should call default handler if the handlers is empty', () => {
152 | let errorMessage = ''
153 |
154 | function onError(err) {
155 | errorMessage = err.message
156 | }
157 |
158 | const plugin = new Plugin()
159 | plugin.apply('onError', onError)({ message: 'hello mickey' })
160 | expect(errorMessage).to.eql('hello mickey')
161 | })
162 | })
163 | })
164 |
--------------------------------------------------------------------------------
/test/spec/checkModel.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import checkModel from '../../src/checkModel'
3 |
4 | describe('checkModel', () => {
5 | it('should throw an error when `namespace` not be specified', () => {
6 | expect(() => {
7 | checkModel({})
8 | }).to.throw(/namespace should be specified/)
9 | })
10 |
11 | it('should throw an error when `namespace` not be a string', () => {
12 | expect(() => {
13 | checkModel({ namespace: 1 })
14 | }).to.throw(/namespace should be string/)
15 | })
16 |
17 | it('should throw an error when `namespace` not unique', () => {
18 | expect(() => {
19 | checkModel({
20 | namespace: '-',
21 | }, [{
22 | namespace: '-',
23 |
24 | }])
25 | }).to.throw(/namespace should be unique/)
26 | })
27 | })
28 |
--------------------------------------------------------------------------------
/test/spec/createErrorHandler.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import createApp from '../../src/createApp'
3 | import createErrorHandler from '../../src/createErrorHandler'
4 |
5 | describe('createErrorHandler', () => {
6 | it('should return a function', () => {
7 | const app = createApp()
8 | expect(createErrorHandler(app)).to.be.an.instanceof(Function)
9 | })
10 |
11 | it('should use the defaule error handler when no handler specified', () => {
12 | const app = createApp()
13 | const handler = createErrorHandler(app)
14 | expect(handler).to.not.throw()
15 | expect(() => { handler('foo') }).to.throw('foo')
16 | })
17 |
18 | it('should call the specified handlers with `error` and `app`', () => {
19 | let instance
20 | let error
21 | const app = createApp({
22 | hooks: {
23 | onError: [
24 | (err, appInstance) => { instance = appInstance },
25 | (err) => { error = err },
26 | ],
27 | },
28 | })
29 | const handler = createErrorHandler(app)
30 | handler(new Error('foo'))
31 |
32 | expect(instance).to.be.eql(app)
33 | expect(error).to.be.an.instanceOf(Error)
34 | })
35 | })
36 |
--------------------------------------------------------------------------------
/test/spec/createHistory.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import createHistory from '../../src/createHistory'
3 |
4 | const routerActions = ['push', 'replace', 'go', 'goBack', 'goForward']
5 |
6 |
7 | describe('createHistory', () => {
8 | it('should throw an error when specified an unknown `historyMode`', () => {
9 | expect(() => { createHistory('foo') }).to.throw(/historyMode "foo" is invalid/)
10 |
11 | const env = process.env.NODE_ENV
12 | process.env.NODE_ENV = 'production'
13 | expect(() => { createHistory('foo') }).to.not.throw()
14 | process.env.NODE_ENV = env
15 | })
16 |
17 | it('should return null when no `historyMode` specified', () => {
18 | expect(createHistory()).to.be.null // eslint-disable-line
19 | expect(createHistory(0)).to.be.null // eslint-disable-line
20 | expect(createHistory(false)).to.be.null // eslint-disable-line
21 | })
22 |
23 | it('should creat a hashHistory when call with \'hash\'', () => {
24 | const history = createHistory('hash')
25 | routerActions.forEach((key) => {
26 | expect(history).to.have.property(key)
27 | })
28 | })
29 |
30 | it('should creat a browserHistory when call with \'browser\'', () => {
31 | const history = createHistory('browser')
32 | routerActions.forEach((key) => {
33 | expect(history).to.have.property(key)
34 | })
35 | })
36 |
37 | it('should creat a memoryHistory when call with \'memory\'', () => {
38 | const history = createHistory('memory')
39 | routerActions.forEach((key) => {
40 | expect(history).to.have.property(key)
41 | })
42 | })
43 | })
44 |
--------------------------------------------------------------------------------
/test/spec/createModel.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import createModel from '../../src/createModel'
3 | import baseModel from '../../src/baseModel'
4 |
5 | describe('createModel', () => {
6 | it('should fix namespace', () => {
7 | expect(createModel({ namespace: 'a/b.c' }).namespace).to.be.equal('a_b/c')
8 | })
9 |
10 | it('should return the same `state`, `enhancers` and `createReducer`', () => {
11 | const model = {
12 | namespace: 'foo.bar',
13 | state: 1,
14 | enhancers: [2],
15 | createReducer: f => f,
16 | }
17 | const ret = createModel(model)
18 |
19 | expect(ret.state).to.be.eql(model.state)
20 | expect(ret.enhancers).to.be.eql(model.enhancers)
21 | expect(ret.createReducer).to.be.eql(model.createReducer)
22 | })
23 |
24 | it('should parse group', () => {
25 | const model = {
26 | namespace: 'foo.bar',
27 | state: 1,
28 | query: {
29 | * effect(payload, effects, callbacks) { yield callbacks.succeed() },
30 | prepare: state => state,
31 | succeed: state => state,
32 | failed: state => state,
33 | },
34 | }
35 | const ret = createModel(model)
36 |
37 | expect(ret.actions).to.own.include({
38 | query: 'query',
39 | querySucceed: 'query/succeed',
40 | queryFailed: 'query/failed',
41 | })
42 |
43 | expect(ret.reducers).to.own.include({
44 | 'foo/bar/query': model.query.prepare,
45 | 'foo/bar/query/succeed': model.query.succeed,
46 | 'foo/bar/query/failed': model.query.failed,
47 | })
48 |
49 | expect(ret.effects).to.be.eql({
50 | 'foo/bar/query': model.query.effect,
51 | })
52 |
53 | expect(ret.callbacks).to.be.eql({
54 | 'foo/bar/query': ['succeed', 'failed'],
55 | })
56 | })
57 |
58 | it('should merge groups', () => {
59 | const model = {
60 | namespace: 'foo.bar',
61 | state: 1,
62 | reducers: { baz1: state => state },
63 | effects: { * baz2(payload, effects, callbacks) { yield callbacks.succeed() } },
64 | query: {
65 | * effect(payload, effects, callbacks) { yield callbacks.succeed() },
66 | prepare: state => state,
67 | succeed: state => state,
68 | failed: state => state,
69 | },
70 | del: {
71 | * effect(payload, effects, callbacks) { yield callbacks.succeed() },
72 | prepare: state => state,
73 | succeed: state => state,
74 | failed: state => state,
75 | },
76 | }
77 | const ret = createModel(model)
78 |
79 | expect(ret.actions).to.own.include({
80 | baz1: 'baz1',
81 | baz2: 'baz2',
82 | query: 'query',
83 | querySucceed: 'query/succeed',
84 | queryFailed: 'query/failed',
85 | del: 'del',
86 | delSucceed: 'del/succeed',
87 | delFailed: 'del/failed',
88 | })
89 |
90 | expect(ret.reducers).to.own.include({
91 | 'foo/bar/baz1': model.reducers.baz1,
92 | 'foo/bar/query': model.query.prepare,
93 | 'foo/bar/query/succeed': model.query.succeed,
94 | 'foo/bar/query/failed': model.query.failed,
95 | 'foo/bar/del': model.del.prepare,
96 | 'foo/bar/del/succeed': model.del.succeed,
97 | 'foo/bar/del/failed': model.del.failed,
98 | })
99 |
100 | expect(ret.effects).to.be.eql({
101 | 'foo/bar/baz2': model.effects.baz2,
102 | 'foo/bar/query': model.query.effect,
103 | 'foo/bar/del': model.del.effect,
104 | })
105 |
106 | expect(ret.callbacks).to.be.eql({
107 | 'foo/bar/query': ['succeed', 'failed'],
108 | 'foo/bar/del': ['succeed', 'failed'],
109 | })
110 | })
111 |
112 | it('should treat `[Generator, options]` as effect', () => {
113 | const model = {
114 | namespace: 'foo.bar',
115 | state: 1,
116 | query: {
117 | effect: [function* effect(payload, effects, callbacks) { yield callbacks.succeed() }, {}],
118 | prepare: state => state,
119 | succeed: state => state,
120 | failed: state => state,
121 | },
122 | }
123 | const ret = createModel(model)
124 | expect(ret.actions).to.own.include({
125 | query: 'query',
126 | querySucceed: 'query/succeed',
127 | queryFailed: 'query/failed',
128 | })
129 |
130 | expect(ret.reducers).to.own.include({
131 | 'foo/bar/query': model.query.prepare,
132 | 'foo/bar/query/succeed': model.query.succeed,
133 | 'foo/bar/query/failed': model.query.failed,
134 | })
135 |
136 | expect(ret.effects).to.be.eql({
137 | 'foo/bar/query': model.query.effect,
138 | })
139 |
140 | expect(ret.callbacks).to.be.eql({
141 | 'foo/bar/query': ['succeed', 'failed'],
142 | })
143 | })
144 |
145 | it('shoule parse unGrouped reducers and effects', () => {
146 | const model = {
147 | namespace: 'foo.bar',
148 | state: 1,
149 | reducer1: state => state,
150 | reducer2: 1,
151 | * effect1(payload, effects, callbacks) { yield callbacks.succeed() },
152 | effect2: [function* effect(payload, effects, callbacks) { yield callbacks.succeed() }, {}],
153 | effect3: 1,
154 | }
155 | const ret = createModel(model)
156 |
157 | expect(ret.actions).to.own.include({
158 | reducer1: 'reducer1',
159 | effect1: 'effect1',
160 | effect2: 'effect2',
161 | })
162 |
163 | expect(ret.reducers).to.own.include({
164 | 'foo/bar/reducer1': model.reducer1,
165 | })
166 |
167 | expect(ret.effects).to.be.eql({
168 | 'foo/bar/effect1': model.effect1,
169 | 'foo/bar/effect2': model.effect2,
170 | })
171 |
172 | expect(ret.callbacks).to.be.eql({})
173 | })
174 |
175 | it('should ignore invalid actions', () => {
176 | const model = {
177 | namespace: 'foo.bar',
178 | state: 1,
179 | reducers: {
180 | reducer1: state => state,
181 | reducer2: 1,
182 | },
183 | effects: {
184 | effect1: function* effect1(payload, effects) { yield effects.call() },
185 | effect2: [function* effect2(payload, effects) { yield effects.call() }, {}],
186 | effect3: 1,
187 | },
188 | query: {
189 | * effect(payload, effects, callbacks) { yield callbacks.succeed() },
190 | prepare: state => state,
191 | succeed: state => state,
192 | failed: 1,
193 | },
194 | }
195 | const ret = createModel(model)
196 |
197 | expect(ret.actions).to.own.include({
198 | reducer1: 'reducer1',
199 | effect1: 'effect1',
200 | effect2: 'effect2',
201 | query: 'query',
202 | querySucceed: 'query/succeed',
203 | })
204 |
205 | expect(ret.reducers).to.own.include({
206 | 'foo/bar/reducer1': model.reducers.reducer1,
207 | 'foo/bar/query': model.query.prepare,
208 | 'foo/bar/query/succeed': model.query.succeed,
209 | })
210 |
211 | expect(ret.effects).to.be.eql({
212 | 'foo/bar/effect1': model.effects.effect1,
213 | 'foo/bar/effect2': model.effects.effect2,
214 | 'foo/bar/query': model.query.effect,
215 | })
216 |
217 | expect(ret.callbacks).to.be.eql({
218 | 'foo/bar/query': ['succeed'],
219 | })
220 | })
221 |
222 | it('should throw an error if there are more than one effects in group', () => {
223 | const badFn = () => {
224 | createModel({
225 | namespace: 'foo.bar',
226 | state: 1,
227 | query: {
228 | * effect1(payload, effects, callbacks) { yield callbacks.succeed() },
229 | * effect2(payload, effects, callbacks) { yield callbacks.succeed() },
230 | prepare: state => state,
231 | succeed: state => state,
232 | failed: state => state,
233 | },
234 | })
235 | }
236 |
237 | expect(badFn).to.throw(/Less than one effect function should be specified in model/)
238 |
239 | const env = process.env.NODE_ENV
240 | process.env.NODE_ENV = 'production'
241 | expect(badFn).to.not.throw()
242 | process.env.NODE_ENV = env
243 | })
244 |
245 | it('should extend model with base `mutate` reducer', () => {
246 | const model = {
247 | namespace: 'foo.bar',
248 | state: {},
249 | }
250 | const ret = createModel(model)
251 | expect(ret.actions).to.be.eql({
252 | mutate: 'mutate',
253 | })
254 |
255 | expect(ret.reducers).to.be.eql({
256 | 'foo/bar/mutate': baseModel.mutate,
257 | })
258 |
259 | expect(ret.effects).to.be.eql({})
260 |
261 | expect(ret.callbacks).to.be.eql({})
262 | })
263 |
264 | it('should throw an error if use the revesed action name `mutate`', () => {
265 | const badFn = () => {
266 | createModel({
267 | namespace: 'foo.bar',
268 | state: 1,
269 | mutate: state => state,
270 | })
271 | }
272 |
273 | expect(badFn).to.throw(/The `mutate` is a reserved action for mutate the state. You should change `mutate` to other action names/)
274 |
275 | const env = process.env.NODE_ENV
276 | process.env.NODE_ENV = 'production'
277 | expect(badFn).to.not.throw()
278 | process.env.NODE_ENV = env
279 | })
280 | })
281 |
--------------------------------------------------------------------------------
/test/spec/getSaga.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import sinon from 'sinon'
3 | import createApp from '../../src/createApp'
4 |
5 | describe('getSata', () => {
6 | it('should inject actions to async reducers', () => {
7 | const app = createApp()
8 | app.model({
9 | namespace: 'foo',
10 | state: 0,
11 | add: state => state + 1,
12 | })
13 | app.model({
14 | namespace: 'counter',
15 | state: {
16 | count: 0,
17 | loading: false,
18 | },
19 | increment: state => ({ ...state, count: state.count + 1 }),
20 | decrement: state => ({ ...state, count: state.count - 1 }),
21 | incrementAsync: {
22 | * effect(payload, effects, { succeed }, innerActions, actions) {
23 | yield succeed()
24 | yield innerActions.increment()
25 | yield actions.foo.add()
26 | },
27 | prepare: state => ({ ...state, loading: true }),
28 | succeed: state => ({ ...state, loading: false }),
29 | },
30 | })
31 | app.render()
32 |
33 | app.actions.counter.increment()
34 | app.actions.counter.incrementAsync()
35 |
36 | const state = app.store.getState()
37 | expect(state.foo).to.be.equal(1)
38 | expect(state.counter.count).to.be.equal(2)
39 | expect(state.counter.loading).to.be.equal(false)
40 | })
41 |
42 | it('should inject actions to watcher', () => {
43 | const app = createApp()
44 | app.model({
45 | namespace: 'foo',
46 | state: 0,
47 | add: state => state + 1,
48 | })
49 | app.model({
50 | namespace: 'counter',
51 | state: {
52 | count: 0,
53 | loading: false,
54 | },
55 | increment: state => ({ ...state, count: state.count + 1 }),
56 | decrement: state => ({ ...state, count: state.count - 1 }),
57 | incrementAsync: {
58 | * effect(payload, { innerPut }, { succeed }, innerActions, actions) {
59 | yield succeed()
60 | yield innerPut({ type: 'increment' })
61 | yield innerActions.increment()
62 | yield actions.foo.add()
63 | },
64 | prepare: state => ({ ...state, loading: true }),
65 | succeed: state => ({ ...state, loading: false }),
66 | },
67 | watcher(helpers, innerActions, actions) {
68 | helpers.dispatch({ type: 'foo/add' })
69 | helpers.innerDispatch({ type: 'increment' })
70 | innerActions.incrementAsync()
71 | actions.foo.add()
72 | },
73 | })
74 | app.render()
75 |
76 | const state = app.store.getState()
77 | expect(state.foo).to.be.equal(3)
78 | expect(state.counter.count).to.be.equal(3)
79 | expect(state.counter.loading).to.be.equal(false)
80 | })
81 |
82 | it('should throw an error if effect type is `throttle` but no throttle duration specified', () => {
83 | const app = createApp()
84 | app.model({
85 | namespace: 'foo',
86 | state: 0,
87 | incrementAsync: {
88 | effect: [
89 | function* effect(payload, { delay }) {
90 | yield delay(10)
91 | },
92 | { type: 'throttle' },
93 | ],
94 | },
95 | })
96 |
97 | const logStub = sinon.stub(console, 'error')
98 | app.render()
99 | expect(logStub.callCount).to.be.eql(1)
100 | expect(logStub.firstCall.args[1]).to.be.match(/options\.ms should be defined if type is throttle/)
101 | logStub.restore()
102 | })
103 |
104 | it('should inject `innerTake` to async reducers', () => {
105 | const app = createApp()
106 | app.model({
107 | namespace: 'counter',
108 | state: {
109 | count: 0,
110 | loading: false,
111 | },
112 | increment: state => ({ ...state, count: state.count + 1 }),
113 | decrement: state => ({ ...state, count: state.count - 1 }),
114 | incrementAsync: {
115 | * effect(payload, { innerPut, innerTake }, { succeed }) {
116 | innerTake('increment')
117 | yield succeed()
118 | yield innerPut({ type: 'increment' })
119 | },
120 | prepare: state => ({ ...state, loading: true }),
121 | succeed: state => ({ ...state, loading: false }),
122 | },
123 | })
124 | app.render()
125 |
126 | app.actions.counter.incrementAsync()
127 | app.actions.counter.increment()
128 |
129 | const state = app.store.getState()
130 | expect(state.counter.count).to.be.equal(2)
131 | expect(state.counter.loading).to.be.equal(false)
132 | })
133 |
134 | it('should extend model with base `mutate` reducer', () => {
135 | const app = createApp()
136 | app.model({
137 | namespace: 'foo',
138 | state: { count: 0 },
139 | * add(payload, { mutate }) {
140 | yield mutate({ count: 1 });
141 | },
142 | })
143 |
144 | app.render()
145 |
146 | app.actions.foo.add()
147 | const state = app.store.getState()
148 | expect(state.foo).to.be.eql({ count: 1 })
149 | })
150 | })
151 |
--------------------------------------------------------------------------------
/test/spec/injectActions.spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/no-multi-comp */
2 |
3 | import { expect } from 'chai'
4 | import React from 'react'
5 | import ReactDOM from 'react-dom'
6 | import PropTypes from 'prop-types'
7 | import TestUtils from 'react-dom/test-utils'
8 | import injectActions from '../../src/injectActions'
9 |
10 | describe('injectActions', () => {
11 | class Passthrough extends React.Component { // eslint-disable-line
12 | render() {
13 | return // eslint-disable-line
14 | }
15 | }
16 |
17 | class ProviderMock extends React.Component {
18 | getChildContext() {
19 | return { actions: this.props.actions }
20 | }
21 |
22 | render() {
23 | return React.Children.only(this.props.children)
24 | }
25 | }
26 |
27 | ProviderMock.childContextTypes = {
28 | actions: PropTypes.object.isRequired,
29 | }
30 |
31 | ProviderMock.propTypes = {
32 | actions: PropTypes.object.isRequired, // eslint-disable-line
33 | children: PropTypes.element.isRequired,
34 | }
35 |
36 | it('should receive the store in the context', () => {
37 | const actions = { foo: 1 }
38 |
39 | @injectActions
40 | class Container extends React.Component {
41 | render() {
42 | return
43 | }
44 | }
45 |
46 | const tree = TestUtils.renderIntoDocument(
47 |
48 |
49 | ,
50 | )
51 |
52 | const container = TestUtils.findRenderedComponentWithType(tree, Container)
53 | expect(container.context.actions).to.be.eql(actions)
54 | })
55 |
56 | it('should pass actions and props to the given component', () => {
57 | const actions = {
58 | foo: 'bar',
59 | baz: 42,
60 | hello: 'world',
61 | }
62 |
63 | @injectActions
64 | class Container extends React.Component {
65 | render() {
66 | return
67 | }
68 | }
69 |
70 | const container = TestUtils.renderIntoDocument(
71 |
72 |
73 | ,
74 | )
75 | const stub = TestUtils.findRenderedComponentWithType(container, Passthrough)
76 | expect(stub.props.actions).to.be.eql(actions)
77 | expect(stub.props.baz).to.equal(50)
78 | expect(stub.props.hello).to.be.undefined // eslint-disable-line
79 |
80 | expect(() =>
81 | TestUtils.findRenderedComponentWithType(container, Container),
82 | ).to.not.throw()
83 | })
84 |
85 | it('should throw an error if a component is not passed to injectActions', () => {
86 | const WrappedComponent = injectActions()
87 | const render = () => { ReactDOM.render(, React.createElement('div')) }
88 | expect(render).to.throw()
89 | })
90 |
91 | it('should throw when trying to access the wrapped instance if withRef is not specified', () => {
92 | const actions = {}
93 |
94 | class Container extends React.Component { // eslint-disable-line
95 | render() {
96 | return
97 | }
98 | }
99 |
100 | const Decorated = injectActions(Container)
101 | const tree = TestUtils.renderIntoDocument(
102 |
103 |
104 | ,
105 | )
106 |
107 | const decorated = TestUtils.findRenderedComponentWithType(tree, Decorated)
108 | expect(() => decorated.getWrappedInstance()).to.throw(
109 | /To access the wrapped instance, you need to specify \{ withRef: true \} in the options argument of the injectActions\(\) call\./,
110 | )
111 | })
112 |
113 | it('should return the instance of the wrapped component for use in calling child methods', () => {
114 | const actions = {}
115 |
116 | const someData = {
117 | some: 'data',
118 | }
119 |
120 | class Container extends React.Component {
121 | someInstanceMethod() { // eslint-disable-line
122 | return someData
123 | }
124 |
125 | render() {
126 | return
127 | }
128 | }
129 |
130 | const Decorated = injectActions(Container, { withRef: true })
131 | const tree = TestUtils.renderIntoDocument(
132 |
133 |
134 | ,
135 | )
136 |
137 | const decorated = TestUtils.findRenderedComponentWithType(tree, Decorated)
138 | expect(() => decorated.someInstanceMethod()).to.throw()
139 | expect(decorated.getWrappedInstance().someInstanceMethod()).to.be.eql(someData)
140 | expect(decorated.wrappedInstance.someInstanceMethod()).to.be.eql(someData)
141 | })
142 | })
143 |
--------------------------------------------------------------------------------
/test/spec/utils.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import { getNamespaceFromPath, getEnhancer } from '../../src/utils'
3 |
4 | describe('utils', () => {
5 | describe('getNamespaceFromPath', () => {
6 | it('should work with relative path', () => {
7 | expect(getNamespaceFromPath('./a/b/c.js')).to.be.equal('a.b.c')
8 | expect(getNamespaceFromPath('../a/b/c.js')).to.be.equal('a.b.c')
9 | })
10 |
11 | it('should work with absolute path', () => {
12 | expect(getNamespaceFromPath('/a/b/c.js')).to.be.equal('a.b.c')
13 | expect(getNamespaceFromPath('a/b/c.js')).to.be.equal('a.b.c')
14 | })
15 | })
16 |
17 | describe('getEnhancer', () => {
18 | it('should return a function which just return the input if enhancers is null', () => {
19 | const enhancer = getEnhancer()
20 | expect(enhancer(1)).to.be.eql(1)
21 | })
22 |
23 | it('should return a function which when enhancers is a function', () => {
24 | const enhancer = getEnhancer(reducer => reducer + 1)
25 | expect(enhancer(1)).to.be.eql(2)
26 | })
27 |
28 | it('should return a function which when enhancers is an array', () => {
29 | const enhancer = getEnhancer([reducer => reducer + 1, reducer => reducer - 1])
30 | expect(enhancer(1)).to.be.eql(1)
31 | })
32 | })
33 | })
34 |
--------------------------------------------------------------------------------
/test/spec/watcher.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import sinon from 'sinon'
3 | import createApp from '../../src/createApp'
4 |
5 | describe('watcher', () => {
6 | it('should call handlers with correct arguments', () => {
7 | const args = []
8 | const app = createApp()
9 | app.model({
10 | namespace: 'count',
11 | state: 0,
12 | add: (state, payload) => state + payload,
13 | })
14 | app.model({
15 | namespace: 'foo.bar',
16 | state: 1,
17 | sub: (state, payload) => state - payload,
18 | watcher(helpers, innerActions, actions, onError) {
19 | args.push(helpers, innerActions, actions, onError)
20 | },
21 | })
22 | app.render()
23 |
24 | const [helpers, innerActions, actions, onError] = args
25 | const { history, getState } = helpers
26 |
27 | expect(history).to.be.null //eslint-disable-line
28 | expect(getState()).to.eql(app.store.getState())
29 | expect(getState(['count'])).to.eql(0)
30 | expect(getState('count')).to.eql(0)
31 | expect(getState('foo.bar')).to.eql(1)
32 | expect(getState(['foo', 'bar'])).to.eql(1)
33 |
34 | expect(innerActions).to.have.own.property('sub')
35 |
36 | expect(actions).to.have.deep.nested.property('count.add')
37 | expect(actions).to.have.deep.nested.property('foo.bar.sub')
38 |
39 | const badFn = () => { onError('error') }
40 | expect(badFn).to.throw()
41 | })
42 |
43 | it('should dispatch actions correctly', () => {
44 | const app = createApp()
45 | app.model({
46 | namespace: 'count',
47 | state: 0,
48 | add: (state, payload) => state + payload,
49 | })
50 | app.model({
51 | namespace: 'foo.bar',
52 | state: 1,
53 | sub: (state, payload) => state - payload,
54 | watcher(helpers, innerActions, actions) {
55 | actions.count.add(2)
56 | innerActions.sub(1)
57 | },
58 | })
59 |
60 | app.render()
61 |
62 | expect(app.store.getState().count).to.eql(2)
63 | expect(app.store.getState().foo.bar).to.eql(0)
64 | })
65 |
66 | it('should unlisten corresponding listeners when eject model', () => {
67 | const app = createApp()
68 | let flag1 = false
69 | let flag2 = false
70 |
71 | app.model({
72 | namespace: 'count',
73 | state: 0,
74 | watcher() {
75 | return () => { flag1 = true }
76 | },
77 | })
78 | app.model({
79 | namespace: 'foo.bar',
80 | state: 0,
81 | watcher() {
82 | return () => { flag2 = true }
83 | },
84 | })
85 | app.render()
86 |
87 | const spy = sinon.stub(console, 'error')
88 |
89 | app.eject('count')
90 | app.eject('foo.bar')
91 | spy.restore()
92 |
93 | expect(flag1).to.be.eql(true)
94 | expect(flag2).to.be.eql(true)
95 | })
96 |
97 | it('should give a warning message if unlistener is not a function when unlisten', () => {
98 | const app = createApp()
99 | app.model({
100 | namespace: 'count',
101 | state: 0,
102 | watcher: [() => null],
103 | })
104 | app.model({
105 | namespace: 'foo.bar',
106 | state: 0,
107 | })
108 | app.render()
109 |
110 | const spy = sinon.stub(console, 'error')
111 |
112 | app.eject('count')
113 | app.eject('foo.bar')
114 |
115 | expect(spy.firstCall.args[0]).to.match(/watcher should return unlistener function/)
116 |
117 | spy.restore()
118 | })
119 | })
120 |
--------------------------------------------------------------------------------