├── .gitignore ├── README.md ├── index.js ├── lib ├── ListViewModel.js ├── Provider.js └── ViewModel.js ├── package.json └── sample ├── counter ├── .eslintrc ├── .gitignore ├── index.html ├── index.js ├── src │ └── app.js └── webpack.config.js └── todolist ├── .eslintrc ├── .gitignore ├── index.html ├── index.js ├── src ├── app.js ├── viewmodels │ ├── root.js │ └── todolist.js └── views │ ├── app.js │ ├── item.js │ └── item.less └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /.idea 3 | -------------------------------------------------------------------------------- /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 (); 367 | } 368 | render(){ 369 | var children = this.props.children; 370 | return (this.renderContent(props, children, vm) 373 | } 374 | viewModel={RootViewModel.instance} 375 | />); 376 | } 377 | } 378 | 379 | class Users extends React.Component 380 | { 381 | renderContent(props, children, vm){ 382 | return (
383 | {children} 384 |
); 385 | } 386 | render(){ 387 | var children = this.props.children; 388 | 389 | return (this.renderContent(props, children , vm) 392 | } 393 | viewModel={RootViewModel.instance.users} 394 | />); 395 | } 396 | } 397 | 398 | import { Router, Route } from 'react-router'; 399 | import { createHistory } from 'history' 400 | 401 | let history = createHistory(); 402 | 403 | $(function(){ 404 | React.render( 405 | 406 | 407 | 408 | 409 | , document.body); 410 | }); 411 | 412 | 413 | ``` 414 | 415 | It's also possible to use same provider by override createElement function of Router, like this: 416 | 417 | ```javascript 418 | // Simple site without modify 419 | class Site extends React.Component 420 | { 421 | render(){ 422 | return (
423 | 424 | {this.props.children} 425 | 426 |
); 427 | } 428 | } 429 | 430 | function createComponetWithProvider(Component, props){ 431 | return ( 433 | {props.children} 434 | ); 435 | } 436 | 437 | $(function(){ 438 | React.render( 439 | 440 | 441 | 442 | 443 | , document.body); 444 | }); 445 | 446 | ``` 447 | 448 | ### Can I use redux-viewmodel with xxx or yyy or zzz? ### 449 | 450 | I don't know. Try it yourself. Tell me what's happening if any error occured. 451 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2_000 on 2015-09-21. 3 | */ 4 | 5 | import ViewModel from './lib/ViewModel'; 6 | import Provider from './lib/Provider'; 7 | import ListViewModel from './lib/ListViewModel'; 8 | 9 | exports.ViewModel = ViewModel; 10 | exports.Provider = Provider; 11 | exports.ListViewModel = ListViewModel; -------------------------------------------------------------------------------- /lib/ListViewModel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2_000 on 2015-09-21. 3 | */ 4 | 5 | import ViewModel from './ViewModel'; 6 | 7 | export default class ListViewModel extends ViewModel 8 | { 9 | static get defaultState(){ 10 | return []; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/Provider.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2_000 on 2015-09-21. 3 | */ 4 | 5 | import React from 'react'; 6 | 7 | export default class Provider extends React.Component{ 8 | constructor(props){ 9 | super({}); 10 | 11 | this.state = { 12 | state: props.viewModel.state 13 | }; 14 | } 15 | resubscribe(){ 16 | if (this._unsubscribe){ 17 | this._unsubscribe(); 18 | this._unsubscribe = undefined; 19 | } 20 | this._unsubscribe = this.props.viewModel.store.subscribe(()=>{ 21 | if (this.props.viewModel.state != this.state.state) { 22 | this.setState({ 23 | state: this.props.viewModel.state 24 | }) 25 | } 26 | }); 27 | } 28 | componentWillMount(){ 29 | this.resubscribe(); 30 | } 31 | componentWillReceiveProps (nextProps){ 32 | if (nextProps.viewModel != this.props.viewModel){ 33 | this.resubscribe(nextProps); 34 | this.setState({ state: nextProps.viewModel.state}); 35 | } 36 | } 37 | componentWillUnmount(){ 38 | if (this._unsubscribe){ 39 | this._unsubscribe(); 40 | this._unsubscribe = undefined; 41 | } 42 | } 43 | render(){ 44 | if (this.props.viewFactory){ 45 | return this.props.viewFactory( this.state.state, this.props.viewModel ); 46 | } 47 | let Class = this.props.viewClass; 48 | return ({this.props.children}); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-viewmodel", 3 | "version": "0.4.2", 4 | "description": "View-Model design based on Redux & React", 5 | "main": "index.js", 6 | "repository": "https://github.com/tdzl2003/redux-viewmodel", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "autoprefixer-loader": "^2.0.0", 14 | "babel-core": "^5.8.23", 15 | "babel-eslint": "^4.1.0", 16 | "babel-loader": "^5.3.2", 17 | "css-loader": "^0.16.0", 18 | "eslint": "^1.2.1", 19 | "eslint-loader": "^1.0.0", 20 | "eslint-plugin-react": "^3.3.0", 21 | "expose-loader": "^0.7.0", 22 | "file-loader": "^0.8.4", 23 | "less": "^2.5.1", 24 | "less-loader": "^2.2.0", 25 | "minimist": "^1.2.0", 26 | "react-tools": "^0.13.3", 27 | "style-loader": "^0.12.3", 28 | "url-loader": "^0.5.6", 29 | "webpack": "^1.12.0", 30 | "webpack-dev-server": "^1.10.1" 31 | }, 32 | "dependencies": { 33 | "react": "*", 34 | "redux": "^3.0.4" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /sample/counter/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": [ 4 | "react" 5 | ], 6 | "env": { 7 | "browser": true, 8 | "es6": true 9 | }, 10 | "globals": { 11 | "__DEV__": true 12 | }, 13 | "ecmaFeatures": { 14 | "jsx": true 15 | }, 16 | "rules": { 17 | // Strict mode 18 | "strict": [2, "never"], 19 | 20 | // Code style 21 | "indent": [2, 4, {"SwitchCase": 4, "VariableDeclarator":4}], 22 | "quotes": [2, "single"] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /sample/counter/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.idea 3 | /build-debug 4 | /build-release 5 | -------------------------------------------------------------------------------- /sample/counter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Redux View Model Demo Page 6 | 7 | 8 |
9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /sample/counter/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2_000 on 2015-09-01. 3 | */ 4 | 5 | // Provide static file for express.js 6 | 7 | "use strict"; 8 | 9 | var path = require("path"); 10 | var config = require("./webpack.config.js"); 11 | var staticPath = config.output.path; 12 | 13 | exports.staticPath = staticPath; 14 | exports.config = config; 15 | 16 | var webpack = require("webpack"); 17 | var compiler = webpack(config); 18 | 19 | exports.watch = function() { 20 | var watcher = compiler.watch({}, function (err, stats) { 21 | if (err) { 22 | if (stats) { 23 | console.error(stats.toString()); 24 | } else { 25 | console.error(err.stack); 26 | } 27 | return; 28 | } 29 | console.log(stats.toString({colors:true, source:false, chunkModules:false})); 30 | }); 31 | 32 | return watcher; 33 | } -------------------------------------------------------------------------------- /sample/counter/src/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2_000 on 2015-08-28. 3 | */ 4 | 5 | import React from 'react'; 6 | import {createStore} from 'redux'; 7 | import {ViewModel, Provider} from 'redux-viewmodel'; 8 | 9 | class CounterViewModel extends ViewModel 10 | { 11 | static get defaultState(){ 12 | return 0; 13 | } 14 | increment(state, val){ 15 | return state + (val||1); 16 | } 17 | decrement(state, val){ 18 | return state - (val||1); 19 | } 20 | } 21 | 22 | class RootViewModel extends ViewModel 23 | { 24 | static get defaultState(){ 25 | return { 26 | counter: CounterViewModel.defaultState 27 | } 28 | } 29 | get counter(){ 30 | return this._counter = this._counter || this.getSubViewModel('counter', CounterViewModel); 31 | } 32 | } 33 | 34 | class NumbericView extends React.Component 35 | { 36 | render(){ 37 | return ({this.props.value}); 38 | } 39 | } 40 | 41 | var rootViewModel = new RootViewModel(); 42 | 43 | class AppView extends React.Component 44 | { 45 | render(){ 46 | return ( 47 |
48 | 49 | 52 | 55 |
); 56 | } 57 | } 58 | 59 | React.render(( 60 | 61 | ), document.getElementById('container')); 62 | -------------------------------------------------------------------------------- /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/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": [ 4 | "react" 5 | ], 6 | "env": { 7 | "browser": true, 8 | "es6": true 9 | }, 10 | "globals": { 11 | "__DEV__": true 12 | }, 13 | "ecmaFeatures": { 14 | "jsx": true 15 | }, 16 | "rules": { 17 | // Strict mode 18 | "strict": [2, "never"], 19 | 20 | // Code style 21 | "indent": [2, 4, {"SwitchCase": 4, "VariableDeclarator":4}], 22 | "quotes": [2, "single"] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /sample/todolist/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.idea 3 | /build-debug 4 | /build-release 5 | -------------------------------------------------------------------------------- /sample/todolist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Redux View Model Demo Page 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /sample/todolist/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2_000 on 2015-09-01. 3 | */ 4 | 5 | // Provide static file for express.js 6 | 7 | "use strict"; 8 | 9 | var path = require("path"); 10 | var config = require("./webpack.config.js"); 11 | var staticPath = config.output.path; 12 | 13 | exports.staticPath = staticPath; 14 | exports.config = config; 15 | 16 | var webpack = require("webpack"); 17 | var compiler = webpack(config); 18 | 19 | exports.watch = function() { 20 | var watcher = compiler.watch({}, function (err, stats) { 21 | if (err) { 22 | if (stats) { 23 | console.error(stats.toString()); 24 | } else { 25 | console.error(err.stack); 26 | } 27 | return; 28 | } 29 | console.log(stats.toString({colors:true, source:false, chunkModules:false})); 30 | }); 31 | 32 | return watcher; 33 | } -------------------------------------------------------------------------------- /sample/todolist/src/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2_000 on 2015-08-28. 3 | */ 4 | 5 | import React from 'react'; 6 | import {Provider} from 'redux-viewmodel'; 7 | import RootViewModel from './viewmodels/root'; 8 | import AppView from "./views/app"; 9 | 10 | React.render(( 11 | 12 | ), document.body); 13 | -------------------------------------------------------------------------------- /sample/todolist/src/viewmodels/root.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2_000 on 2015-09-21. 3 | */ 4 | 5 | import {ViewModel} from 'redux-viewmodel'; 6 | import TodoListViewModel from './todolist.js'; 7 | 8 | var _instance; 9 | 10 | export default class RootViewModel extends ViewModel 11 | { 12 | static get instance(){ 13 | if (!_instance){ 14 | _instance = new RootViewModel(); 15 | } 16 | return _instance; 17 | } 18 | static get defaultState(){ 19 | return { 20 | todoList: TodoListViewModel.defaultState 21 | } 22 | } 23 | get todoList(){ 24 | return this._todoList = this._todoList || this.getSubViewModel('todoList', TodoListViewModel); 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /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/todolist/src/views/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2_000 on 2015-09-21. 3 | */ 4 | import React from 'react'; 5 | import TodoItemView from './item'; 6 | import RootViewModel from '../viewmodels/root'; 7 | 8 | export default class AppView extends React.Component 9 | { 10 | addItem(){ 11 | var context = RootViewModel.instance; 12 | context.todoList.dispatch("addItem", prompt("Input a title")); 13 | } 14 | switchFirst(){ 15 | var context = RootViewModel.instance; 16 | context.todoList.first.dispatch("switch"); 17 | } 18 | deleteItem(key){ 19 | var context = RootViewModel.instance; 20 | context.todoList.dispatch("deleteItem", key); 21 | } 22 | render(){ 23 | var context = RootViewModel.instance; 24 | return ( 25 |
26 | 27 | 28 |
    29 | { 30 | this.props.todoList.map((v)=>{ 31 | return this.deleteItem(v.key)} 35 | /> 36 | }) 37 | } 38 |
39 |
); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /sample/todolist/src/views/item.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2_000 on 2015-09-21. 3 | */ 4 | import React from 'react'; 5 | import "./item.less"; 6 | 7 | export default class TodoItemView extends React.Component 8 | { 9 | onChange(ev){ 10 | var context = this.props.context; 11 | context.dispatch(ev.target.checked ? "check":"uncheck"); 12 | } 13 | render(){ 14 | var context = this.props.context; 15 | return ( 16 |
  • 17 | this.onChange(ev)} /> 18 | {this.props.title} 19 | 20 |
  • 21 | ) 22 | } 23 | } -------------------------------------------------------------------------------- /sample/todolist/src/views/item.less: -------------------------------------------------------------------------------- 1 | 2 | .todo-list li { 3 | list-style-type: none; 4 | } 5 | 6 | .done { 7 | color: lightgray; 8 | text-decoration: line-through; 9 | } -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------