);
56 | }
57 | }
58 |
59 | React.render((
60 |
61 | ), document.getElementById('container'));
62 |
--------------------------------------------------------------------------------
/sample/todolist/src/viewmodels/todolist.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by tdzl2_000 on 2015-09-21.
3 | */
4 |
5 | import {ViewModel, ListViewModel} from 'redux-viewmodel';
6 |
7 | export class TodoItemViewModel extends ViewModel
8 | {
9 | check(state){
10 | return {
11 | ...state,
12 | done: true
13 | }
14 | }
15 | uncheck(state){
16 | let {done, ...other} = state
17 | return other
18 | }
19 | switch(state){
20 | return {
21 | ...state,
22 | done: !state.done
23 | }
24 | }
25 | }
26 |
27 | export default class TodoListViewModel extends ListViewModel
28 | {
29 | static get defaultState(){
30 | return [
31 | {
32 | key: 1,
33 | title: 'Foo'
34 | },
35 | {
36 | key: 2,
37 | title: 'Bar',
38 | done: true
39 | }
40 | ]
41 | }
42 | get first(){
43 | return this.getSubViewModel("first", TodoItemViewModel, state=>{
44 | return state[0];
45 | }, (state, newValue)=>{
46 | return [newValue, ...(state.slice(1))];
47 | })
48 | }
49 | get last(){
50 | return this.getSubViewModel("first", TodoItemViewModel, state=>state[state.length-1], (state, newValue)=>{
51 | return [...(state.slice(0, state.length-1)), state];
52 | })
53 | }
54 | getItem(i){
55 | return getItemByKey(this.state[i]);
56 | }
57 | getItemByKey(key){
58 | return super.getItemByKey(key, TodoItemViewModel);
59 | }
60 | addItem(state, title){
61 | // Find largest id
62 | var id = state.map(v=>v.key).reduce((a, b)=>{return Math.max(a, b)}, 0) + 1;
63 |
64 | return [
65 | ...state,
66 | {
67 | key: id,
68 | title: title
69 | }
70 | ]
71 | }
72 | deleteItem(state, key){
73 | return this.state.filter(v=>v.key!=key);
74 | }
75 | }
--------------------------------------------------------------------------------
/sample/counter/webpack.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by tdzl2_000 on 2015-08-28.
3 | */
4 |
5 | 'use strict';
6 |
7 | var webpack = require('webpack');
8 | var argv = require('minimist')(process.argv.slice(2));
9 | var DEBUG = !argv.release;
10 |
11 | var AUTOPREFIXER_LOADER = 'autoprefixer-loader?{browsers:[' +
12 | '"Android 2.3", "Android >= 4", "Chrome >= 20", "Firefox >= 24", ' +
13 | '"Explorer >= 8", "iOS >= 6", "Opera >= 12", "Safari >= 6"]}';
14 |
15 | var GLOBALS = {
16 | 'process.env.NODE_ENV': DEBUG ? '"development"' : '"production"',
17 | '__DEV__': DEBUG
18 | };
19 |
20 | var path = require("path");
21 |
22 | var rootPath = path.dirname(module.filename);
23 |
24 | var config = {
25 | output: {
26 | filename: 'app.js',
27 | path: path.join(rootPath, DEBUG?'build-debug/':'build-release/'),
28 | publicPath: '/static/',
29 | sourcePrefix: ' '
30 | },
31 | entry: path.join(rootPath, 'src/app.js'),
32 | plugins: ([
33 | new webpack.optimize.OccurenceOrderPlugin(),
34 | new webpack.DefinePlugin(GLOBALS)
35 | ].concat(DEBUG ? [] : [
36 | new webpack.optimize.DedupePlugin(),
37 | new webpack.optimize.UglifyJsPlugin(),
38 | new webpack.optimize.AggressiveMergingPlugin()
39 | ])
40 | ),
41 | cache: DEBUG,
42 | debug: DEBUG,
43 | devtool: DEBUG ? '#inline-source-map' : false,
44 |
45 | resolve: {
46 | root: rootPath,
47 | extensions: ['', '.webpack.js', '.web.js', '.js', '.jsx']
48 | },
49 | resolveLoader: {
50 | root: path.join(rootPath, 'node_modules'),
51 | extensions: ['', '.loader.js', '.js', '.jsx']
52 | },
53 | module: {
54 | preLoaders: [
55 | {
56 | test: /\.js$/,
57 | exclude: /node_modules/,
58 | loader: 'eslint-loader'
59 | }
60 | ],
61 |
62 | loaders: [
63 | {
64 | test: /(\.eot)|(\.woff2?)|(\.ttf)$/,
65 | loader: 'file-loader'
66 | },
67 | {
68 | test: /\.css$/,
69 | loader: 'style-loader!css-loader!' + AUTOPREFIXER_LOADER
70 | },
71 | {
72 | test: /\.less$/,
73 | loader: 'style-loader!css-loader!' + AUTOPREFIXER_LOADER +
74 | '!less-loader'
75 | },
76 | {
77 | test: /\.gif/,
78 | loader: 'url-loader?limit=10000&mimetype=image/gif'
79 | },
80 | {
81 | test: /\.jpg/,
82 | loader: 'url-loader?limit=10000&mimetype=image/jpg'
83 | },
84 | {
85 | test: /\.png/,
86 | loader: 'url-loader?limit=10000&mimetype=image/png'
87 | },
88 | {
89 | test: /\.svg/,
90 | loader: 'url-loader?limit=10000&mimetype=image/svg+xml'
91 | },
92 | {
93 | test: /\.jsx?$/,
94 | exclude:function(t){
95 | return /node_modules/.test(path.relative(rootPath, t));
96 | },
97 | loader: 'babel-loader'
98 | }
99 | ]
100 | },
101 | }
102 |
103 | module.exports = config;
104 |
--------------------------------------------------------------------------------
/sample/todolist/webpack.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by tdzl2_000 on 2015-08-28.
3 | */
4 |
5 | 'use strict';
6 |
7 | var webpack = require('webpack');
8 | var argv = require('minimist')(process.argv.slice(2));
9 | var DEBUG = !argv.release;
10 |
11 | var AUTOPREFIXER_LOADER = 'autoprefixer-loader?{browsers:[' +
12 | '"Android 2.3", "Android >= 4", "Chrome >= 20", "Firefox >= 24", ' +
13 | '"Explorer >= 8", "iOS >= 6", "Opera >= 12", "Safari >= 6"]}';
14 |
15 | var GLOBALS = {
16 | 'process.env.NODE_ENV': DEBUG ? '"development"' : '"production"',
17 | '__DEV__': DEBUG
18 | };
19 |
20 | var path = require("path");
21 |
22 | var rootPath = path.dirname(module.filename);
23 |
24 | var config = {
25 | output: {
26 | filename: 'app.js',
27 | path: path.join(rootPath, DEBUG?'build-debug/':'build-release/'),
28 | publicPath: '/static/',
29 | sourcePrefix: ' '
30 | },
31 | entry: path.join(rootPath, 'src/app.js'),
32 | plugins: ([
33 | new webpack.optimize.OccurenceOrderPlugin(),
34 | new webpack.DefinePlugin(GLOBALS)
35 | ].concat(DEBUG ? [] : [
36 | new webpack.optimize.DedupePlugin(),
37 | new webpack.optimize.UglifyJsPlugin(),
38 | new webpack.optimize.AggressiveMergingPlugin()
39 | ])
40 | ),
41 | cache: DEBUG,
42 | debug: DEBUG,
43 | devtool: DEBUG ? '#inline-source-map' : false,
44 |
45 | resolve: {
46 | root: rootPath,
47 | extensions: ['', '.webpack.js', '.web.js', '.js', '.jsx']
48 | },
49 | resolveLoader: {
50 | root: path.join(rootPath, 'node_modules'),
51 | extensions: ['', '.loader.js', '.js', '.jsx']
52 | },
53 | module: {
54 | preLoaders: [
55 | {
56 | test: /\.js$/,
57 | exclude: /node_modules/,
58 | loader: 'eslint-loader'
59 | }
60 | ],
61 |
62 | loaders: [
63 | {
64 | test: /(\.eot)|(\.woff2?)|(\.ttf)$/,
65 | loader: 'file-loader'
66 | },
67 | {
68 | test: /\.css$/,
69 | loader: 'style-loader!css-loader!' + AUTOPREFIXER_LOADER
70 | },
71 | {
72 | test: /\.less$/,
73 | loader: 'style-loader!css-loader!' + AUTOPREFIXER_LOADER +
74 | '!less-loader'
75 | },
76 | {
77 | test: /\.gif/,
78 | loader: 'url-loader?limit=10000&mimetype=image/gif'
79 | },
80 | {
81 | test: /\.jpg/,
82 | loader: 'url-loader?limit=10000&mimetype=image/jpg'
83 | },
84 | {
85 | test: /\.png/,
86 | loader: 'url-loader?limit=10000&mimetype=image/png'
87 | },
88 | {
89 | test: /\.svg/,
90 | loader: 'url-loader?limit=10000&mimetype=image/svg+xml'
91 | },
92 | {
93 | test: /\.jsx?$/,
94 | exclude:function(t){
95 | return /node_modules/.test(path.relative(rootPath, t));
96 | },
97 | loader: 'babel-loader'
98 | }
99 | ]
100 | },
101 | }
102 |
103 | module.exports = config;
104 |
--------------------------------------------------------------------------------
/lib/ViewModel.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by tdzl2_000 on 2015-09-21.
3 | */
4 |
5 | import {createStore} from 'redux';
6 |
7 | const regKey = /^\[(.*)\]$/;
8 |
9 | function defaultGetter(state, name){
10 | return typeof(state)==='object' ? state[name] : undefined;
11 | }
12 |
13 | function defaultReduceParent(state, newValue, name){
14 | if (typeof(state)==='object'?(state[name] === newValue):(newValue===undefined)) {
15 | return state;
16 | }
17 | if (__DEV__ && state!==undefined){
18 | if (typeof(state) != 'object'){
19 | console.warn('Warning: using default reducer with a '+typeof(state)+' state, may cause data lost.');
20 | }
21 | if (state instanceof Array) {
22 | console.warn("Warning: using default reducer with a Array state, may cause data lost.");
23 | }
24 | }
25 | if (newValue === undefined ){
26 | // remove value from state.
27 | if (typeof (state) === 'object') {
28 | var ret = {};
29 | for (var k in state) {
30 | if (k != name) {
31 | ret[k] = state[k];
32 | }
33 | }
34 | return ret;
35 | } else {
36 | return state;
37 | }
38 | }
39 | if (typeof(state) != 'object'){
40 | return {[name]: newValue};
41 | }
42 | return {...state, [name]: newValue}
43 | }
44 |
45 | function defaultGetterByKey(state, name){
46 | if (typeof(state)!== 'object' || !(state instanceof Array)){
47 | if (__DEV__ && state != undefined) {
48 | console.warn("Warning: using default key getter with a non-array state.");
49 | }
50 | return undefined;
51 | }
52 | let key = name.substr(1, name.length-2);
53 | if (state.find) {
54 | return state.find(v=>typeof(v) === 'object' ? v.key === key : v === key); // Return undefined if not exists.
55 | }
56 | for (var i = 0; i < state.length; i++){
57 | let v = state[i];
58 | if (typeof(v) == 'object' ? v.key === key : v === key){
59 | return v;
60 | }
61 | }
62 | }
63 |
64 | function defaultReduceParentByKey(state, newValue, name){
65 | if (!(state instanceof Array)){
66 | if (__DEV__ && state !== undefined){
67 | console.warn("Warning: using default key reducer with a non-array state. May cause data lost.");
68 | }
69 | return [newValue];
70 | }
71 | if (newValue === undefined){
72 | // Remove the item.
73 | return state.filter(v=>{
74 | return typeof(v) == 'object'?v.key !== key : v !== key
75 | })
76 | }
77 | let key = name.substr(1, name.length-2);
78 | var mark = false, same = false;
79 | var ret = state.map(v=>{
80 | if (typeof(v) == 'object'?v.key === key : v === key){
81 | mark = true;
82 | if (v === newValue){
83 | same = true;
84 | }
85 | return newValue;
86 | }
87 | return v;
88 | })
89 | if (same){
90 | return state;
91 | }
92 | if (!mark){
93 | ret.push(newValue);
94 | }
95 | return ret;
96 | }
97 |
98 | export default class ViewModel
99 | {
100 | constructor(parent, name, getter, reducer){
101 | if (parent) {
102 | // sub view model
103 | this._parent = parent;
104 | //this._name = name;
105 | this._path = [...(parent._path), name];
106 | this._store = parent._store;
107 | this._getter = getter;
108 | this._reduceParent = reducer;
109 | } else {
110 | // root view model.
111 | this._path = [];
112 | this._store = createStore((state, action)=> {
113 | return this.reduce(state, action);
114 | }, (this.constructor).defaultState)
115 | }
116 | }
117 | static get defaultState(){
118 | return undefined;
119 | }
120 | getSubViewModelClass(name){
121 | return ViewModel;
122 | }
123 | getItemClass(key){
124 | return ViewModel;
125 | }
126 | get name(){
127 | return this._path[this._path.length-1];
128 | }
129 | get key(){
130 | let name = this.name;
131 | if (name[0] == '['){
132 | return name.substr(1, name.length-2);
133 | }
134 | }
135 | get store(){
136 | return this._store;
137 | }
138 | get path(){
139 | return this._path;
140 | }
141 | get state(){
142 | if (!this._parent) {
143 | return this._store.getState();
144 | }
145 | return this._getter(this._parent.state, this.name);
146 | }
147 | getSubViewModel(name, clazz, getter, reduceParent){
148 | if (name[0] == '['){
149 | return this.getItemByKey(name.substr(1, name.length-2), clazz, getter, reduceParent);
150 | } else {
151 | clazz = clazz || this.getSubViewModelClass(name);
152 | return new clazz(this, name, getter || defaultGetter, reduceParent || defaultReduceParent);
153 | }
154 | }
155 | getItemByKey(key, clazz, getter, reduceParent){
156 | clazz = clazz || this.getItemClass(key);
157 | return new clazz(this, '['+key+']', getter || defaultGetterByKey, reduceParent || defaultReduceParentByKey);
158 | }
159 | dispatch(method, ...Args){
160 | this._store.dispatch({
161 | type: 'REDUX_VM_DISPATCH',
162 | path: this._path,
163 | method: method,
164 | args: Args
165 | })
166 | }
167 | _reduce(state, path, method, args) {
168 | if (!path || !path.length){
169 | // method on this;
170 | return this[method](state, ...args);
171 | } else {
172 | let name = path.shift();
173 | let sub;
174 | if (name[0] == '[') {
175 | sub = this.getItemByKey(name.substr(1, name.length-2));
176 | } else {
177 | sub = this[name] || this.getSubViewModel(name);
178 | }
179 | return sub._reduceParent(state, sub._reduce(sub._getter(state, name), path, method, args), name);
180 | }
181 | }
182 | reduce(state, action){
183 | if (this.path.length !== 0){
184 | throw new Error("Do not call reduce directly! use dispatch instead.");
185 | }
186 | if (action.type==='REDUX_VM_DISPATCH'){
187 | return this._reduce(state, [...action.path], action.method, action.args);
188 | } else {
189 | return state;
190 | }
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Redux-ViewModel #
2 |
3 | Redux-ViewModel is designed to beautify your code with [React](facebook.github.io/react/) & [Redux](http://rackt.github.io/redux/).
4 |
5 | You don't have to write action factory and switch-cases to identity actions any more.
6 |
7 | ## Changelog ##
8 |
9 | ### 0.4.2 ###
10 |
11 | bugfix: state doesn't change sometime when use with react-router.
12 |
13 | ### 0.4.1 ###
14 |
15 | Please skip this version.
16 |
17 | ### 0.4.0 ###
18 |
19 | * Now, return a undefined state will remove self in parent state. Specially in a list. See [#3](https://github.com/tdzl2003/redux-viewmodel/issues/3)
20 |
21 | * Fix issue: "0" and 0 was consider as same key in a array before 0.3.1. Now they are not.
22 |
23 | ### 0.3.1 ###
24 |
25 | * Change dependency of 'react' version to '*' for issue with React 0.14.0
26 |
27 | ### 0.3.0 ###
28 |
29 | * Constructor arguments changed.
30 |
31 | * ViewModel.state property works fine now.
32 |
33 | * Parent state should not change if reducer doesn't change state. This can make view re-render less.
34 |
35 | * Key will store in a pair of square brackets like '[key]' in path.
36 |
37 | * ListViewModel only have different defaultState, all it's method can be accessed in ViewModel.js. You can use ViewModel even if your state is a array.
38 |
39 | * Add method `getDefaultSubViewModelClass(name)` and `getDefaultItemClass(key)`. You can override this to provide
40 | a default class for sub-viewmodel or item-viewmodel
41 |
42 | * Add property `name`
43 |
44 | * Fix Array.prototype.find on IE9
45 |
46 | ### 0.2.1 ###
47 |
48 | * Fix a bug that provider didn't rerender when use together with react-router
49 |
50 | * Known issue: ViewModel.state was broken now.
51 |
52 | ### 0.2.0 ###
53 |
54 | * Add getter/reducer parameter to view model constructor, make's you can create view model alias or special view model like .first/.last
55 |
56 | * Known issue: ViewModel.state was broken now.
57 |
58 | ## Overview ##
59 |
60 | ### Counter demo: ###
61 |
62 | ```javascript
63 | /**
64 | * Created by tdzl2_000 on 2015-08-28.
65 | */
66 |
67 | import React from 'react';
68 | import {createStore} from 'redux';
69 | import {ViewModel, Provider} from 'redux-viewmodel';
70 |
71 | class CounterViewModel extends ViewModel
72 | {
73 | static get defaultState(){
74 | return 0;
75 | }
76 | increment(state, val){
77 | return state + (val||1);
78 | }
79 | decrement(state, val){
80 | return state - (val||1);
81 | }
82 | }
83 |
84 | class RootViewModel extends ViewModel
85 | {
86 | static get defaultState(){
87 | return {
88 | counter: CounterViewModel.defaultState
89 | }
90 | }
91 | get counter(){
92 | return this._counter = this._counter || this.getSubViewModel('counter', CounterViewModel);
93 | }
94 | }
95 |
96 | class NumbericView extends React.Component
97 | {
98 | render(){
99 | return ({this.props.value});
100 | }
101 | }
102 |
103 | var rootViewModel = new RootViewModel();
104 |
105 | class AppView extends React.Component
106 | {
107 | render(){
108 | return (
109 |
110 |
111 |
114 |
117 |
);
118 | }
119 | }
120 |
121 | React.render((
122 |
123 | ), document.getElementById("container"));
124 |
125 | ```
126 |
127 | ## Introduce ##
128 |
129 | React-ViewModel make it possible to write 'ViewModel' classes to reorganize code for reducer implement in Redux.
130 |
131 | ### View-Model Class ###
132 |
133 | View-Model class in React-ViewModel is defined as a class that contains reducer functions as methods.
134 |
135 | In general, there should be a single 'RootViewModel' for a single app. It will hold store instance
136 | and will be used to generate any other View-Model object in getters.
137 |
138 | ### Reducer Method ###
139 |
140 | You can define reducer method in View-Model Class. Like reducer function in Redux, reducer method
141 | is method of View-Model class that receive a state and return a new state. But unlike reducer function
142 | in Redux, reducer method can receive any count of parameters instead of a single action parameter.
143 |
144 | You can call `viewModel.dispatch(methodName, ...args)` to dispatch a reducer action. The method will be execute later
145 | and the state tree will be updated.
146 |
147 | ### SubViewModel ###
148 |
149 | ViewModel can have sub-view-models as property. Sub-view-model can be get by call `this.getSubViewModel(name, clazz)`.
150 |
151 | Name of sub-view-model Does not need to match state field from 0.2.0. You can create alias of a sub-view-model by
152 | implement a getter that returns a sub-view-model with different getter/reducer.
153 |
154 | It doesn't matter that whether you cache the sub-view-model object. It doesn't store any state, instead, it store a
155 | path from RootViewModel. If the path doesn't exists, any reducer that return a valid value will create the path.
156 |
157 | Sub-view-model will use same store with the root one.
158 |
159 | ### ListViewModel ###
160 |
161 | ListViewModel is view-model that operate a Array state. All item in state should be either a object with 'key' property,
162 | or a string/number value that mark key of itself. All actions to list should use key to identify items instead of array index.
163 |
164 | You can also create sub-view-model alias for ListViewModel from 0.2.0. See 'Switch first' button in 'todolist' for a sample.
165 |
166 | From 0.3.0, there's no different between ViewModel and ListViewModel except defaultState. You can
167 | use ViewModel on a array state.
168 |
169 | ### Provider ###
170 |
171 | Use redux-viewmodel with react, you should use Provider component as root component.
172 |
173 | Provider will subscribe the store, and wrap state of root view-model as props of the real root view.
174 |
175 | ## Reference ##
176 |
177 | ### class ViewModel ###
178 |
179 | All View-Model class should extend class ViewModel.
180 |
181 | #### constructor() ####
182 |
183 | Create a root view-model. A new store will be created.
184 |
185 | #### constructor(parent, name, getter, reducer) ####
186 |
187 | Create a sub view-model. This should be called via ViewModel::getSubViewModel or ViewModel::getItemByKey.
188 |
189 | * parent: parent view-model.
190 |
191 | * name: Name of this view model.
192 |
193 | * getter(state, name): get sub-state object from state of parent view-model.
194 |
195 | * reduceParent(state, newValue, name): reduce state of current view-model in parent state.
196 |
197 | #### static get defaultState() ####
198 |
199 | Return a default State. Can be overridden. Return `undefined` if not.
200 |
201 | #### getSubViewModelClass(name) ####
202 |
203 | Return view-model class for sub-view-model.
204 |
205 | #### getItemClass(name) ####
206 |
207 | Return view-model class for item.
208 |
209 | #### get name() ####
210 |
211 | Return name of current view-model class.
212 |
213 | #### get key() ####
214 |
215 | Return key of current view-model class if the view-model is a item view-model.
216 |
217 | #### get store() ####
218 |
219 | Return store instance associated with the view model.
220 |
221 | #### get path() ####
222 |
223 | Return path of current view-model.
224 |
225 | #### get state() ####
226 |
227 | Return state data of the view model.
228 |
229 | #### getSubViewModel(name[, clazz, [getter, reduceParent]]) ####
230 |
231 | Create sub-view-model instance of field `name`
232 |
233 | See description of `constructor(parent, name, getter, reduceParent)` about `getter` and `reduceParent`.
234 |
235 | #### getItemByKey(key[, clazz, [getter, reduceParent]]) ####
236 |
237 | Create sub-view-model instance of item with speficied `key`.
238 |
239 | See description of `constructor(parent, name, getter, reduceParent)` about `getter` and `reduceParent`.
240 |
241 | #### dispatch(methodName, ...args) ####
242 |
243 | Dispatch a reduce action. The method `methodName` will be executed and the state tree of the store will be updated.
244 |
245 | The generated action is like this:
246 |
247 | ```javascript
248 | {
249 | "type": "REDUX_VM_DISPATCH",
250 | "path": [/*An array with all path splits */],
251 | "method": "methodName",
252 | args: [/*All extra arguments.*/]
253 | }
254 | ```
255 |
256 | #### _reduce(state, path, method, args) ####
257 |
258 | Called before the reducer method is invoked. Path is relative to current view-model.
259 |
260 | You can override this method to do extra work.
261 |
262 | You can also invoke this method to simulate multiple works in a reducer method.
263 |
264 | ### Provider ###
265 |
266 | #### prop: viewModel ####
267 |
268 | The view-model used inside the provider.
269 |
270 | #### prop: viewClass ####
271 |
272 | The root component class of this application.
273 |
274 | #### prop: viewFactory (props, viewModel) ####
275 |
276 | A function that return the root component of this application. If viewFactory is passed, viewClass will be ignored.
277 |
278 | The root state(as props) and the root view model will be passed to viewFactory.
279 |
280 | #### prop: children ####
281 |
282 | Children of provider will be passed to the component directly.
283 |
284 | ## F.A.Q. ##
285 |
286 | ### Can I create many root view models? ###
287 |
288 | I think you can, but usually you don't need to.
289 |
290 | ### Can I pass view-models as prop of components? ###
291 |
292 | Yes, you can.
293 |
294 | ### Can a view-model be associated to two or more state object? ###
295 |
296 | No. Reducer method will called only on one sub-state. You can implement reducer function on parent view-model to do this.
297 |
298 | You can invoke `_reduce` method to simulate action on multiply sub-view model, like this:
299 |
300 | ```
301 | class someListViewModel extends ListViewModel
302 | {
303 | checkAll(state){
304 | // run 'check' on each item in a single action.
305 | var keys = state.map(v=>v.key)
306 | keys.forEach(key=>{
307 | state = this._reduce(state, ['['+key+']'], 'check', []);
308 | })
309 | return state;
310 | }
311 | }
312 | ```
313 |
314 | ### Can I create a sub-view-model alias? ###
315 |
316 | You can since v0.2.0. You can just return another sub-view-model object(which should also be accessible), or return
317 | a view-model class with specified `getter` and `reducer`.
318 |
319 | See 'Switch first' button in 'todolist' for a sample.
320 |
321 | ### Can I create many providers? ###
322 |
323 | Yes. And they can be bind to different view model created from a same root.
324 |
325 | ### Can I use same view-model/component class in different path? ###
326 |
327 | Yes, that's the right way to use Redux-ViewModel.
328 |
329 | Data of same `class` may use same view-model class, even they are on different path.
330 |
331 | For example, if we are writing a site like github, the following path with different value may have same class:
332 |
333 | ```
334 | projects, tdzl2003/lua.js
335 | users, tdzl2003, ownedProjects, tdzl2003/redux-viewmodel
336 | users, tdzl2003, stars, stewartlord/identicon.js
337 | users, tdzl2003, contributed, facebook/react-native.js
338 | ```
339 |
340 | So they can be visited with same view-model class like `ProjectViewModel`, and be rendered with same component like 'ProjectShortInfo' or 'ProjectDetails'.
341 |
342 | Also, you can use same view-model on different components to render/interact differently, but don't use different
343 | view-model class with same path.
344 |
345 | ### Can I use redux-viewmodel with react-native? ###
346 |
347 | Yes.
348 |
349 | ### Can I use redux-viewmodel with angularjs? ###
350 |
351 | I think so, but I didn't test it.
352 |
353 | ### Can I use redux-viewmodel with react-router? ###
354 |
355 | Yes, use Provider as root of your router handler and use viewFactory to create actually views, like this:
356 |
357 | ```javascript
358 | import {Provider} from 'redux-viewmodel';
359 | import RootViewModel from '../viewmodels/root';
360 |
361 | class Site extends React.Component
362 | {
363 | renderContent(props, children, vm){
364 | return (