├── .gitignore ├── .npmignore ├── examples ├── ballade-immutable-todomvc │ ├── src │ │ ├── fonts │ │ │ ├── iconfont.eot │ │ │ ├── iconfont.ttf │ │ │ ├── iconfont.woff │ │ │ └── iconfont.svg │ │ ├── js │ │ │ ├── index.js │ │ │ ├── constants │ │ │ │ └── todos.js │ │ │ ├── dispatcher │ │ │ │ └── dispatcher.js │ │ │ ├── actions │ │ │ │ └── todos.js │ │ │ ├── components │ │ │ │ ├── header.js │ │ │ │ ├── main-section.js │ │ │ │ ├── text-input.js │ │ │ │ ├── footer.js │ │ │ │ ├── app.js │ │ │ │ └── todo-item.js │ │ │ ├── stores │ │ │ │ └── todos.js │ │ │ └── babel-external-helpers.js │ │ ├── index.html │ │ └── css │ │ │ └── app.css │ ├── README.md │ ├── package.json │ └── gulpfile.js └── ballade-mutable-todomvc │ ├── src │ ├── fonts │ │ ├── iconfont.eot │ │ ├── iconfont.ttf │ │ ├── iconfont.woff │ │ └── iconfont.svg │ ├── js │ │ ├── index.js │ │ ├── constants │ │ │ └── todos.js │ │ ├── dispatcher │ │ │ └── dispatcher.js │ │ ├── actions │ │ │ └── todos.js │ │ ├── components │ │ │ ├── main-section.js │ │ │ ├── header.js │ │ │ ├── text-input.js │ │ │ ├── app.js │ │ │ ├── footer.js │ │ │ └── todo-item.js │ │ ├── stores │ │ │ └── todos.js │ │ └── babel-external-helpers.js │ ├── index.html │ └── css │ │ └── app.css │ ├── README.md │ ├── package.json │ └── gulpfile.js ├── eslintrc.json ├── test ├── mutable │ ├── actions2.js │ ├── dispatcher.js │ ├── actions1.js │ ├── store2.js │ ├── store1.js │ └── index.js ├── immutable │ ├── actions2.js │ ├── dispatcher.js │ ├── actions1.js │ ├── store2.js │ ├── store1.js │ └── index.js ├── schema-immutable.js └── schema-mutable.js ├── src ├── accessor.js ├── immutable-store.js ├── queue.js ├── immutable-deep-equal.js ├── event.js ├── bindstore.js ├── persistence.js ├── copy.js ├── cache.js ├── ballade.js ├── ballade.immutable.js └── store.js ├── license ├── package.json ├── gulpfile.js ├── cache_CN.md ├── cache.md ├── update-guide_CN.md ├── update-guide.md ├── schema_CN.md ├── schema.md └── dist ├── ballade.min.js └── ballade.immutable.min.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | .swap 4 | .orig 5 | .cache 6 | npm-debug.log 7 | bundle.js 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | examples/ 4 | .DS_Store 5 | .swap 6 | .orig 7 | npm-debug.log 8 | .cache 9 | .git 10 | bundle.js 11 | -------------------------------------------------------------------------------- /examples/ballade-immutable-todomvc/src/fonts/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenmnkken/ballade/HEAD/examples/ballade-immutable-todomvc/src/fonts/iconfont.eot -------------------------------------------------------------------------------- /examples/ballade-immutable-todomvc/src/fonts/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenmnkken/ballade/HEAD/examples/ballade-immutable-todomvc/src/fonts/iconfont.ttf -------------------------------------------------------------------------------- /examples/ballade-mutable-todomvc/src/fonts/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenmnkken/ballade/HEAD/examples/ballade-mutable-todomvc/src/fonts/iconfont.eot -------------------------------------------------------------------------------- /examples/ballade-mutable-todomvc/src/fonts/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenmnkken/ballade/HEAD/examples/ballade-mutable-todomvc/src/fonts/iconfont.ttf -------------------------------------------------------------------------------- /examples/ballade-mutable-todomvc/src/fonts/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenmnkken/ballade/HEAD/examples/ballade-mutable-todomvc/src/fonts/iconfont.woff -------------------------------------------------------------------------------- /examples/ballade-immutable-todomvc/src/fonts/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenmnkken/ballade/HEAD/examples/ballade-immutable-todomvc/src/fonts/iconfont.woff -------------------------------------------------------------------------------- /examples/ballade-mutable-todomvc/src/js/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './components/app'; 4 | 5 | ReactDOM.render( 6 | , 7 | document.getElementById('root') 8 | ); 9 | -------------------------------------------------------------------------------- /examples/ballade-immutable-todomvc/src/js/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './components/app'; 4 | 5 | ReactDOM.render( 6 | , 7 | document.getElementById('root') 8 | ); 9 | -------------------------------------------------------------------------------- /examples/ballade-mutable-todomvc/README.md: -------------------------------------------------------------------------------- 1 | # Ballade Mutable TodoMVC Example 2 | 3 | ## Install 4 | ``` 5 | $ npm install 6 | ``` 7 | 8 | ## Running 9 | ``` 10 | $ npm start 11 | ``` 12 | 13 | Enter the `http://localhost:3003` Browse in Browser 14 | -------------------------------------------------------------------------------- /examples/ballade-immutable-todomvc/README.md: -------------------------------------------------------------------------------- 1 | # Ballade Immutable TodoMVC Example 2 | 3 | ## Install 4 | ``` 5 | $ npm install 6 | ``` 7 | 8 | ## Running 9 | ``` 10 | $ npm start 11 | ``` 12 | 13 | Enter the `http://localhost:3004` Browse in Browser 14 | -------------------------------------------------------------------------------- /examples/ballade-mutable-todomvc/src/js/constants/todos.js: -------------------------------------------------------------------------------- 1 | export default { 2 | CREATE: 'create', 3 | UPDATE: 'update', 4 | DELETE: 'delete', 5 | DELETE_COMPLETE: 'delete-complete', 6 | TOGGLE: 'toggle', 7 | TOGGLE_ALL: 'toggle-all' 8 | }; 9 | -------------------------------------------------------------------------------- /examples/ballade-immutable-todomvc/src/js/constants/todos.js: -------------------------------------------------------------------------------- 1 | export default { 2 | CREATE: 'create', 3 | UPDATE: 'update', 4 | DELETE: 'delete', 5 | DELETE_COMPLETE: 'delete-complete', 6 | TOGGLE: 'toggle', 7 | TOGGLE_ALL: 'toggle-all' 8 | }; 9 | -------------------------------------------------------------------------------- /examples/ballade-mutable-todomvc/src/js/dispatcher/dispatcher.js: -------------------------------------------------------------------------------- 1 | const Ballade = require('ballade'); 2 | const dispatcher = new Ballade.Dispatcher(); 3 | 4 | // Register debug middleware 5 | dispatcher.use((payload, next) => { 6 | console.info(`action ${payload.type}`); 7 | console.log(payload); 8 | next(); 9 | }); 10 | 11 | export default dispatcher; 12 | -------------------------------------------------------------------------------- /examples/ballade-immutable-todomvc/src/js/dispatcher/dispatcher.js: -------------------------------------------------------------------------------- 1 | import Ballade from 'ballade/src/ballade.immutable'; 2 | const dispatcher = new Ballade.Dispatcher(); 3 | 4 | // Register debug middleware 5 | dispatcher.use((payload, next) => { 6 | console.info(`action ${payload.type}`); 7 | console.log(payload); 8 | next(); 9 | }); 10 | 11 | export default dispatcher; 12 | -------------------------------------------------------------------------------- /eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "env": { 4 | "node": true 5 | }, 6 | "plugins": [ 7 | "standard" 8 | ], 9 | "rules": { 10 | "strict": 0, 11 | "semi": [2, "always"], 12 | "indent": [2, 4, {"SwitchCase": 1}], 13 | "brace-style": [2, "stroustrup"], 14 | "operator-linebreak": [2, "after"], 15 | "generator-star-spacing": [2, {"before": false, "after": true}] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/ballade-mutable-todomvc/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ballade Mutable TodoMVC Example 6 | 7 | 8 | 9 |
10 | 11 |
12 |

Double-click to edit a todo

13 |
14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/ballade-immutable-todomvc/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ballade Immutable TodoMVC Example 6 | 7 | 8 | 9 |
10 | 11 |
12 |

Double-click to edit a todo

13 |
14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/mutable/actions2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var dispatcher = require('./dispatcher'); 4 | 5 | var actions = dispatcher.createActions({ 6 | updateTitle: function (title) { 7 | return { 8 | type: 'mutable-test2/update-title', 9 | title: title 10 | }; 11 | }, 12 | 13 | addMusic: function (music) { 14 | return { 15 | type: 'mutable-test2/add-music', 16 | music: music 17 | } 18 | } 19 | }); 20 | 21 | module.exports = actions; 22 | -------------------------------------------------------------------------------- /test/immutable/actions2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var dispatcher = require('./dispatcher'); 4 | 5 | var actions = dispatcher.createActions({ 6 | updateTitle: function (title) { 7 | return { 8 | type: 'immutable-test2/update-title', 9 | title: title 10 | }; 11 | }, 12 | 13 | addMusic: function (music) { 14 | return { 15 | type: 'immutable-test2/add-music', 16 | music: music 17 | } 18 | } 19 | }); 20 | 21 | module.exports = actions; 22 | -------------------------------------------------------------------------------- /test/mutable/dispatcher.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Dispatcher = require('../../src/ballade').Dispatcher; 4 | var dispatcher = new Dispatcher(); 5 | 6 | dispatcher.use(function (payload, next) { 7 | if (payload.title) { 8 | setTimeout(function () { 9 | payload.title += ' is'; 10 | next(); 11 | }, 500); 12 | } 13 | else { 14 | next(); 15 | } 16 | }); 17 | 18 | dispatcher.use(function (payload, next) { 19 | if (payload.title) { 20 | payload.title += ' done'; 21 | } 22 | 23 | next(); 24 | }); 25 | 26 | module.exports = dispatcher; 27 | -------------------------------------------------------------------------------- /test/immutable/dispatcher.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Dispatcher = require('../../src/ballade.immutable').Dispatcher; 4 | var dispatcher = new Dispatcher(); 5 | 6 | dispatcher.use(function (payload, next) { 7 | if (payload.title) { 8 | setTimeout(function () { 9 | payload.title += ' is'; 10 | next(); 11 | }, 500); 12 | } 13 | else { 14 | next(); 15 | } 16 | }); 17 | 18 | dispatcher.use(function (payload, next) { 19 | if (payload.title) { 20 | payload.title += ' done'; 21 | } 22 | 23 | next(); 24 | }); 25 | 26 | module.exports = dispatcher; 27 | -------------------------------------------------------------------------------- /examples/ballade-immutable-todomvc/src/fonts/iconfont.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/ballade-mutable-todomvc/src/fonts/iconfont.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/ballade-mutable-todomvc/src/js/actions/todos.js: -------------------------------------------------------------------------------- 1 | import dispatcher from '../dispatcher/dispatcher'; 2 | import constants from '../constants/todos'; 3 | const TODOS = 'todos'; 4 | 5 | const todoActions = dispatcher.createActions({ 6 | create: (text) => ({ 7 | type: `${TODOS}/${constants.CREATE}`, 8 | text 9 | }), 10 | 11 | update: (id, text) => ({ 12 | type: `${TODOS}/${constants.UPDATE}`, 13 | text, 14 | id 15 | }), 16 | 17 | delete: (id) => ({ 18 | type: `${TODOS}/${constants.DELETE}`, 19 | id 20 | }), 21 | 22 | deleteComplete: () => ({ 23 | type: `${TODOS}/${constants.DELETE_COMPLETE}` 24 | }), 25 | 26 | toggle: (id) => ({ 27 | type: `${TODOS}/${constants.TOGGLE}`, 28 | id 29 | }), 30 | 31 | toggleAll: () => ({ 32 | type: `${TODOS}/${constants.TOGGLE_ALL}` 33 | }) 34 | }); 35 | 36 | export default todoActions; 37 | -------------------------------------------------------------------------------- /examples/ballade-immutable-todomvc/src/js/actions/todos.js: -------------------------------------------------------------------------------- 1 | import dispatcher from '../dispatcher/dispatcher'; 2 | import constants from '../constants/todos'; 3 | const TODOS = 'todos'; 4 | 5 | const todoActions = dispatcher.createActions({ 6 | create: (text) => ({ 7 | type: `${TODOS}/${constants.CREATE}`, 8 | text 9 | }), 10 | 11 | update: (id, text) => ({ 12 | type: `${TODOS}/${constants.UPDATE}`, 13 | text, 14 | id 15 | }), 16 | 17 | delete: (id) => ({ 18 | type: `${TODOS}/${constants.DELETE}`, 19 | id 20 | }), 21 | 22 | deleteComplete: () => ({ 23 | type: `${TODOS}/${constants.DELETE_COMPLETE}` 24 | }), 25 | 26 | toggle: (id) => ({ 27 | type: `${TODOS}/${constants.TOGGLE}`, 28 | id 29 | }), 30 | 31 | toggleAll: () => ({ 32 | type: `${TODOS}/${constants.TOGGLE_ALL}` 33 | }) 34 | }); 35 | 36 | export default todoActions; 37 | -------------------------------------------------------------------------------- /examples/ballade-mutable-todomvc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ballade-mutable-todomvc", 3 | "description": "Ballade mutable todomvc example", 4 | "scripts": { 5 | "start": "gulp", 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "author": "chenmnkken@gmail.com", 9 | "license": "ISC", 10 | "devDependencies": { 11 | "babel-plugin-external-helpers": "6.8.0", 12 | "babel-preset-es2015": "6.16.0", 13 | "babel-preset-react": "6.16.0", 14 | "babel-preset-stage-0": "6.16.0", 15 | "babelify": "7.3.0", 16 | "browserify": "13.1.1", 17 | "cacheify": "0.4.1", 18 | "gulp": "3.9.0", 19 | "gulp-connect": "5.0.0", 20 | "gulp-less": "3.1.0", 21 | "gulp-util": "3.0.7", 22 | "leveldown": "1.5.0", 23 | "levelup": "1.3.3", 24 | "vinyl-source-stream": "1.1.0", 25 | "watchify": "3.7.0" 26 | }, 27 | "dependencies": { 28 | "ballade": "^1.0.5", 29 | "object-assign": "4.1.0", 30 | "react": "15.4.2", 31 | "react-dom": "15.4.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/accessor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // accessor for mutable/immutable data 4 | 5 | var accessor = { 6 | set: function (obj, key, value, isImmutable) { 7 | if (value !== undefined && value !== null) { 8 | if (isImmutable) { 9 | obj = obj.set(key, value); 10 | } 11 | else { 12 | obj[key] = value; 13 | } 14 | } 15 | 16 | return obj; 17 | }, 18 | 19 | get: function (obj, key, isImmutable) { 20 | if (isImmutable) { 21 | return obj.get(key); 22 | } 23 | 24 | return obj[key]; 25 | }, 26 | 27 | 'delete': function (obj, key, isImmutable) { 28 | if (isImmutable) { 29 | obj = obj.delete(key); 30 | } 31 | else if (Array.isArray(obj)) { 32 | obj = obj.splice(key, 1); 33 | } 34 | else { 35 | delete obj[key]; 36 | } 37 | 38 | return obj; 39 | } 40 | }; 41 | 42 | module.exports = accessor; 43 | -------------------------------------------------------------------------------- /test/mutable/actions1.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var dispatcher = require('./dispatcher'); 4 | 5 | var actions = dispatcher.createActions({ 6 | updateTitle: function (title) { 7 | return { 8 | type: 'mutable-test1/update-title', 9 | title: title 10 | }; 11 | }, 12 | 13 | addMusic: function (music) { 14 | return { 15 | type: 'mutable-test1/add-music', 16 | music: music 17 | } 18 | }, 19 | 20 | sayHello: function (greetings) { 21 | return { 22 | type: 'mutable-test/say-hello', 23 | greetings: greetings 24 | } 25 | }, 26 | 27 | addUser: function (user) { 28 | return { 29 | type: 'mutable-test/add-user', 30 | user: user 31 | } 32 | }, 33 | 34 | delUser: function (userId) { 35 | return { 36 | type: 'immutable-test/del-user', 37 | userId: userId 38 | } 39 | } 40 | }); 41 | 42 | module.exports = actions; 43 | -------------------------------------------------------------------------------- /test/mutable/store2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var dispatcher = require('./dispatcher'); 4 | var Schema = require('../../src/ballade').Schema; 5 | var store1 = require('./store1'); 6 | 7 | var playitem = new Schema({ 8 | name: String, 9 | musician: String 10 | }); 11 | 12 | var schema = new Schema({ 13 | title: String, 14 | playlist: [playitem], 15 | greetings: String 16 | }); 17 | 18 | var store = dispatcher.createMutableStore(schema, { 19 | 'mutable-test2/update-title': function (store, action) { 20 | store.set('title', action.title); 21 | }, 22 | 23 | 'mutable-test2/add-music': function (store, action) { 24 | var playlist = store.get('playlist'); 25 | playlist.push(action.music); 26 | 27 | store.set('playlist', playlist); 28 | }, 29 | 30 | 'mutable-test/say-hello': function (store, action) { 31 | var greetings = store1.get('greetings') + ' world'; 32 | store.set('greetings', greetings); 33 | } 34 | }); 35 | 36 | module.exports = store; 37 | -------------------------------------------------------------------------------- /test/immutable/actions1.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var dispatcher = require('./dispatcher'); 4 | 5 | var actions = dispatcher.createActions({ 6 | updateTitle: function (title) { 7 | return { 8 | type: 'immutable-test1/update-title', 9 | title: title 10 | }; 11 | }, 12 | 13 | addMusic: function (music) { 14 | return { 15 | type: 'immutable-test1/add-music', 16 | music: music 17 | } 18 | }, 19 | 20 | sayHello: function (greetings) { 21 | return { 22 | type: 'immutable-test/say-hello', 23 | greetings: greetings 24 | } 25 | }, 26 | 27 | addUser: function (user) { 28 | return { 29 | type: 'immutable-test/add-user', 30 | user: user 31 | } 32 | }, 33 | 34 | delUser: function (userId) { 35 | return { 36 | type: 'immutable-test/del-user', 37 | userId: userId 38 | } 39 | } 40 | }); 41 | 42 | module.exports = actions; 43 | -------------------------------------------------------------------------------- /examples/ballade-immutable-todomvc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ballade-immutable-todomvc", 3 | "description": "Ballade immutable todomvc example", 4 | "scripts": { 5 | "start": "gulp", 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "author": "chenmnkken@gmail.com", 9 | "license": "ISC", 10 | "devDependencies": { 11 | "babel-plugin-external-helpers": "6.8.0", 12 | "babel-preset-es2015": "6.16.0", 13 | "babel-preset-react": "6.16.0", 14 | "babel-preset-stage-0": "6.16.0", 15 | "babelify": "7.3.0", 16 | "browserify": "13.1.1", 17 | "cacheify": "0.4.1", 18 | "gulp": "3.9.0", 19 | "gulp-connect": "5.0.0", 20 | "gulp-less": "3.1.0", 21 | "gulp-util": "3.0.7", 22 | "leveldown": "1.5.0", 23 | "levelup": "1.3.3", 24 | "vinyl-source-stream": "1.1.0", 25 | "watchify": "3.7.0" 26 | }, 27 | "dependencies": { 28 | "ballade": "^1.0.5", 29 | "immutable": "3.7.6", 30 | "object-assign": "4.1.0", 31 | "react": "15.4.2", 32 | "react-dom": "15.4.2" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/ballade-mutable-todomvc/src/js/components/main-section.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | import TodoItem from './todo-item'; 3 | 4 | class MainSection extends Component { 5 | static propTypes = { 6 | todos: PropTypes.array, 7 | filter: PropTypes.string 8 | }; 9 | 10 | renderItems = () => { 11 | const {filter, todos} = this.props; 12 | 13 | return todos.map((item, i) => { 14 | if (filter === 'all' || 15 | (filter === 'completed' && item.complete) || 16 | (filter === 'active' && !item.complete) 17 | ) { 18 | return ( 19 | 20 | ); 21 | } 22 | }); 23 | }; 24 | 25 | render () { 26 | return ( 27 |
28 |
    29 | {this.renderItems()} 30 |
31 |
32 | ) 33 | }; 34 | }; 35 | 36 | export default MainSection; 37 | -------------------------------------------------------------------------------- /test/immutable/store2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Immutable = require('immutable'); 4 | var dispatcher = require('./dispatcher'); 5 | var store1 = require('./store1'); 6 | var Schema = require('../../src/ballade').Schema; 7 | 8 | var musicSchema = new Schema({ 9 | name: String, 10 | musician: String 11 | }); 12 | 13 | var schema = new Schema({ 14 | title: String, 15 | playlist: [musicSchema], 16 | greetings: String 17 | }); 18 | 19 | var store = dispatcher.createImmutableStore(schema, { 20 | 'immutable-test2/update-title': function (store, action) { 21 | store.set('title', action.title); 22 | }, 23 | 24 | 'immutable-test2/add-music': function (store, action) { 25 | var playlist = store.get('playlist'); 26 | playlist = playlist.push(Immutable.Map(action.music)); 27 | store.set('playlist', playlist); 28 | }, 29 | 30 | 'immutable-test/say-hello': function (store, action) { 31 | var greetings = store1.get('greetings') + ' world'; 32 | store.set('greetings', greetings); 33 | } 34 | }); 35 | 36 | module.exports = store; 37 | -------------------------------------------------------------------------------- /src/immutable-store.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* global self */ 4 | 5 | var Store = require('./store'); 6 | var _Immutable; 7 | 8 | if (typeof self !== 'undefined' && self.Immutable) { 9 | _Immutable = self.Immutable; 10 | } 11 | else if (typeof global !== 'undefined' && global.Immutable) { 12 | _Immutable = global.Immutable; 13 | } 14 | else { 15 | _Immutable = require('immutable'); 16 | } 17 | 18 | /** 19 | * ImmutableStore Class 20 | * @param {Object} store schema 21 | * @param {Object} store options 22 | * options.cache set cache in store 23 | * options.error schema validator error 24 | * var immutableStore = new ImmutableStore({foo: 'bar'}); 25 | * @immutableStore.immutable: Immutable data 26 | * @immutableStore.event: Event instance 27 | */ 28 | var ImmutableStore = function (schema, options) { 29 | Store.call(this, schema, options, _Immutable); 30 | }; 31 | 32 | ImmutableStore.prototype = Object.create(Store.prototype, { 33 | constructor: { 34 | value: ImmutableStore, 35 | enumerable: false, 36 | writable: true, 37 | configurable: true 38 | } 39 | }); 40 | 41 | module.exports = ImmutableStore; 42 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Yiguo Chen (stylechen.com) 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/ballade-immutable-todomvc/src/js/components/header.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | import TextInput from './text-input'; 3 | import todoActions from '../actions/todos'; 4 | 5 | class Header extends Component { 6 | static propTypes = { 7 | completed: PropTypes.bool 8 | }; 9 | 10 | handleSave = (text) => { 11 | todoActions.create(text); 12 | }; 13 | 14 | handleToggle = () => { 15 | todoActions.toggleAll(); 16 | }; 17 | 18 | render () { 19 | let toggleClassName = 'icon-font toggle-all'; 20 | 21 | if (this.props.completed) { 22 | toggleClassName += ' completed'; 23 | } 24 | 25 | return ( 26 |
27 |

todos

28 | 35 |
36 | ) 37 | }; 38 | }; 39 | 40 | export default Header; 41 | -------------------------------------------------------------------------------- /examples/ballade-mutable-todomvc/src/js/components/header.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | import TextInput from './text-input'; 3 | import todoActions from '../actions/todos'; 4 | 5 | class Header extends Component { 6 | static propTypes = { 7 | completed: PropTypes.bool 8 | }; 9 | 10 | handleSave = (text) => { 11 | todoActions.create(text); 12 | }; 13 | 14 | handleToggle = () => { 15 | todoActions.toggleAll(); 16 | }; 17 | 18 | render () { 19 | let toggleClassName = 'icon-font toggle-all'; 20 | 21 | if (this.props.completed) { 22 | toggleClassName += ' completed'; 23 | } 24 | 25 | return ( 26 |
27 |

todos

28 |
35 |
36 | ) 37 | }; 38 | }; 39 | 40 | export default Header; 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ballade", 3 | "version": "1.2.8", 4 | "description": "For unidirectional data flow.", 5 | "keywords": [ 6 | "flux", 7 | "react", 8 | "action", 9 | "store", 10 | "cache", 11 | "immutable", 12 | "dispatch" 13 | ], 14 | "main": "src/ballade.js", 15 | "scripts": { 16 | "build": "./node_modules/.bin/gulp", 17 | "lint": "./node_modules/.bin/gulp lint", 18 | "test": "cd test; ../node_modules/.bin/mocha schema-mutable.js; ../node_modules/.bin/mocha schema-immutable.js; cd ./mutable; ../../node_modules/.bin/mocha index.js; cd ../immutable; ../../node_modules/.bin/mocha index.js" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/chenmnkken/ballade" 23 | }, 24 | "bugs": "https://github.com/chenmnkken/ballade/issues", 25 | "author": "chenmnkken@gmail.com", 26 | "license": "MIT", 27 | "devDependencies": { 28 | "browserify": "12.0.1", 29 | "eslint-config-standard": "7.1.0", 30 | "eslint-plugin-promise": "3.5.0", 31 | "eslint-plugin-standard": "2.1.1", 32 | "gulp": "3.9.0", 33 | "gulp-eslint": "3.0.1", 34 | "gulp-uglify": "1.5.1", 35 | "immutable": "3.7.6", 36 | "mocha": "2.3.4", 37 | "vinyl-buffer": "1.0.0", 38 | "vinyl-source-stream": "1.1.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/ballade-immutable-todomvc/src/js/components/main-section.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | import TodoItem from './todo-item'; 3 | 4 | class MainSection extends Component { 5 | static propTypes = { 6 | $todos: PropTypes.object, 7 | filter: PropTypes.string 8 | }; 9 | 10 | renderItems = () => { 11 | const {filter, $todos} = this.props; 12 | 13 | return $todos.map((item, i) => { 14 | const id = item.get('id'); 15 | const text = item.get('text'); 16 | const complete = item.get('complete'); 17 | 18 | if (filter === 'all' || 19 | (filter === 'completed' && complete) || 20 | (filter === 'active' && !complete) 21 | ) { 22 | return ( 23 | 28 | ); 29 | } 30 | }); 31 | }; 32 | 33 | render () { 34 | return ( 35 |
36 |
    37 | {this.renderItems()} 38 |
39 |
40 | ) 41 | }; 42 | }; 43 | 44 | export default MainSection; 45 | -------------------------------------------------------------------------------- /src/queue.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Mini Queue Class 5 | * @param {Function} complete callback, 6 | * when queue is done, then invoke complete callback 7 | * @param {Boolean} whether execute workflow of loop 8 | */ 9 | var Queue = function (completeCallback, loop) { 10 | this.workflows = []; 11 | this.completeCallback = completeCallback; 12 | 13 | if (loop) { 14 | this._workflows = []; 15 | } 16 | }; 17 | 18 | Queue.prototype = { 19 | /** 20 | * Enter queue 21 | * @param {Function} workflow function 22 | */ 23 | enter: function (workflow) { 24 | this.workflows.push(workflow); 25 | 26 | // Backup workflow 27 | if (this._workflows) { 28 | this._workflows.push(workflow); 29 | } 30 | }, 31 | 32 | /** 33 | * Execute workflow 34 | * @param {Object} workflow function data required 35 | */ 36 | execute: function (data, workflows) { 37 | workflows = workflows || this.workflows.concat(); 38 | var workflow; 39 | 40 | if (workflows.length) { 41 | workflow = workflows.shift(); 42 | workflow(data, this.execute.bind(this, data, workflows)); 43 | } 44 | else { 45 | // Get backup, begin loop 46 | if (this._workflows) { 47 | this.workflows = this._workflows.concat(); 48 | } 49 | 50 | workflows = null; 51 | this.completeCallback(data); 52 | } 53 | } 54 | }; 55 | 56 | module.exports = Queue; 57 | -------------------------------------------------------------------------------- /src/immutable-deep-equal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* global self */ 4 | 5 | var _Immutable; 6 | 7 | if (typeof self !== 'undefined' && self.Immutable) { 8 | _Immutable = self.Immutable; 9 | } 10 | else if (typeof global !== 'undefined' && global.Immutable) { 11 | _Immutable = global.Immutable; 12 | } 13 | else { 14 | _Immutable = require('immutable'); 15 | } 16 | 17 | var keys = Object.keys; 18 | var is = _Immutable.is; 19 | 20 | var immutableDeepEqual = function (Component) { 21 | Component.prototype.shouldComponentUpdate = function (nextProps, nextState) { 22 | var context = this; 23 | var currentState = context.state; 24 | var currentProps = context.props; 25 | var nextStateKeys = keys(nextState || {}); 26 | var nextPropsKeys = keys(nextProps || {}); 27 | var isUpdate; 28 | 29 | if (nextStateKeys.length !== keys(currentState || {}).length || 30 | nextPropsKeys.length !== keys(currentProps || {}).length 31 | ) { 32 | return true; 33 | } 34 | 35 | isUpdate = nextStateKeys.some(function (item) { 36 | return currentState[item] !== nextState[item] && 37 | !is(currentState[item], nextState[item]); 38 | }); 39 | 40 | return isUpdate || nextPropsKeys.some(function (item) { 41 | return currentProps[item] !== nextProps[item] && 42 | !is(currentProps[item], nextProps[item]); 43 | }); 44 | }; 45 | 46 | return Component; 47 | }; 48 | 49 | module.exports = immutableDeepEqual; 50 | -------------------------------------------------------------------------------- /examples/ballade-immutable-todomvc/src/js/components/text-input.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | 3 | class TextInput extends Component { 4 | state = { 5 | value: this.props.value || '' 6 | }; 7 | 8 | static propTypes = { 9 | placeholder: PropTypes.string, 10 | onSave: PropTypes.func.isRequired, 11 | value: PropTypes.string, 12 | bindBlur: PropTypes.bool 13 | }; 14 | 15 | handleChange = (event) => { 16 | this.setState({ 17 | value: event.target.value 18 | }); 19 | }; 20 | 21 | handleBlur = () => { 22 | if (this.props.bindBlur) { 23 | this.save(this.state.value); 24 | } 25 | }; 26 | 27 | handleKeyDown = (event) => { 28 | if (event.keyCode === 13) { 29 | this.save(this.state.value); 30 | } 31 | }; 32 | 33 | save = (text) => { 34 | text = text.trim(); 35 | 36 | if (text) { 37 | this.props.onSave(text); 38 | 39 | this.setState({ 40 | value: '' 41 | }); 42 | } 43 | }; 44 | 45 | render () { 46 | return ( 47 | 55 | ) 56 | }; 57 | }; 58 | 59 | export default TextInput; 60 | -------------------------------------------------------------------------------- /examples/ballade-mutable-todomvc/src/js/components/text-input.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | 3 | class TextInput extends Component { 4 | state = { 5 | value: this.props.value || '' 6 | }; 7 | 8 | static propTypes = { 9 | placeholder: PropTypes.string, 10 | onSave: PropTypes.func.isRequired, 11 | value: PropTypes.string, 12 | bindBlur: PropTypes.bool 13 | }; 14 | 15 | handleChange = (event) => { 16 | this.setState({ 17 | value: event.target.value 18 | }); 19 | }; 20 | 21 | handleBlur = () => { 22 | if (this.props.bindBlur) { 23 | this.save(this.state.value); 24 | } 25 | }; 26 | 27 | handleKeyDown = (event) => { 28 | if (event.keyCode === 13) { 29 | this.save(this.state.value); 30 | } 31 | }; 32 | 33 | save = (text) => { 34 | text = text.trim(); 35 | 36 | if (text) { 37 | this.props.onSave(text); 38 | 39 | this.setState({ 40 | value: '' 41 | }); 42 | } 43 | }; 44 | 45 | render () { 46 | return ( 47 | 55 | ) 56 | }; 57 | }; 58 | 59 | export default TextInput; 60 | -------------------------------------------------------------------------------- /test/mutable/store1.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var dispatcher = require('./dispatcher'); 4 | var Schema = require('../../src/ballade').Schema; 5 | 6 | var musicSchema = new Schema({ 7 | name: String, 8 | musician: String 9 | }); 10 | 11 | var schema = new Schema({ 12 | title: String, 13 | playlist: [musicSchema], 14 | greetings: String, 15 | users: { 16 | id: Number, 17 | name: String 18 | }, 19 | count: { 20 | $type: Number, 21 | $default: 0 22 | }, 23 | extended: { 24 | $type: Boolean, 25 | $default: false 26 | } 27 | }); 28 | 29 | var options = { 30 | cache: { 31 | users: { 32 | id: 'id', 33 | maxLength: 10 34 | } 35 | } 36 | }; 37 | 38 | var store = dispatcher.createMutableStore(schema, options, { 39 | 'mutable-test1/update-title': function (store, action) { 40 | store.set('title', action.title); 41 | }, 42 | 43 | 'mutable-test1/add-music': function (store, action) { 44 | var playlist = store.get('playlist'); 45 | playlist.push(action.music); 46 | 47 | store.set('playlist', playlist); 48 | }, 49 | 50 | 'mutable-test/say-hello': function (store, action) { 51 | store.set('greetings', action.greetings); 52 | }, 53 | 54 | 'mutable-test/add-user': function (store, action) { 55 | store.set('users', action.user); 56 | }, 57 | 58 | 'immutable-test/del-user': function (store, action) { 59 | store.delete('users', action.userId); 60 | } 61 | }); 62 | 63 | module.exports = store; 64 | -------------------------------------------------------------------------------- /test/immutable/store1.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Immutable = require('immutable'); 4 | var dispatcher = require('./dispatcher'); 5 | var Schema = require('../../src/ballade').Schema; 6 | 7 | var musicSchema = new Schema({ 8 | name: String, 9 | musician: String 10 | }); 11 | 12 | var schema = new Schema({ 13 | title: String, 14 | playlist: [musicSchema], 15 | greetings: String, 16 | users: { 17 | id: Number, 18 | name: String 19 | }, 20 | count: { 21 | $type: Number, 22 | $default: 0 23 | }, 24 | extended: { 25 | $type: Boolean, 26 | $default: false 27 | } 28 | }); 29 | 30 | var options = { 31 | cache: { 32 | users: { 33 | id: 'id', 34 | maxLength: 10 35 | } 36 | } 37 | }; 38 | 39 | var store = dispatcher.createImmutableStore(schema, options, { 40 | 'immutable-test1/update-title': function (store, action) { 41 | store.set('title', action.title); 42 | }, 43 | 44 | 'immutable-test1/add-music': function (store, action) { 45 | var playlist = store.get('playlist'); 46 | playlist = playlist.push(Immutable.Map(action.music)); 47 | store.set('playlist', playlist); 48 | }, 49 | 50 | 'immutable-test/say-hello': function (store, action) { 51 | store.set('greetings', action.greetings); 52 | }, 53 | 54 | 'immutable-test/add-user': function (store, action) { 55 | store.set('users', action.user); 56 | }, 57 | 58 | 'immutable-test/del-user': function (store, action) { 59 | store.delete('users', action.userId); 60 | } 61 | }); 62 | 63 | module.exports = store; 64 | -------------------------------------------------------------------------------- /examples/ballade-mutable-todomvc/src/js/components/app.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { bindStore } from 'ballade'; 3 | import Footer from './footer'; 4 | import Header from './header'; 5 | import MainSection from './main-section'; 6 | import todosStore from '../stores/todos'; 7 | 8 | class App extends Component { 9 | state = { 10 | todos: todosStore.get('todos'), 11 | filter: 'all' 12 | }; 13 | 14 | handleFilter = (filter) => { 15 | this.setState({ 16 | filter 17 | }); 18 | }; 19 | 20 | render () { 21 | const todos = this.state.todos; 22 | const todoSize = todos.length; 23 | let completedCount = 0; 24 | let activeCount = 0; 25 | let completed; 26 | 27 | todos.forEach((item) => { 28 | if (item.complete) { 29 | completedCount++; 30 | } 31 | }); 32 | 33 | activeCount = todoSize - completedCount; 34 | completed = todoSize > 0 && completedCount === todoSize; 35 | 36 | return ( 37 |
38 |
39 | 40 |
45 |
46 | ); 47 | }; 48 | }; 49 | 50 | App = bindStore(App, todosStore, { 51 | todos (value) { 52 | this.setState({ 53 | todos: value 54 | }); 55 | } 56 | }); 57 | 58 | export default App; 59 | -------------------------------------------------------------------------------- /examples/ballade-mutable-todomvc/src/js/components/footer.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import todoActions from '../actions/todos'; 3 | 4 | class Header extends React.Component { 5 | static propTypes = { 6 | onFilter: PropTypes.func, 7 | filter: PropTypes.string 8 | }; 9 | 10 | hanldeFilter = (event) => { 11 | const conditions = event.target.dataset.filter; 12 | 13 | this.props.onFilter(conditions); 14 | }; 15 | 16 | renderFilter = () => { 17 | return ['all', 'active', 'completed'].map((item, i) => { 18 | const className = this.props.filter === item ? 'current': ''; 19 | 20 | return ( 21 |
26 | {item} 27 | 28 | ) 29 | }); 30 | }; 31 | 32 | render () { 33 | const filterElems = this.renderFilter(); 34 | const {activeCount, completedCount} = this.props; 35 | 36 | return ( 37 | 48 | ) 49 | }; 50 | }; 51 | 52 | export default Header; 53 | -------------------------------------------------------------------------------- /examples/ballade-immutable-todomvc/src/js/components/footer.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import todoActions from '../actions/todos'; 3 | 4 | class Header extends React.Component { 5 | static propTypes = { 6 | onFilter: PropTypes.func, 7 | filter: PropTypes.string 8 | }; 9 | 10 | hanldeFilter = (event) => { 11 | const conditions = event.target.dataset.filter; 12 | 13 | this.props.onFilter(conditions); 14 | }; 15 | 16 | renderFilter = () => { 17 | return ['all', 'active', 'completed'].map((item, i) => { 18 | const className = this.props.filter === item ? 'current': ''; 19 | 20 | return ( 21 | 26 | {item} 27 | 28 | ) 29 | }); 30 | }; 31 | 32 | render () { 33 | const filterElems = this.renderFilter(); 34 | const {activeCount, completedCount} = this.props; 35 | 36 | return ( 37 | 48 | ) 49 | }; 50 | }; 51 | 52 | export default Header; 53 | -------------------------------------------------------------------------------- /src/event.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Mini Event Class 5 | */ 6 | var Event = function () { 7 | this.handlers = []; 8 | }; 9 | 10 | Event.prototype = { 11 | /** 12 | * Publish event 13 | * @param {String} event type 14 | */ 15 | publish: function (type, value) { 16 | this.handlers.forEach(function (item) { 17 | if (item.type === type) { 18 | item.handler(value); 19 | } 20 | }); 21 | }, 22 | 23 | /** 24 | * Subscribe event 25 | * @param {String} event type 26 | * @param {Function} event handler 27 | */ 28 | subscribe: function (type, handler) { 29 | this.handlers.push({ 30 | type: type, 31 | handler: handler 32 | }); 33 | }, 34 | 35 | /** 36 | * Cancel subscribe event 37 | * @param {String} event type 38 | * @param {Function} event handler 39 | */ 40 | unsubscribe: function (type, handler) { 41 | if (typeof type === 'function') { 42 | handler = type; 43 | type = null; 44 | } 45 | 46 | var i = 0; 47 | var item; 48 | var flag = false; 49 | 50 | for (; i < this.handlers.length; i++) { 51 | item = this.handlers[i]; 52 | 53 | if (type && handler) { 54 | flag = item.type === type && item.handler === handler; 55 | } 56 | else if (type) { 57 | flag = item.type === type; 58 | } 59 | else if (handler) { 60 | flag = item.handler === handler; 61 | } 62 | 63 | if (flag) { 64 | this.handlers.splice(i--, 1); 65 | } 66 | } 67 | } 68 | }; 69 | 70 | module.exports = Event; 71 | -------------------------------------------------------------------------------- /src/bindstore.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var bindStore = function (Component, store, callbacks) { 4 | var originComponentDidMount = Component.prototype.componentDidMount; 5 | var originComponentWillUnmount = Component.prototype.componentWillUnmount; 6 | var callbacksArr = Object.keys(callbacks); 7 | 8 | if (callbacksArr.length) { 9 | Component.prototype.componentDidMount = function (args) { 10 | var self = this; 11 | var newCallbacks = {}; 12 | 13 | if (!self.__storeCallback__) { 14 | self.__storeCallback__ = {}; 15 | } 16 | 17 | callbacksArr.forEach(function (item) { 18 | newCallbacks[item] = callbacks[item].bind(self); 19 | store.subscribe(item, newCallbacks[item]); 20 | }); 21 | 22 | self.__storeCallback__[store.id] = newCallbacks; 23 | 24 | if (typeof originComponentDidMount === 'function') { 25 | originComponentDidMount.apply(self, args); 26 | } 27 | }; 28 | 29 | Component.prototype.componentWillUnmount = function (args) { 30 | var self = this; 31 | 32 | callbacksArr.forEach(function (item) { 33 | store.unsubscribe(item, self.__storeCallback__[store.id][item]); 34 | }); 35 | 36 | delete self.__storeCallback__[store.id]; 37 | 38 | if (!Object.keys(self.__storeCallback__).length) { 39 | delete self.__storeCallback__; 40 | } 41 | 42 | if (typeof originComponentWillUnmount === 'function') { 43 | originComponentWillUnmount.apply(self, args); 44 | } 45 | }; 46 | } 47 | 48 | return Component; 49 | }; 50 | 51 | module.exports = bindStore; 52 | -------------------------------------------------------------------------------- /examples/ballade-immutable-todomvc/src/js/components/app.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { bindStore, immutableDeepEqual } from 'ballade/src/ballade.immutable'; 3 | import { is } from 'immutable'; 4 | import Footer from './footer'; 5 | import Header from './header'; 6 | import MainSection from './main-section'; 7 | import todosStore from '../stores/todos'; 8 | 9 | class App extends Component { 10 | state = { 11 | $todos: todosStore.get('todos'), 12 | filter: 'all' 13 | }; 14 | 15 | handleFilter = (filter) => { 16 | this.setState({ 17 | filter 18 | }); 19 | }; 20 | 21 | render () { 22 | const $todos = this.state.$todos; 23 | const todoSize = $todos.size; 24 | let completedCount = 0; 25 | let activeCount = 0; 26 | let completed; 27 | 28 | $todos.forEach((item) => { 29 | if (item.get('complete')) { 30 | completedCount++; 31 | } 32 | }); 33 | 34 | activeCount = todoSize - completedCount; 35 | completed = todoSize > 0 && completedCount === todoSize; 36 | 37 | return ( 38 |
39 |
40 | 41 |
46 |
47 | ); 48 | }; 49 | }; 50 | 51 | App = bindStore(App, todosStore, { 52 | todos (value) { 53 | this.setState({ 54 | $todos: value 55 | }); 56 | } 57 | }); 58 | 59 | export default immutableDeepEqual(App); 60 | -------------------------------------------------------------------------------- /src/persistence.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // persistence for localStorage / sessionStorage 4 | var PREFIX = 'Ballade.'; 5 | 6 | var baseTypes = { 7 | 'string': true, 8 | 'number': true, 9 | 'null': true, 10 | 'undefind': true, 11 | 'boolean': true 12 | }; 13 | 14 | var persistence = { 15 | set: function (key, value, type) { 16 | if (type !== 'localStorage' && type !== 'sessionStorage') { 17 | throw new Error('persistence params must be set localStorage or sessionStorage'); 18 | } 19 | 20 | key = PREFIX + key; 21 | var valueType = typeof value; 22 | 23 | if (baseTypes[valueType]) { 24 | value += ''; 25 | } 26 | else { 27 | value = JSON.stringify(value); 28 | } 29 | 30 | value = valueType + '@' + value; 31 | window[type].setItem(key, value); 32 | }, 33 | 34 | get: function (key, type) { 35 | if (type !== 'localStorage' && type !== 'sessionStorage') { 36 | throw new Error('persistence type must be set localStorage or sessionStorage'); 37 | } 38 | 39 | key = PREFIX + key; 40 | var value = window[type].getItem(key); 41 | 42 | if (!value) { 43 | return; 44 | } 45 | 46 | var index = value.indexOf('@'); 47 | var valueType = value.slice(0, index); 48 | 49 | value = value.slice(index + 1); 50 | 51 | if (baseTypes[valueType]) { 52 | return value; 53 | } 54 | 55 | return JSON.parse(value); 56 | }, 57 | 58 | 'delete': function (key, type) { 59 | if (type !== 'localStorage' && type !== 'sessionStorage') { 60 | throw new Error('persistence type must be set localStorage or sessionStorage'); 61 | } 62 | 63 | key = PREFIX + key; 64 | window[type].removeItem(key); 65 | } 66 | }; 67 | 68 | module.exports = persistence; 69 | -------------------------------------------------------------------------------- /examples/ballade-immutable-todomvc/gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var gutil = require('gulp-util'); 3 | var babelify = require('babelify'); 4 | var browserify = require('browserify'); 5 | var watchify = require('watchify'); 6 | var cacheify = require('cacheify'); 7 | var levelup = require('levelup'); 8 | var source = require('vinyl-source-stream'); 9 | var less = require('gulp-less'); 10 | var connect = require('gulp-connect'); 11 | var objectAssign = require('object-assign'); 12 | var db = levelup('./.cache'); 13 | 14 | var srcRoot = 'src'; 15 | var jsSrcPath = './src/js/index.js'; 16 | var jsDestPath = './src/js'; 17 | var port = 3004; 18 | 19 | var browserOpts = objectAssign({}, watchify.args, { 20 | entries: [jsSrcPath], 21 | debug: true, 22 | insertGlobals: true, 23 | detectGlobals: false 24 | }); 25 | 26 | gulp.task('connect', function () { 27 | connect.server({ 28 | root: [srcRoot], 29 | port: port, 30 | livereload: { 31 | port: port * 10 32 | }, 33 | fallback: 'src/index.html' 34 | }); 35 | }); 36 | 37 | gulp.task('watch-html', function () { 38 | gulp.watch(srcRoot + '/**/*.html', function () { 39 | return gulp.src(srcRoot + '/**/*.html') 40 | .pipe(connect.reload()); 41 | }); 42 | }); 43 | 44 | var bundle = function () { 45 | return watcher.bundle() 46 | .on('error', function (err) { 47 | console.log(err.message); 48 | console.log(err.stack); 49 | }) 50 | .pipe(source('bundle.js')) 51 | .pipe(gulp.dest(jsDestPath)) 52 | .pipe(connect.reload()); 53 | }; 54 | 55 | var babelifyCache = cacheify(babelify.configure({ 56 | presets: ["es2015", "stage-0", "react"], 57 | plugins: ['external-helpers'] 58 | }), db); 59 | 60 | var bundler = browserify(browserOpts) 61 | .transform(babelifyCache); 62 | 63 | var watcher = watchify(bundler) 64 | .on('update', bundle) 65 | .on('log', gutil.log); 66 | 67 | gulp.task('watch-js', bundle); 68 | gulp.task('watch', ['watch-js', 'watch-html']) 69 | gulp.task('default', ['connect', 'watch']); 70 | -------------------------------------------------------------------------------- /examples/ballade-mutable-todomvc/gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var gutil = require('gulp-util'); 3 | var babelify = require('babelify'); 4 | var browserify = require('browserify'); 5 | var watchify = require('watchify'); 6 | var cacheify = require('cacheify'); 7 | var levelup = require('levelup'); 8 | var source = require('vinyl-source-stream'); 9 | var less = require('gulp-less'); 10 | var connect = require('gulp-connect'); 11 | var objectAssign = require('object-assign'); 12 | var db = levelup('./.cache'); 13 | 14 | var srcRoot = 'src'; 15 | var jsSrcPath = './src/js/index.js'; 16 | var jsDestPath = './src/js'; 17 | var port = 3003; 18 | 19 | var browserOpts = objectAssign({}, watchify.args, { 20 | entries: [jsSrcPath], 21 | debug: true, 22 | insertGlobals: true, 23 | detectGlobals: false 24 | }); 25 | 26 | gulp.task('connect', function () { 27 | connect.server({ 28 | root: [srcRoot], 29 | port: port, 30 | livereload: { 31 | port: port * 10 32 | }, 33 | fallback: 'src/index.html' 34 | }); 35 | }); 36 | 37 | gulp.task('watch-html', function () { 38 | gulp.watch(srcRoot + '/**/*.html', function () { 39 | return gulp.src(srcRoot + '/**/*.html') 40 | .pipe(connect.reload()); 41 | }); 42 | }); 43 | 44 | var bundle = function () { 45 | return watcher.bundle() 46 | .on('error', function (err) { 47 | console.log(err.message); 48 | console.log(err.stack); 49 | }) 50 | .pipe(source('bundle.js')) 51 | .pipe(gulp.dest(jsDestPath)) 52 | .pipe(connect.reload()); 53 | }; 54 | 55 | var babelifyCache = cacheify(babelify.configure({ 56 | presets: ["es2015", "stage-0", "react"], 57 | plugins: ['external-helpers'] 58 | }), db); 59 | 60 | var bundler = browserify(browserOpts) 61 | .transform(babelifyCache); 62 | 63 | var watcher = watchify(bundler) 64 | .on('update', bundle) 65 | .on('log', gutil.log); 66 | 67 | gulp.task('watch-js', bundle); 68 | gulp.task('watch', ['watch-js', 'watch-html']) 69 | gulp.task('default', ['connect', 'watch']); 70 | -------------------------------------------------------------------------------- /examples/ballade-mutable-todomvc/src/js/components/todo-item.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | import todoActions from '../actions/todos'; 3 | import TextInput from './text-input'; 4 | 5 | class TodoItem extends Component { 6 | state = { 7 | status: 'normal' 8 | }; 9 | 10 | static propTypes = { 11 | id: PropTypes.string, 12 | complete: PropTypes.bool, 13 | text: PropTypes.string 14 | }; 15 | 16 | handleUpdate = (text) => { 17 | this.setState({ 18 | status: 'normal' 19 | }, () => { 20 | todoActions.update(this.props.id, text); 21 | }); 22 | }; 23 | 24 | handleToggle = () => { 25 | todoActions.toggle(this.props.id); 26 | }; 27 | 28 | handleDelete = () => { 29 | todoActions.delete(this.props.id); 30 | }; 31 | 32 | handleDoubleClick = () => { 33 | this.setState({ 34 | status: 'editing' 35 | }); 36 | }; 37 | 38 | render () { 39 | const { id, text, complete } = this.props; 40 | const status = this.state.status; 41 | let toggleClassName = 'icon-font toggle'; 42 | let textClassName = 'text'; 43 | let itemElem; 44 | 45 | if (complete) { 46 | toggleClassName += ' completed'; 47 | textClassName += ' completed'; 48 | } 49 | 50 | if (status === 'editing') { 51 | itemElem = ( 52 |
53 | 57 |
58 | ) 59 | } 60 | else { 61 | itemElem = ( 62 |
63 |
{text}
64 | 65 |
66 | ) 67 | } 68 | 69 | return ( 70 |
  • 71 | 72 | {itemElem} 73 |
  • 74 | ) 75 | }; 76 | } 77 | 78 | export default TodoItem; 79 | -------------------------------------------------------------------------------- /examples/ballade-immutable-todomvc/src/js/components/todo-item.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | import todoActions from '../actions/todos'; 3 | import TextInput from './text-input'; 4 | 5 | class TodoItem extends Component { 6 | state = { 7 | status: 'normal' 8 | }; 9 | 10 | static propTypes = { 11 | id: PropTypes.string, 12 | complete: PropTypes.bool, 13 | text: PropTypes.string 14 | }; 15 | 16 | handleUpdate = (text) => { 17 | this.setState({ 18 | status: 'normal' 19 | }, () => { 20 | todoActions.update(this.props.id, text); 21 | }); 22 | }; 23 | 24 | handleToggle = () => { 25 | todoActions.toggle(this.props.id); 26 | }; 27 | 28 | handleDelete = () => { 29 | todoActions.delete(this.props.id); 30 | }; 31 | 32 | handleDoubleClick = () => { 33 | this.setState({ 34 | status: 'editing' 35 | }); 36 | }; 37 | 38 | render () { 39 | const { id, text, complete } = this.props; 40 | const status = this.state.status; 41 | let toggleClassName = 'icon-font toggle'; 42 | let textClassName = 'text'; 43 | let itemElem; 44 | 45 | if (complete) { 46 | toggleClassName += ' completed'; 47 | textClassName += ' completed'; 48 | } 49 | 50 | if (status === 'editing') { 51 | itemElem = ( 52 |
    53 | 57 |
    58 | ) 59 | } 60 | else { 61 | itemElem = ( 62 |
    63 |
    {text}
    64 |
    65 |
    66 | ) 67 | } 68 | 69 | return ( 70 |
  • 71 | 72 | {itemElem} 73 |
  • 74 | ) 75 | }; 76 | }; 77 | 78 | export default TodoItem; 79 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var path = require('path'); 4 | var gulp = require('gulp'); 5 | var uglify = require('gulp-uglify'); 6 | var gutil = require('gulp-util'); 7 | var browserify = require('browserify'); 8 | var source = require('vinyl-source-stream'); 9 | var buffer = require('vinyl-buffer'); 10 | 11 | var eslint = require('gulp-eslint'); 12 | var eslintrcPath = path.resolve(__dirname, './eslintrc.json'); 13 | 14 | var uglifyConfig = { 15 | compress: { 16 | drop_console: true 17 | } 18 | }; 19 | 20 | var balladeBrowserifyConfig = { 21 | entries: 'src/ballade.js', 22 | insertGlobals: true, 23 | detectGlobals: false, 24 | standalone: 'Ballade' 25 | }; 26 | 27 | var balladeImmutableBrowserifyConfig = { 28 | entries: 'src/ballade.immutable.js', 29 | insertGlobals: true, 30 | detectGlobals: false, 31 | standalone: 'Ballade' 32 | }; 33 | 34 | gulp.task('build-ballade', function () { 35 | return browserify(balladeBrowserifyConfig) 36 | .bundle() 37 | .pipe(source('ballade.js')) 38 | .pipe(buffer()) 39 | .pipe(gulp.dest('dist/')); 40 | }); 41 | 42 | gulp.task('build-ballade-min', function () { 43 | return browserify(balladeBrowserifyConfig) 44 | .bundle() 45 | .pipe(source('ballade.min.js')) 46 | .pipe(buffer()) 47 | .pipe(uglify(uglifyConfig)) 48 | .pipe(gulp.dest('dist/')); 49 | }); 50 | 51 | gulp.task('build-ballade-immutable', function () { 52 | return browserify(balladeImmutableBrowserifyConfig) 53 | .ignore('immutable') 54 | .bundle() 55 | .pipe(source('ballade.immutable.js')) 56 | .pipe(buffer()) 57 | .pipe(gulp.dest('dist/')); 58 | }); 59 | 60 | gulp.task('build-ballade-immutable-min', function () { 61 | return browserify(balladeImmutableBrowserifyConfig) 62 | .ignore('immutable') 63 | .bundle() 64 | .pipe(source('ballade.immutable.min.js')) 65 | .pipe(buffer()) 66 | .pipe(uglify(uglifyConfig)) 67 | .pipe(gulp.dest('dist/')); 68 | }); 69 | 70 | gulp.task('default', [ 71 | 'build-ballade', 72 | 'build-ballade-min', 73 | 'build-ballade-immutable', 74 | 'build-ballade-immutable-min' 75 | ]); 76 | 77 | // js lint 78 | gulp.task('lint', function () { 79 | return gulp.src(['./src/**/*.js']) 80 | .pipe(eslint(eslintrcPath)) 81 | .pipe(eslint.format()) 82 | .pipe(eslint.failAfterError()); 83 | }); 84 | -------------------------------------------------------------------------------- /src/copy.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Performs a deep clone of `subject`, returning a duplicate which can be 3 | * modified freely without affecting `subject`. 4 | * 5 | * The `originals` and `duplicates` variables allow us to copy references as 6 | * well, and also means we don't have to serialise any object more than once. 7 | * https://github.com/evlun/copy 8 | */ 9 | function copy (subject, originals, duplicates) { 10 | if (!(subject instanceof Object)) { 11 | return subject; 12 | } 13 | 14 | var type = Object.prototype.toString.call(subject).slice(8, -1); 15 | var duplicate; 16 | 17 | // create the base for our duplicate 18 | switch (type) { 19 | case 'Array': 20 | duplicate = []; 21 | break; 22 | 23 | case 'Date': 24 | duplicate = new Date(subject.getTime()); 25 | break; 26 | 27 | case 'RegExp': 28 | duplicate = new RegExp(subject); 29 | break; 30 | 31 | case 'Function': 32 | break; 33 | 34 | case 'Uint8Array': 35 | case 'Uint8ClampedArray': 36 | case 'Uint16Array': 37 | case 'Uint32Array': 38 | case 'Int8Array': 39 | case 'Int16Array': 40 | case 'Int32Array': 41 | case 'Float32Array': 42 | case 'Float64Array': 43 | duplicate = subject.subarray(); 44 | break; 45 | 46 | default: 47 | duplicate = {}; 48 | } 49 | 50 | originals.push(subject); 51 | duplicates.push(duplicate); 52 | 53 | // special case for arrays 54 | if (subject instanceof Array) { 55 | for (var i = 0; i < subject.length; i++) { 56 | duplicate[i] = copy(subject[i], originals, duplicates); 57 | } 58 | } 59 | 60 | var keys = Object.keys(subject).sort(); 61 | var skip = Object.keys(duplicate).sort(); 62 | 63 | for (var j = 0; j < keys.length; j++) { 64 | var key = keys[j]; 65 | 66 | // ignore keys in `skip` 67 | if (skip.length > 0 && key === skip[0]) { 68 | skip.shift(); 69 | continue; 70 | } 71 | 72 | if (Object.prototype.hasOwnProperty.call(subject, key)) { 73 | var value = subject[key]; 74 | var index = originals.indexOf(value); 75 | 76 | duplicate[key] = index !== -1 ? duplicates[index] : copy(subject[key], originals, duplicates); 77 | } 78 | } 79 | 80 | return duplicate; 81 | }; 82 | 83 | /* 84 | * Wrapper for `copy()`. 85 | */ 86 | module.exports = function (subject) { 87 | return copy(subject, [], []); 88 | }; 89 | -------------------------------------------------------------------------------- /cache_CN.md: -------------------------------------------------------------------------------- 1 | # Store 中的缓存设计 2 | 3 | ## 缓存的使用场景 4 | 5 | 对于请求频次高的数据存储场景来说,前端适当的缓存数据,可以有效的降低对服务端的请求压力。比如在单页应用中对某个大列表进行分页加载时,大部分时候已加载过的数据不应该再发起第二次请求,这是前端缓存的便利性。 6 | 7 | 但是单页应用中如果对数据进行无限制的缓存,会导致浏览器的内存暴涨,最终耗光系统的资源。此时有限制性的缓存很有必要。通过使用 Ballade 提供的缓存模块,可以确保缓存到 store 中的数据不会无限的增长。 8 | 9 | 先定义 Schema。 10 | 11 | ``` 12 | var schema1 = new Schema({ 13 | photo: { 14 | id: String, 15 | title: String, 16 | width: Number, 17 | height: Number 18 | } 19 | }); 20 | 21 | var schema2 = new Schema({ 22 | avatar: String, 23 | photos: { 24 | key: String, 25 | list: [schema1], 26 | total: Number 27 | } 28 | }); 29 | ``` 30 | 31 | 对 `photos` 的数据进行限制长度的缓存,需要在创建 Store 时设置 `cache` 的配置。 32 | 33 | ``` 34 | var options = { 35 | cache: { 36 | photos: { 37 | id: 'key' // 设置 key 作为缓存的唯一 id 38 | } 39 | } 40 | }; 41 | ``` 42 | 43 | 在限制长度的缓存中,需要指定一个 `id` 字段,该字段的值必须确保是唯一的,如上面 `photos` 的数据就使用 `key` 这个字段来作为缓存的唯一 `id` 字段名。 44 | 45 | 创建 Store 时传上该配置即可。 46 | 47 | ``` 48 | var store = dispatcher.createImmutableStore(schema2, options, { 49 | 'fetch-photos': (store, action) => { 50 | const photos = action.response.data; 51 | photos.key = action.key; 52 | // 存入 Store,并且具备缓存数据的能力 53 | store.set('photos', photos); 54 | } 55 | }); 56 | ``` 57 | 58 | 在 UI View 中获取 Store 中的数据还需带上对应的数据 id。 59 | 60 | ``` 61 | const id = '001'; 62 | store.get('photos', id); => 直接输出 001 对应的数据 63 | ``` 64 | 65 | 通过 `maxLength` 可以限定缓存数据的最大长度,如果超出最大长度,则会采用先进先出的策略,先清除最先缓存的数据,然后才缓存新的数据。 66 | 67 | ``` 68 | var options = { 69 | cache: { 70 | photos: { 71 | id: 'key' // 设置 key 作为缓存的唯一 id 72 | } 73 | } 74 | }; 75 | ``` 76 | 77 | ## 持久化的 Store 78 | 79 | 很多时候,单页应用期望能将一些数据持久化到本地,通过集成 `localStorage` 和 `sessionStorage` 就可以进行持久化的缓存。 80 | 81 | Ballade 的缓存模块可以将数据持久化到本地,无需直接操作 `localStorage` 和 `sessionStorage`,只要在数据存储时指定 `persistence` 选项即可。 82 | 83 | 对于上面的例子来说,想对 `avatar` 的数据做本地缓存,那么缓存配置应该是这样的: 84 | 85 | ``` 86 | const options = { 87 | cache: { 88 | avatar: { 89 | persistence: { 90 | type: 'localStorage', 91 | prefix: 'user' 92 | } 93 | } 94 | } 95 | }; 96 | ``` 97 | 98 | 其中 `persistence.type` 指定的持久化的类型,`persistence.prefix` 指定的持久化缓存的前缀,避免数据存储时的存储 key 的冲突。 99 | 100 | 访问缓存的数据与之前获取 Store 中的用法没有差别,应用在启动的时候会自动从本地缓存中获取持久化的数据。 101 | 102 | ``` 103 | store.get('avatar'); => 输出 avatar 的持久化缓存数据 104 | ``` 105 | 106 | **注意:** 使用 Ballade 向 `localStorage` 存储数据时非常便捷,但也需要考虑应该什么时候清除该数据,以避免持久化缓存的空间无限膨胀。 107 | 108 | 当然,持久化的缓存可以和普通的缓存结合使用。 109 | -------------------------------------------------------------------------------- /src/cache.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // simple cache module 4 | // @TODO add expires options 5 | 6 | var accessor = require('./accessor'); 7 | var proxyGet = accessor.get; 8 | 9 | var MAX_LENGTH = 20; 10 | 11 | var Cache = function (options) { 12 | if (!options.id) { 13 | throw new Error('Cache must set a ' + options.id); 14 | } 15 | 16 | this.id = options.id; 17 | this.maxLength = options.maxLength || MAX_LENGTH; 18 | this.cacheStore = []; 19 | this.idKeys = {}; 20 | }; 21 | 22 | Cache.prototype = { 23 | set: function (value, isImmutable) { 24 | var idKey = this.id; 25 | var cacheStore = this.cacheStore; 26 | var length = cacheStore.length; 27 | var idValue = proxyGet(value, idKey, isImmutable); 28 | 29 | if (idValue === undefined) { 30 | return; 31 | } 32 | 33 | // update cache 34 | if (this.idKeys[idValue]) { 35 | cacheStore.some(function (item, i) { 36 | if (proxyGet(item, idKey, isImmutable) === idValue) { 37 | cacheStore[i] = value; 38 | return true; 39 | } 40 | }); 41 | } 42 | // push cache 43 | else { 44 | this.idKeys[idValue] = true; 45 | 46 | // limit length 47 | if (length === this.maxLength) { 48 | idValue = proxyGet(cacheStore[0], idKey, isImmutable); 49 | delete this.idKeys[idValue]; 50 | cacheStore.shift(); 51 | } 52 | 53 | cacheStore.push(value); 54 | } 55 | }, 56 | 57 | get: function (id, isImmutable) { 58 | var cacheStore = this.cacheStore; 59 | var i = cacheStore.length - 1; 60 | var idKey = this.id; 61 | var idValue = id; 62 | var item; 63 | 64 | if (this.idKeys[idValue]) { 65 | for (; i > -1; i--) { 66 | item = cacheStore[i]; 67 | if (proxyGet(item, idKey, isImmutable) === idValue) { 68 | return item; 69 | } 70 | } 71 | } 72 | }, 73 | 74 | 'delete': function (id, isImmutable) { 75 | var cacheStore = this.cacheStore; 76 | var i = cacheStore.length - 1; 77 | var idKey = this.id; 78 | var idValue = id; 79 | var item; 80 | 81 | if (this.idKeys[idValue]) { 82 | for (; i > -1; i--) { 83 | item = cacheStore[i]; 84 | if (proxyGet(item, idKey, isImmutable) === idValue) { 85 | cacheStore.splice(i, 1); 86 | delete this.idKeys[idValue]; 87 | break; 88 | } 89 | } 90 | } 91 | } 92 | }; 93 | 94 | module.exports = Cache; 95 | -------------------------------------------------------------------------------- /examples/ballade-mutable-todomvc/src/js/stores/todos.js: -------------------------------------------------------------------------------- 1 | const Schema = require('ballade').Schema; 2 | import dispatcher from '../dispatcher/dispatcher'; 3 | import constatns from '../constants/todos'; 4 | const TODOS = 'todos'; 5 | 6 | const todoSchema = new Schema({ 7 | id: { 8 | $type: String, 9 | $default: (+new Date() + Math.floor(Math.random() * 999999)).toString(36) 10 | }, 11 | complete: { 12 | $type: Boolean, 13 | $default: false 14 | }, 15 | text: { 16 | $type: String, 17 | $default: "Ballade Getting Started" 18 | } 19 | }); 20 | 21 | const todosSchema = new Schema({ 22 | todos: [todoSchema] 23 | }); 24 | 25 | const todosStore = dispatcher.createMutableStore(todosSchema, { 26 | [`${TODOS}/${constatns.CREATE}`]: (store, action) => { 27 | const todos = store.get('todos'); 28 | 29 | todos.unshift({ 30 | id: (+new Date() + Math.floor(Math.random() * 999999)).toString(36), 31 | complete: false, 32 | text: action.text 33 | }); 34 | 35 | store.set('todos', todos); 36 | }, 37 | 38 | [`${TODOS}/${constatns.UPDATE}`]: (store, action) => { 39 | const todos = store.get('todos'); 40 | 41 | todos.some((item) => { 42 | if (item.id === action.id) { 43 | item.text = action.text; 44 | return true; 45 | } 46 | }); 47 | 48 | store.set('todos', todos); 49 | }, 50 | 51 | [`${TODOS}/${constatns.DELETE}`]: (store, action) => { 52 | const todos = store.get('todos'); 53 | let index; 54 | 55 | todos.some((item, i) => { 56 | if (item.id === action.id) { 57 | index = i; 58 | return true; 59 | } 60 | }); 61 | 62 | if (index !== undefined) { 63 | todos.splice(index, 1); 64 | } 65 | 66 | store.set('todos', todos); 67 | }, 68 | 69 | [`${TODOS}/${constatns.DELETE_COMPLETE}`]: (store, action) => { 70 | let todos = store.get('todos'); 71 | 72 | todos = todos.filter((item) => ( 73 | !item.complete 74 | )); 75 | 76 | store.set('todos', todos); 77 | }, 78 | 79 | [`${TODOS}/${constatns.TOGGLE}`]: (store, action) => { 80 | const todos = store.get('todos'); 81 | 82 | todos.some((item, i) => { 83 | if (item.id === action.id) { 84 | item.complete = !item.complete; 85 | return true; 86 | } 87 | }); 88 | 89 | store.set('todos', todos); 90 | }, 91 | 92 | [`${TODOS}/${constatns.TOGGLE_ALL}`]: (store, action) => { 93 | const todos = store.get('todos'); 94 | const active = todos.some((item) => !item.complete); 95 | 96 | todos.forEach((item) => { 97 | item.complete = active; 98 | }); 99 | 100 | store.set('todos', todos); 101 | } 102 | }); 103 | 104 | export default todosStore; 105 | -------------------------------------------------------------------------------- /examples/ballade-immutable-todomvc/src/js/stores/todos.js: -------------------------------------------------------------------------------- 1 | const Schema = require('ballade').Schema; 2 | import {Map} from 'immutable'; 3 | import dispatcher from '../dispatcher/dispatcher'; 4 | import constatns from '../constants/todos'; 5 | const TODOS = 'todos'; 6 | 7 | const todoSchema = new Schema({ 8 | id: { 9 | $type: String, 10 | $default: (+new Date() + Math.floor(Math.random() * 999999)).toString(36) 11 | }, 12 | complete: { 13 | $type: Boolean, 14 | $default: false 15 | }, 16 | text: { 17 | $type: String, 18 | $default: "Ballade Getting Started" 19 | } 20 | }); 21 | 22 | const todosSchema = new Schema({ 23 | todos: [todoSchema] 24 | }); 25 | 26 | // Filter specific index from todos by id 27 | const getTodoId = ($todos, id) => { 28 | let index; 29 | 30 | $todos.some((item, i) => { 31 | if (item.get('id') === id) { 32 | index = i; 33 | return true; 34 | } 35 | }); 36 | 37 | return index; 38 | }; 39 | 40 | const todosStore = dispatcher.createImmutableStore(todosSchema, { 41 | [`${TODOS}/${constatns.CREATE}`]: (store, action) => { 42 | let $todos = store.get('todos'); 43 | 44 | $todos = $todos.unshift(Map({ 45 | id: (+new Date() + Math.floor(Math.random() * 999999)).toString(36), 46 | complete: false, 47 | text: action.text 48 | })); 49 | 50 | store.set('todos', $todos); 51 | }, 52 | 53 | [`${TODOS}/${constatns.UPDATE}`]: (store, action) => { 54 | let $todos = store.get('todos'); 55 | let index = getTodoId($todos, action.id); 56 | 57 | if (index !== undefined) { 58 | $todos = $todos.setIn([index, 'text'], action.text); 59 | } 60 | 61 | store.set('todos', $todos); 62 | }, 63 | 64 | [`${TODOS}/${constatns.DELETE}`]: (store, action) => { 65 | let $todos = store.get('todos'); 66 | let index = getTodoId($todos, action.id); 67 | 68 | if (index !== undefined) { 69 | $todos = $todos.splice(index, 1); 70 | } 71 | 72 | store.set('todos', $todos); 73 | }, 74 | 75 | [`${TODOS}/${constatns.DELETE_COMPLETE}`]: (store, action) => { 76 | let $todos = store.get('todos'); 77 | 78 | $todos = $todos.filter((item) => ( 79 | !item.get('complete') 80 | )); 81 | 82 | store.set('todos', $todos); 83 | }, 84 | 85 | [`${TODOS}/${constatns.TOGGLE}`]: (store, action) => { 86 | let $todos = store.get('todos'); 87 | let index = getTodoId($todos, action.id); 88 | let complete; 89 | 90 | if (index !== undefined) { 91 | complete = $todos.getIn([index, 'complete']); 92 | $todos = $todos.setIn([index, 'complete'], !complete); 93 | } 94 | 95 | store.set('todos', $todos); 96 | }, 97 | 98 | [`${TODOS}/${constatns.TOGGLE_ALL}`]: (store, action) => { 99 | let $todos = store.get('todos'); 100 | const active = $todos.some((item) => !item.get('complete')); 101 | 102 | $todos = $todos.update((list) => ( 103 | list.map((item) => ( 104 | item.set('complete', active) 105 | )) 106 | )); 107 | 108 | store.set('todos', $todos); 109 | } 110 | }); 111 | 112 | export default todosStore; 113 | -------------------------------------------------------------------------------- /cache.md: -------------------------------------------------------------------------------- 1 | # Cache in Store 2 | 3 | ## Cache usage scenarios 4 | 5 | For the high frequency data storage scenario, the cache data can effectively reduce the server request pressure. For example, in a single page web application for a large list of page loading, most of the time the data has been loaded should not be launched second requests, this is the convenience of the front-end cache. 6 | 7 | But in single page web application, if the cache data is not limit, will cause browser memory resource consumption. Ballade cache module make sure the browser memory is controllable and limited. 8 | 9 | Definition schema. 10 | 11 | ``` 12 | var schema1 = new Schema({ 13 | photo: { 14 | id: String, 15 | title: String, 16 | width: Number, 17 | height: Number 18 | } 19 | }); 20 | 21 | var schema2 = new Schema({ 22 | avatar: String, 23 | photos: { 24 | key: String, 25 | list: [schema1], 26 | total: Number 27 | } 28 | }); 29 | ``` 30 | 31 | Limited data cache length for `photos`, when create Store need configure `cache` options. 32 | 33 | ``` 34 | var options = { 35 | cache: { 36 | photos: { 37 | id: 'key', // 'key' is unique id 38 | maxLength: 10 // Limit cache data max length 39 | } 40 | } 41 | }; 42 | ``` 43 | 44 | In limited data cache length case, need specify a `id` field, and make sure the `id` is unique. Above example, the `photos` used `key` for cache unique `id` field. 45 | 46 | When create Store just specify the `options`. 47 | 48 | ```js 49 | var store = dispatcher.createImmutableStore(schema2, options, { 50 | 'fetch-photos': (store, action) => { 51 | const photos = action.response.data; 52 | photos.key = action.key; 53 | // Stored data in Store, and the ability to cache data 54 | store.set('photos', photos); 55 | } 56 | }); 57 | ``` 58 | 59 | Get data from Store in UI Views required data id. 60 | 61 | ``` 62 | const id = '001'; 63 | store.get('photos', id); => Will output id is 001 data 64 | ``` 65 | 66 | 通过 `maxLength` 可以限定缓存数据的最大长度,如果超出最大长度,则会采用先进先出的策略,先清除最先缓存的数据,然后才缓存新的数据。 67 | 68 | `maxLength` can limit cache data max length, if length is overflow, then remove the first cache data and stored new cache data. 69 | 70 | ## Persistent Store 71 | 72 | In most cases, signle page web application expect persistent data in local. Persistent cache just used `localStorage` or `sessionStorage`. 73 | 74 | Ballade cache module can persistent data in local, don't operation `localStorage ` or `sessionStorage`, only specify `persistence` option. 75 | 76 | For above example, will cache `avatar` data in local, just configure below. 77 | 78 | ``` 79 | var options = { 80 | cache: { 81 | avatar: { 82 | persistence: { 83 | type: 'localStorage', 84 | prefix: 'user' 85 | } 86 | } 87 | } 88 | }; 89 | ``` 90 | 91 | `persistence.type` is persistence cache type, `persistence.prefix` is persistence cache prefix, make sure the key is unique. 92 | 93 | Get data from cache same as common methods of use. When application is startup, the data will get data from localStorage. 94 | 95 | ``` 96 | store.get('avatar'); => Output avatar persistence cache 97 | ``` 98 | 99 | > **Notice:** When used Ballade storage data to `localStorage`, think about what time to clear the data, avoid persistence cache is limited. 100 | 101 | Of cause, persistence cache and common cache can simultaneous use. 102 | -------------------------------------------------------------------------------- /update-guide_CN.md: -------------------------------------------------------------------------------- 1 | # 0.2.x 升级至 1.0 2 | 3 | **1. 移除了 `store.mutable` 和 `store.immutable` 两个命名空间** 4 | 5 | **0.2.x** 6 | 7 | 在 0.2.x 的版本中,`createMutableStore` 返回的 Store 包含了 `mutable` 的命名空间,并且提供了 `set`、`get`、`delete` 三个方法。 8 | 9 | `creatImmutableStore` 返回的 Store 包含了 `immutable` 的命名空间,`store.immutable` 实际上是 Immutable 的实例。 10 | 11 | ```javascript 12 | var exampleStore1 = dispatcher.createMutableStore(schema, callbacks); 13 | console.log(typeof exampleStore1.mutable.set === 'function'); // true 14 | console.log(typeof exampleStore1.mutable.get === 'function'); // true 15 | console.log(typeof exampleStore1.mutable.delete === 'function'); // true 16 | 17 | var exampleStore2 = dispatcher.createImmutableStore(schema, callbacks); 18 | console.log(exampleStore1.immutable instanceof Immutable); // true 19 | console.log(typeof exampleStore1.immutable.set === 'function'); // true 20 | console.log(typeof exampleStore1.immutable.setIn === 'function'); // true 21 | console.log(typeof exampleStore1.immutable.get === 'function'); // true 22 | console.log(typeof exampleStore1.immutable.getIn === 'function'); // true 23 | console.log(typeof exampleStore1.immutable.delete === 'function'); // true 24 | ``` 25 | 26 | **1.0** 27 | 28 | 在 1.0 的版本中,`createMutableStore` 和 `creatImmutableStore` 返回的 Store 都没有了 `mutable` 和 `immutable` 的命名空间,都提供了相同的 `set`、`get`、`delete` 三个方法。 29 | 30 | ```javascript 31 | var exampleStore1 = dispatcher.createMutableStore(schema, callbacks); 32 | console.log(typeof exampleStore1.set === 'function'); // true 33 | console.log(typeof exampleStore1.get === 'function'); // true 34 | console.log(typeof exampleStore1.delete === 'function'); // true 35 | 36 | var exampleStore2 = dispatcher.createImmutableStore(schema, callbacks); 37 | console.log(typeof exampleStore2.set === 'function'); // true 38 | console.log(typeof exampleStore2.get === 'function'); // true 39 | console.log(typeof exampleStore2.delete === 'function'); // true 40 | ``` 41 | 42 | 对于 `creatImmutableStore` 返回的 Store,其获取到的数据都会是 Immutable 类型的。 43 | 44 | **2. Store 的 get 和 delete 方法增加了第二个参数** 45 | 46 | 增加的参数用于获取和删除缓存的数据。 47 | 48 | **3. 移除了 `store.event` 的命名空间** 49 | 50 | 原来的 Store 处理事件的方法都在 `store.event` 的命名空间中,现在可以直接在一级命名空间进行事件处理方法的访问。 51 | 52 | **0.2.x** 53 | 54 | ```js 55 | var exampleStore1 = dispatcher.createMutableStore(schema, callbacks); 56 | console.log(typeof exampleStore1.event.publish === 'function'); // true 57 | console.log(typeof exampleStore1.event.subscribe === 'function'); // true 58 | console.log(typeof exampleStore1.event.unsubscribe === 'function'); // true 59 | ``` 60 | 61 | **1.0** 62 | 63 | ```js 64 | var exampleStore1 = dispatcher.createMutableStore(schema, callbacks); 65 | console.log(typeof exampleStore1.publish === 'function'); // true 66 | console.log(typeof exampleStore1.subscribe === 'function'); // true 67 | console.log(typeof exampleStore1.unsubscribe === 'function'); // true 68 | ``` 69 | 70 | **4. Store Callbacks 无需再有返回值** 71 | 72 | 原来的 Store Callbacks 都要返回数据变化的 key,这样 Store 才知道变化的数据的 key。现在只要有 set 操作,Store 都知道数据变化的 key。 73 | 74 | **0.2.x** 75 | 76 | ```js 77 | var exampleStore = dispatcher.createImmutableStore(schema, { 78 | 'example/update-title': function (store, action) { 79 | return store.set('title', action.title); 80 | } 81 | }); 82 | ``` 83 | 84 | **1.0** 85 | 86 | ```js 87 | var exampleStore = dispatcher.createImmutableStore(schema, { 88 | 'example/update-title': function (store, action) { 89 | store.set('title', action.title); 90 | } 91 | }); 92 | ``` 93 | 94 | **5. Schema 的优化** 95 | 96 | 1.0 的 Schema 完全重构,详见 [Schema](/schema.md)。 97 | 98 | **6. `createMutableStore` 和 `createImmutableStore` 增加了 `options`** 99 | 100 | `options.error` 回调函数可以监听数据校验失败的错误。 101 | 102 | `options.cache` 可以对数据进行缓存,详见 [Cache](/cache.md)。 103 | 104 | **7. publish 和 subscribe 方法增加了变化的数据的发送和接收** 105 | 106 | 在 1.0 的版本中,`publish(type, changedValue)` 中的 `changedValue` 可以将广播事件的时候将变化的数据发送出去,而在 `subscribe(type, handler)` 中的 `handler` 事件处理函数的第一个参数就是 `publish` 传递过来的 `changedValue`。 107 | 108 | **8. 增加了 `Ballade.bindStore` 方法** 109 | 110 | **9. 增加了 `Ballade.immutableDeepEqual` 方法** 111 | 112 | -------------------------------------------------------------------------------- /update-guide.md: -------------------------------------------------------------------------------- 1 | # 0.2.x update to 1.0 2 | 3 | **1. Remove `store.mutable` and `store.immutable` two namespace** 4 | 5 | **0.2.x** 6 | 7 | In 0.2.x version, `createMutableStore` returned Store include `mutable` namespace, and has `set` `get` `delete` three methods. 8 | 9 | `creatImmutableStore` returned Store include `immutable` namespace, `store.immutable` actually is Immutable instance. 10 | 11 | ```javascript 12 | var exampleStore1 = dispatcher.createMutableStore(schema, callbacks); 13 | console.log(typeof exampleStore1.mutable.set === 'function'); // true 14 | console.log(typeof exampleStore1.mutable.get === 'function'); // true 15 | console.log(typeof exampleStore1.mutable.delete === 'function'); // true 16 | 17 | var exampleStore2 = dispatcher.createImmutableStore(schema, callbacks); 18 | console.log(exampleStore1.immutable instanceof Immutable); // true 19 | console.log(typeof exampleStore1.immutable.set === 'function'); // true 20 | console.log(typeof exampleStore1.immutable.setIn === 'function'); // true 21 | console.log(typeof exampleStore1.immutable.get === 'function'); // true 22 | console.log(typeof exampleStore1.immutable.getIn === 'function'); // true 23 | console.log(typeof exampleStore1.immutable.delete === 'function'); // true 24 | ``` 25 | 26 | **1.0** 27 | 28 | In 1.0 version, `createMutableStore` and `creatImmutableStore` returned Store both not `mutable` and `immutable` namespace, both have `set` `get` `delete` three methods. 29 | 30 | ```javascript 31 | var exampleStore1 = dispatcher.createMutableStore(schema, callbacks); 32 | console.log(typeof exampleStore1.set === 'function'); // true 33 | console.log(typeof exampleStore1.get === 'function'); // true 34 | console.log(typeof exampleStore1.delete === 'function'); // true 35 | 36 | var exampleStore2 = dispatcher.createImmutableStore(schema, callbacks); 37 | console.log(typeof exampleStore2.set === 'function'); // true 38 | console.log(typeof exampleStore2.get === 'function'); // true 39 | console.log(typeof exampleStore2.delete === 'function'); // true 40 | ``` 41 | 42 | For `creatImmutableStore` returned Store, the geted data will immutable data. 43 | 44 | **2. Store.get and Store.delete added second argument** 45 | 46 | Added argument be used for get and delete cached data. 47 | 48 | **3. Remove `store.event` namespace** 49 | 50 | **0.2.x** 51 | 52 | ```js 53 | var exampleStore1 = dispatcher.createMutableStore(schema, callbacks); 54 | console.log(typeof exampleStore1.event.publish === 'function'); // true 55 | console.log(typeof exampleStore1.event.subscribe === 'function'); // true 56 | console.log(typeof exampleStore1.event.unsubscribe === 'function'); // true 57 | ``` 58 | 59 | **1.0** 60 | 61 | ```js 62 | var exampleStore1 = dispatcher.createMutableStore(schema, callbacks); 63 | console.log(typeof exampleStore1.publish === 'function'); // true 64 | console.log(typeof exampleStore1.subscribe === 'function'); // true 65 | console.log(typeof exampleStore1.unsubscribe === 'function'); // true 66 | ``` 67 | 68 | **4. Store Callbacks no return value** 69 | 70 | **0.2.x** 71 | 72 | ```js 73 | var exampleStore = dispatcher.createImmutableStore(schema, { 74 | 'example/update-title': function (store, action) { 75 | return store.set('title', action.title); 76 | } 77 | }); 78 | ``` 79 | 80 | **1.0** 81 | 82 | ```js 83 | var exampleStore = dispatcher.createImmutableStore(schema, { 84 | 'example/update-title': function (store, action) { 85 | store.set('title', action.title); 86 | } 87 | }); 88 | ``` 89 | 90 | **5. Schema Feature** 91 | 92 | View details about [Schema documentation](/schema.md). 93 | 94 | **6. `createMutableStore` and `createImmutableStore` added `options`** 95 | 96 | `options.error` If Schema validation data error, will trigger this callback function. 97 | 98 | `options.cache` Be used for configure cache for data item in Store, view details about [Cache documentation](/cache.md). 99 | 100 | **7. `publish` and `subscribe` added `changedValue`** 101 | 102 | `changedValue` is used for transmit changed value to event subscriber. 103 | 104 | **8. Added `Ballade.bindStore` method** 105 | 106 | **9. Added `Ballade.immutableDeepEqual` method** 107 | 108 | -------------------------------------------------------------------------------- /src/ballade.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Ballade 1.2.8 3 | * author: chenmnkken@gmail.com 4 | * date: 2017-12-07 5 | * url: https://github.com/chenmnkken/ballade 6 | */ 7 | 8 | 'use strict'; 9 | 10 | var Queue = require('./queue'); 11 | var Schema = require('./schema'); 12 | var MutableStore = require('./store'); 13 | var bindStore = require('./bindstore'); 14 | 15 | var Ballade = { 16 | version: '1.2.8', 17 | Schema: Schema, 18 | bindStore: bindStore 19 | }; 20 | 21 | /** 22 | * Dispatcher Class 23 | */ 24 | var Dispatcher = function () { 25 | this.actionTypes = {}; 26 | this.storeQueue = []; 27 | this.id = Date.now() + Math.round(Math.random() * 1000); 28 | 29 | this.middlewareQueue = new Queue(function (payload) { 30 | this.__invokeCallback__(payload); 31 | }.bind(this), true); 32 | }; 33 | 34 | Dispatcher.prototype = { 35 | /** 36 | * When middleware queue is done, then invoke Store callbacks 37 | * @param {Object} Action payload 38 | */ 39 | __invokeCallback__: function (payload) { 40 | this.storeQueue.forEach(function (item) { 41 | var callback = item.callbacks[payload.type]; 42 | 43 | if (typeof callback === 'function') { 44 | setTimeout(function () { 45 | callback(item.store, payload); 46 | }, 0); 47 | } 48 | }); 49 | }, 50 | 51 | /** 52 | * Register action middleware 53 | * Middleware use to process action payload 54 | * @param {Function} middleware function 55 | */ 56 | use: function (middleware) { 57 | if (typeof middleware === 'function') { 58 | this.middlewareQueue.enter(middleware); 59 | } 60 | }, 61 | 62 | /** 63 | * Dispatch Action 64 | * Dispatch Action to Store callback 65 | * @param {String} actionsId is make sure every action type do not duplicate 66 | * @param {Object} action 67 | */ 68 | __dispatch__: function (actionsId, action) { 69 | var payload = action(); 70 | var actionTypes = this.actionTypes; 71 | var actionType = payload.type; 72 | var lastActionsId; 73 | 74 | if (!actionType) { 75 | throw new Error('action type does not exist in \n' + JSON.stringify(payload, null, 2)); 76 | } 77 | 78 | lastActionsId = actionTypes[actionType]; 79 | 80 | if (!lastActionsId) { 81 | actionTypes[actionType] = actionsId; 82 | } 83 | else if (lastActionsId !== actionsId) { 84 | throw new Error('action type "' + actionType + '" is duplicate'); 85 | } 86 | 87 | this.middlewareQueue.execute(payload); 88 | }, 89 | 90 | /** 91 | * Create Actions 92 | * @param {String} action creators 93 | * @return {Object} Actions 94 | */ 95 | createActions: function (actionCreators) { 96 | // actionsId is make sure every action type do not duplicate 97 | var actionsId = (this.id++).toString(32); 98 | var self = this; 99 | var name; 100 | var creator; 101 | var actions = {}; 102 | 103 | for (name in actionCreators) { 104 | creator = actionCreators[name]; 105 | 106 | actions[name] = (function (creator, actionsId) { 107 | return function () { 108 | var args = arguments; 109 | 110 | self.__dispatch__(actionsId, function () { 111 | return creator.apply(null, Array.prototype.slice.call(args)); 112 | }); 113 | }; 114 | })(creator, actionsId); 115 | } 116 | 117 | return actions; 118 | }, 119 | 120 | /** 121 | * Create mutable store 122 | * @param {Object} store schema 123 | * @param {Object} store options 124 | * @param {Object} store callbacks 125 | * @return {Object} proxy store instance, it can not set data, only get data 126 | */ 127 | createMutableStore: function (schema, options, callbacks) { 128 | if (!callbacks) { 129 | callbacks = options; 130 | options = null; 131 | } 132 | 133 | var store = new MutableStore(schema, options); 134 | 135 | var proxyStore = { 136 | id: store.id, 137 | get: store.get.bind(store), 138 | publish: store.publish.bind(store), 139 | subscribe: store.subscribe.bind(store), 140 | unsubscribe: store.unsubscribe.bind(store) 141 | }; 142 | 143 | this.storeQueue.push({ 144 | store: store, 145 | callbacks: callbacks 146 | }); 147 | 148 | return proxyStore; 149 | } 150 | }; 151 | 152 | Ballade.Dispatcher = Dispatcher; 153 | 154 | module.exports = Ballade; 155 | -------------------------------------------------------------------------------- /examples/ballade-mutable-todomvc/src/css/app.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | body { 8 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 9 | line-height: 1.4em; 10 | background: #f0f0f0; 11 | color: #333; 12 | width: 500px; 13 | margin: 0 auto; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-font-smoothing: antialiased; 16 | -ms-font-smoothing: antialiased; 17 | -o-font-smoothing: antialiased; 18 | font-smoothing: antialiased; 19 | } 20 | 21 | button, 22 | input { 23 | outline: none; 24 | } 25 | 26 | @font-face { 27 | font-family: 'MaterialIcon'; 28 | src: url('/fonts/iconfont.eot?ka72u5'); 29 | src: url('/fonts/iconfont.eot?ka72u5#iefix') format('embedded-opentype'), 30 | url('/fonts/iconfont.ttf?ka72u5') format('truetype'), 31 | url('/fonts/iconfont.woff?ka72u5') format('woff'), 32 | url('/fonts/iconfont.svg?ka72u5#iconfont') format('svg'); 33 | font-weight: normal; 34 | font-style: normal; 35 | } 36 | 37 | .icon-font { 38 | /* use !important to prevent issues with browser extensions that change fonts */ 39 | font-family: 'MaterialIcon' !important; 40 | speak: none; 41 | font-style: normal; 42 | font-weight: normal; 43 | font-variant: normal; 44 | text-transform: none; 45 | line-height: 1; 46 | 47 | /* Better Font Rendering =========== */ 48 | -webkit-font-smoothing: antialiased; 49 | -moz-osx-font-smoothing: grayscale; 50 | } 51 | 52 | .text-input { 53 | width: 430px; 54 | padding: 12px 10px; 55 | height: 20px; 56 | line-height: 20px; 57 | font: 16px 'Helvetica Neue', Helvetica, Arial, sans-serif; 58 | border: 0 none; 59 | background: #f0f0f0; 60 | } 61 | 62 | .text-input:focus { 63 | background: #fff; 64 | } 65 | 66 | .todo-header h1 { 67 | font-size: 36px; 68 | font-weight: 200; 69 | margin: 0; 70 | height: 80px; 71 | line-height: 80px; 72 | text-align: center; 73 | } 74 | 75 | .todo-header > div { 76 | width: 100%; 77 | overflow: hidden; 78 | border: solid #e0e0e0; 79 | border-width: 1px 0 1px 0; 80 | } 81 | 82 | .todo-header .toggle-all { 83 | width: 49px; 84 | border-right: 1px solid #e0e0e0; 85 | height: 44px; 86 | line-height: 44px; 87 | font-size: 22px; 88 | text-align: center; 89 | float: left; 90 | color: #999; 91 | cursor: pointer; 92 | } 93 | 94 | .todo-header .toggle-all:before { 95 | content: "\e877"; 96 | } 97 | 98 | .todo-header .toggle-all.completed { 99 | color: #0D9E69; 100 | } 101 | 102 | .todo-header .text-input { 103 | float: left; 104 | } 105 | 106 | .todo-list { 107 | list-style: none; 108 | margin: 0; 109 | padding: 0; 110 | } 111 | 112 | .todo-list li { 113 | position: relative; 114 | border-bottom: 1px solid #e0e0e0; 115 | height: 44px; 116 | line-height: 44px; 117 | overflow: hidden; 118 | } 119 | 120 | .todo-list .toggle { 121 | width: 49px; 122 | border-right: 1px solid #e0e0e0; 123 | height: 44px; 124 | line-height: 44px; 125 | font-size: 22px; 126 | text-align: center; 127 | float: left; 128 | color: #aaa; 129 | cursor: pointer; 130 | } 131 | 132 | .todo-list .toggle.completed { 133 | color: #0D9E69; 134 | } 135 | 136 | .todo-list .toggle:before { 137 | content: "\e876"; 138 | } 139 | 140 | .todo-list .item { 141 | float: left; 142 | width: 450px; 143 | } 144 | 145 | .todo-list .text { 146 | padding: 0 10px; 147 | font-size: 16px; 148 | } 149 | 150 | .todo-list .text.completed { 151 | text-decoration: line-through; 152 | color: #999; 153 | } 154 | 155 | .todo-list .delete { 156 | width: 40px; 157 | height: 44px; 158 | line-height: 44px; 159 | font-size: 22px; 160 | color: #999; 161 | position: absolute; 162 | top: 0; 163 | right: 0; 164 | text-align: center; 165 | cursor: pointer; 166 | display: none; 167 | } 168 | 169 | .todo-list .delete:before { 170 | content: "\e5cd"; 171 | } 172 | 173 | .todo-list .delete:hover { 174 | color: #000; 175 | } 176 | 177 | .todo-list li:hover .delete { 178 | display: block; 179 | } 180 | 181 | .todo-footer { 182 | height: 40px; 183 | line-height: 40px; 184 | position: relative; 185 | margin: 10px 0 30px; 186 | } 187 | 188 | .todo-footer .count{ 189 | float: left; 190 | margin: 0; 191 | color: #666; 192 | } 193 | 194 | .todo-footer .filter { 195 | position: absolute; 196 | top: 7px; 197 | left: 50%; 198 | height: 27px; 199 | overflow: hidden; 200 | margin-left: -100px; 201 | } 202 | 203 | .todo-footer .filter a { 204 | float: left; 205 | padding: 0 6px; 206 | margin-right: 5px; 207 | border: 1px solid #e0e0e0; 208 | background: #fff; 209 | border-radius: 3px; 210 | text-align: center; 211 | cursor: pointer; 212 | height: 25px; 213 | line-height: 25px; 214 | text-transform: capitalize; 215 | color: #666; 216 | } 217 | 218 | .todo-footer .filter .current { 219 | border-color: #0D9E69; 220 | background: #f0f0f0; 221 | color: #0D9E69; 222 | } 223 | 224 | .todo-footer .clear-btn { 225 | float: right; 226 | padding: 0 6px; 227 | border: 1px solid #e0e0e0; 228 | background: #fff; 229 | border-radius: 3px; 230 | text-align: center; 231 | cursor: pointer; 232 | height: 25px; 233 | line-height: 25px; 234 | margin-top: 7px; 235 | text-transform: capitalize; 236 | color: #666; 237 | } 238 | 239 | .todo-footer .clear-btn:hover { 240 | color: #000; 241 | } 242 | 243 | .info { 244 | text-align: center; 245 | color: #666; 246 | } 247 | -------------------------------------------------------------------------------- /examples/ballade-immutable-todomvc/src/css/app.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | body { 8 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 9 | line-height: 1.4em; 10 | background: #f0f0f0; 11 | color: #333; 12 | width: 500px; 13 | margin: 0 auto; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-font-smoothing: antialiased; 16 | -ms-font-smoothing: antialiased; 17 | -o-font-smoothing: antialiased; 18 | font-smoothing: antialiased; 19 | } 20 | 21 | button, 22 | input { 23 | outline: none; 24 | } 25 | 26 | @font-face { 27 | font-family: 'MaterialIcon'; 28 | src: url('/fonts/iconfont.eot?ka72u5'); 29 | src: url('/fonts/iconfont.eot?ka72u5#iefix') format('embedded-opentype'), 30 | url('/fonts/iconfont.ttf?ka72u5') format('truetype'), 31 | url('/fonts/iconfont.woff?ka72u5') format('woff'), 32 | url('/fonts/iconfont.svg?ka72u5#iconfont') format('svg'); 33 | font-weight: normal; 34 | font-style: normal; 35 | } 36 | 37 | .icon-font { 38 | /* use !important to prevent issues with browser extensions that change fonts */ 39 | font-family: 'MaterialIcon' !important; 40 | speak: none; 41 | font-style: normal; 42 | font-weight: normal; 43 | font-variant: normal; 44 | text-transform: none; 45 | line-height: 1; 46 | 47 | /* Better Font Rendering =========== */ 48 | -webkit-font-smoothing: antialiased; 49 | -moz-osx-font-smoothing: grayscale; 50 | } 51 | 52 | .text-input { 53 | width: 430px; 54 | padding: 12px 10px; 55 | height: 20px; 56 | line-height: 20px; 57 | font: 16px 'Helvetica Neue', Helvetica, Arial, sans-serif; 58 | border: 0 none; 59 | background: #f0f0f0; 60 | } 61 | 62 | .text-input:focus { 63 | background: #fff; 64 | } 65 | 66 | .todo-header h1 { 67 | font-size: 36px; 68 | font-weight: 200; 69 | margin: 0; 70 | height: 80px; 71 | line-height: 80px; 72 | text-align: center; 73 | } 74 | 75 | .todo-header > div { 76 | width: 100%; 77 | overflow: hidden; 78 | border: solid #e0e0e0; 79 | border-width: 1px 0 1px 0; 80 | } 81 | 82 | .todo-header .toggle-all { 83 | width: 49px; 84 | border-right: 1px solid #e0e0e0; 85 | height: 44px; 86 | line-height: 44px; 87 | font-size: 22px; 88 | text-align: center; 89 | float: left; 90 | color: #999; 91 | cursor: pointer; 92 | } 93 | 94 | .todo-header .toggle-all:before { 95 | content: "\e877"; 96 | } 97 | 98 | .todo-header .toggle-all.completed { 99 | color: #0D9E69; 100 | } 101 | 102 | .todo-header .text-input { 103 | float: left; 104 | } 105 | 106 | .todo-list { 107 | list-style: none; 108 | margin: 0; 109 | padding: 0; 110 | } 111 | 112 | .todo-list li { 113 | position: relative; 114 | border-bottom: 1px solid #e0e0e0; 115 | height: 44px; 116 | line-height: 44px; 117 | overflow: hidden; 118 | } 119 | 120 | .todo-list .toggle { 121 | width: 49px; 122 | border-right: 1px solid #e0e0e0; 123 | height: 44px; 124 | line-height: 44px; 125 | font-size: 22px; 126 | text-align: center; 127 | float: left; 128 | color: #aaa; 129 | cursor: pointer; 130 | } 131 | 132 | .todo-list .toggle.completed { 133 | color: #0D9E69; 134 | } 135 | 136 | .todo-list .toggle:before { 137 | content: "\e876"; 138 | } 139 | 140 | .todo-list .item { 141 | float: left; 142 | width: 450px; 143 | } 144 | 145 | .todo-list .text { 146 | padding: 0 10px; 147 | font-size: 16px; 148 | } 149 | 150 | .todo-list .text.completed { 151 | text-decoration: line-through; 152 | color: #999; 153 | } 154 | 155 | .todo-list .delete { 156 | width: 40px; 157 | height: 44px; 158 | line-height: 44px; 159 | font-size: 22px; 160 | color: #999; 161 | position: absolute; 162 | top: 0; 163 | right: 0; 164 | text-align: center; 165 | cursor: pointer; 166 | display: none; 167 | } 168 | 169 | .todo-list .delete:before { 170 | content: "\e5cd"; 171 | } 172 | 173 | .todo-list .delete:hover { 174 | color: #000; 175 | } 176 | 177 | .todo-list li:hover .delete { 178 | display: block; 179 | } 180 | 181 | .todo-footer { 182 | height: 40px; 183 | line-height: 40px; 184 | position: relative; 185 | margin: 10px 0 30px; 186 | } 187 | 188 | .todo-footer .count{ 189 | float: left; 190 | margin: 0; 191 | color: #666; 192 | } 193 | 194 | .todo-footer .filter { 195 | position: absolute; 196 | top: 7px; 197 | left: 50%; 198 | height: 27px; 199 | overflow: hidden; 200 | margin-left: -100px; 201 | } 202 | 203 | .todo-footer .filter a { 204 | float: left; 205 | padding: 0 6px; 206 | margin-right: 5px; 207 | border: 1px solid #e0e0e0; 208 | background: #fff; 209 | border-radius: 3px; 210 | text-align: center; 211 | cursor: pointer; 212 | height: 25px; 213 | line-height: 25px; 214 | text-transform: capitalize; 215 | color: #666; 216 | } 217 | 218 | .todo-footer .filter .current { 219 | border-color: #0D9E69; 220 | background: #f0f0f0; 221 | color: #0D9E69; 222 | } 223 | 224 | .todo-footer .clear-btn { 225 | float: right; 226 | padding: 0 6px; 227 | border: 1px solid #e0e0e0; 228 | background: #fff; 229 | border-radius: 3px; 230 | text-align: center; 231 | cursor: pointer; 232 | height: 25px; 233 | line-height: 25px; 234 | margin-top: 7px; 235 | text-transform: capitalize; 236 | color: #666; 237 | } 238 | 239 | .todo-footer .clear-btn:hover { 240 | color: #000; 241 | } 242 | 243 | .info { 244 | text-align: center; 245 | color: #666; 246 | } 247 | -------------------------------------------------------------------------------- /schema_CN.md: -------------------------------------------------------------------------------- 1 | # Schema 2 | 3 | ## 为什么会有 Schema ? 4 | 5 | Schema 的概念源于数据库中的数据存储,为了确保数据在存储到数据库中的时候数据是「可控」的,可以在存储数据时对数据结构、数据类型进行预定义,只有符合定义好的条件时才会存入。 6 | 7 | 在 Ballade 应用于前端开发中的时候,我们可以把 Store 当作一个简单的前端数据库,界面中所有的数据都是来源于 Store,Schema 确保了数据库的「写入」是可控的,那么从其中「读取」的数据也会是可控的。如果前端的数据都是可控的话,那么可以确保前端的界面不会因为数据的错误、异常而出现问题。这样一来,数据可控也就意味着界面可控。 8 | 9 | Ballade 的 Schema 的设计思想就来源于 [Mongoose Schema](http://mongoosejs.com/docs/schematypes.html)。 10 | 11 | 12 | ## Schema 的使用 13 | 14 | ### 基本使用 15 | 16 | Schema 的核心功能就是提前定义好数据的结构和数据项的类型。 17 | 18 | ```js 19 | var Schema = require('ballade').Schema; 20 | 21 | var schema1 = new Schema({ 22 | str: String, 23 | num: Number, 24 | bol: Boolean, 25 | date: Date, 26 | strArr: [String], 27 | numArr: [Number], 28 | dateArr: [Date], 29 | objArr: [{ 30 | name: String, 31 | title: String 32 | }], 33 | anyArr: [], 34 | anyObj: {}, 35 | obj: { 36 | votes: Number, 37 | favs: Number, 38 | foo: { 39 | bar: String 40 | } 41 | } 42 | }); 43 | ``` 44 | 45 | 对于上面的 schema 实例,在实际存储时符合 schema 定义的合法数据结果应该是下面这样的。 46 | 47 | ```js 48 | { 49 | str: 'hello', 50 | num: 2, 51 | bol: false, 52 | date: 'Sun Feb 12 2017 09:25:53 GMT+0800 (CST)', 53 | strArr: ['a', 'b'], 54 | numArr: [2, 4, 6], 55 | dateArr: ['Sun Feb 12 2017 09:25:53 GMT+0800 (CST)'], 56 | objArr: [{ 57 | name: 'this is name', 58 | title: 'this is title' 59 | }], 60 | anyArr: [1, '2', false, { foo: 'bar' }], 61 | anyObj: {foo: 'bar', 'a': {b: 'c'}}, 62 | obj: { 63 | votes: 200, 64 | favs: 100, 65 | foo: { 66 | bar: 'bar' 67 | } 68 | } 69 | } 70 | ``` 71 | 72 | #### Schema 的类型: 73 | 74 | Schema 支持如下类型的数据: 75 | 76 | * String 77 | * Number 78 | * Boolean 79 | * Date 80 | * Array 81 | * Object 82 | * Mixed 83 | 84 | Array 和 Object 是支持嵌套的。 85 | 86 | Mixed 就是任意类型,使用 Mixed 类型时,Schema 将不作任何校验。与其他类型不同,Mixed 并不是 JavaScript 内置的构造函数,因此要设置数据为 Mixed 类型,实际上就是设置 `Array` 或 `Object`. 像上面的 schema1 例子中,`anyArr: []` 和 `anyObj: {}` 的子元素就是 Mixed 类型,`[]` 和 `{}` 字面量的写法等同于 `Array` 和 `Object` 构造函数的写法。 87 | 88 | `anyArr: []` 等价于 `anyArr: Array`. 89 | 90 | `anyObj: {}` 等价于 `anyObj: Object`. 91 | 92 | ### 类型校验选项 93 | 94 | 对于类型描述,上面给出的写法是简写的,也可以使用 `$type` 属性来指定。 95 | 96 | ```js 97 | // 简写 98 | new Schema({ 99 | str: String 100 | }); 101 | 102 | // 使用 $type 字段 103 | new Schema({ 104 | str: { 105 | $type: String 106 | } 107 | }); 108 | ``` 109 | 110 | 那么什么情况下该使用 `$type` 呢?Ballade 的 Schema 还内置了其他很多不同的类型校验选项,用来更方便的数据的存储校验。 111 | 112 | 如可以指定存储的字符串是小写的。 113 | 114 | ```js 115 | new Schema({ 116 | str: { 117 | $type: String, 118 | $lowercase: true // 设置存储时的字符串是小写的 119 | } 120 | }); 121 | ``` 122 | 123 | 那么当写入 `str = 'Hello'` 这种数据时,最终的存储结果就会是 `str: 'hello'`。类型校验选项可以有多个组合使用。如下面就定义了存储时的值既要是小写也没有首尾空格。 124 | 125 | ```js 126 | new Schema({ 127 | str: { 128 | $type: String, 129 | $lowercase: true, // 设置存储时的字符串是小写的 130 | $trim: true // 去掉首尾空格 131 | } 132 | }); 133 | ``` 134 | 135 | 不同的数据类型,有不同的类型校验辅助选项,也有一些是通用的校验选项。 136 | 137 | Array 和 Object 没有辅助选项一说,因为设置这两种类型的数据实际上是 Mixed 类型,凡是 Mixed 类型,不做任何校验。 138 | 139 | **通用选项:** 140 | 141 | * `$required` *Boolean* 指定该字段必须要有值,如果存储时没值将抛出错误; 142 | * `$default` *Any* 指定该字段默认的值,如果存储时没有指定值将使用默认的值; 143 | * `$validate` *Function* 自定义校验的方法,存储值时会调用该校验方法; 144 | 145 | **字符串的选项:** 146 | 147 | * `$lowercase` *Boolean* 指定该字段在存储时转化成小写; 148 | * `$uppercase` *Boolean* 指定该字段在存储时转化成大写; 149 | * `$trim` *Boolean* 指定该字段在存储时去掉首位空格; 150 | * `$match` *Regexp* 指定该字段在存储时必须和给出的正则相匹配; 151 | * `$enum` *Array* 指定该字段在存储时必须和给出的条件列表相匹配; 152 | 153 | **数值的选项:** 154 | 155 | * `$min` *Number* 指定该字段在存储时其数值范围不能小于该值; 156 | * `$max` *Number* 指定该字段在存储时其数值范围不能大于该值; 157 | 158 | **日期的选项:** 159 | 160 | * `$min` *Date* 指定该字段在存储时其日期时间范围不能小于该值; 161 | * `$max` *Date* 指定该字段在存储时其日期时间范围不能大于该值; 162 | 163 | ### 错误处理 164 | 165 | 数据存储时如果类型校验不成功,Schema 内部会先尝试进行基本的转换,无论转换是否成功都会给出相应的消息提示。如果转换失败,则会给出 `error` 类型的消息。 166 | 167 | **不同的消息类型:** 168 | 169 | * `warning` 转换成功,则会给出 `warning` 类型的消息,存储可以正常进行; 170 | * `error` 转换失败,则会给出 `error` 类型的消息,存储不可以正常进行; 171 | 172 | `warning` 类型的消息; 173 | 174 | ```js 175 | // schema 176 | new Schema({ 177 | str: String 178 | }); 179 | 180 | // 这种存储场景因为可以转换,会给出 warning 的消息提示 181 | // 条件:str = 2; 182 | // 结果:str === '2' 183 | ``` 184 | 185 | `error` 类型的消息; 186 | 187 | ```js 188 | // schema 189 | new Schema({ 190 | num: Number 191 | }); 192 | 193 | // 这种存储场景不能转换,会给出 error 的消息提示 194 | // 条件:num = 'hello'; 195 | // 结果:num === undefined; 196 | ``` 197 | 198 | 对于 `null` 和 `undefined` 的值,则会直接给出 `error` 类型的消息,不能存储。 199 | 200 | ### 嵌套的 Schema 201 | 202 | 实际的数据存储场景会比较复杂,为了能满足实际的复杂场景,Schema 支持嵌套,这种情况主要出现在 `Array` 类型和 `Object` 类型的数据存储时。 203 | 204 | 简单的类型定义使用下面这种简单的嵌套写法是没有问题的。 205 | 206 | ```js 207 | new Schema({ 208 | objArr: [{ 209 | name: String, 210 | title: String 211 | }] 212 | }); 213 | ``` 214 | 215 | 但是要对每个数据项中的对象的属性使用类型校验辅助选项时就必须要使用 Schema 嵌套了。 216 | 217 | ```js 218 | var childSchema = new Schema({ 219 | name: { 220 | $type: String, 221 | $lowercase: true 222 | }, 223 | title: { 224 | $type: String, 225 | $uppercase: true 226 | } 227 | }); 228 | 229 | var parentSchema = new Schema({ 230 | objArr: [childSchema] // 嵌套的 Schema 231 | }); 232 | ``` 233 | 234 | 235 | ### 对于 Immutable 类型数据的支持 236 | 237 | Ballade 的 Schema 还支持 Immutable 类型的数据校验,所以在使用 Immutable 的 Store 时也可以无障碍的使用 Schema 的。 238 | 239 | Mutable 类型和 Immutable 类型的数据在 Schema 的定义上是没有任何区别的。 240 | 241 | ```js 242 | // schema 243 | new Schema({ 244 | foo: { 245 | bar: String, 246 | biz: String 247 | } 248 | }); 249 | 250 | // mutable 数据 251 | foo = { 252 | bar: 'bar', 253 | biz: 'biz' 254 | }; 255 | 256 | // immutable 数据 257 | foo = Immutable.Map({ 258 | bar: 'bar', 259 | biz: 'biz' 260 | }); 261 | ``` 262 | -------------------------------------------------------------------------------- /src/ballade.immutable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Ballade 1.2.8 3 | * author: chenmnkken@gmail.com 4 | * date: 2017-12-07 5 | * url: https://github.com/chenmnkken/ballade 6 | */ 7 | 8 | 'use strict'; 9 | 10 | var Queue = require('./queue'); 11 | var Schema = require('./schema'); 12 | var MutableStore = require('./store'); 13 | var ImmutableStore = require('./immutable-store'); 14 | var bindStore = require('./bindstore'); 15 | var immutableDeepEqual = require('./immutable-deep-equal'); 16 | 17 | var Ballade = { 18 | version: '1.2.8', 19 | Schema: Schema, 20 | bindStore: bindStore, 21 | immutableDeepEqual: immutableDeepEqual 22 | }; 23 | 24 | /** 25 | * Dispatcher Class 26 | */ 27 | var Dispatcher = function () { 28 | this.actionTypes = {}; 29 | this.storeQueue = []; 30 | this.id = Date.now() + Math.round(Math.random() * 1000); 31 | 32 | this.middlewareQueue = new Queue(function (payload) { 33 | this.__invokeCallback__(payload); 34 | }.bind(this), true); 35 | }; 36 | 37 | Dispatcher.prototype = { 38 | /** 39 | * When middleware queue is done, then invoke Store callbacks 40 | * @param {Object} Action payload 41 | */ 42 | __invokeCallback__: function (payload) { 43 | this.storeQueue.forEach(function (item) { 44 | var callback = item.callbacks[payload.type]; 45 | 46 | if (typeof callback === 'function') { 47 | setTimeout(function () { 48 | callback(item.store, payload); 49 | }, 0); 50 | } 51 | }); 52 | }, 53 | 54 | /** 55 | * Register action middleware 56 | * Middleware use to process action payload 57 | * @param {Function} middleware function 58 | */ 59 | use: function (middleware) { 60 | if (typeof middleware === 'function') { 61 | this.middlewareQueue.enter(middleware); 62 | } 63 | }, 64 | 65 | /** 66 | * Dispatch Action 67 | * Dispatch Action to Store callback 68 | * @param {String} actionsId is make sure every action type do not duplicate 69 | * @param {Object} action 70 | */ 71 | __dispatch__: function (actionsId, action) { 72 | var payload = action(); 73 | var actionTypes = this.actionTypes; 74 | var actionType = payload.type; 75 | var lastActionsId; 76 | 77 | if (!actionType) { 78 | throw new Error('action type does not exist in \n' + JSON.stringify(payload, null, 2)); 79 | } 80 | 81 | lastActionsId = actionTypes[actionType]; 82 | 83 | if (!lastActionsId) { 84 | actionTypes[actionType] = actionsId; 85 | } 86 | else if (lastActionsId !== actionsId) { 87 | throw new Error('action type "' + actionType + '" is duplicate'); 88 | } 89 | 90 | this.middlewareQueue.execute(payload); 91 | }, 92 | 93 | /** 94 | * Create Actions 95 | * @param {String} action creators 96 | * @return {Object} Actions 97 | */ 98 | createActions: function (actionCreators) { 99 | // actionsId is make sure every action type do not duplicate 100 | var actionsId = (this.id++).toString(32); 101 | var self = this; 102 | var name; 103 | var creator; 104 | var actions = {}; 105 | 106 | for (name in actionCreators) { 107 | creator = actionCreators[name]; 108 | 109 | actions[name] = (function (creator, actionsId) { 110 | return function () { 111 | var args = arguments; 112 | 113 | self.__dispatch__(actionsId, function () { 114 | return creator.apply(null, Array.prototype.slice.call(args)); 115 | }); 116 | }; 117 | })(creator, actionsId); 118 | } 119 | 120 | return actions; 121 | }, 122 | 123 | /** 124 | * Create mutable store 125 | * @param {Object} store schema 126 | * @param {Object} store callbacks 127 | * @return {Object} proxy store instance, it can not set data, only get data 128 | */ 129 | createMutableStore: function (schema, callbacks) { 130 | if (!callbacks) { 131 | throw new Error('schema must in createMutableStore arguments'); 132 | } 133 | 134 | var store = new MutableStore(schema); 135 | 136 | var proxyStore = { 137 | id: store.id, 138 | get: store.get.bind(store), 139 | publish: store.publish.bind(store), 140 | subscribe: store.subscribe.bind(store), 141 | unsubscribe: store.unsubscribe.bind(store) 142 | }; 143 | 144 | this.storeQueue.push({ 145 | store: store, 146 | callbacks: callbacks 147 | }); 148 | 149 | return proxyStore; 150 | }, 151 | 152 | /** 153 | * Create immutable store 154 | * @param {Object} store schema 155 | * @param {Object} store options 156 | * @param {Object} store callbacks 157 | * @return {Object} store instance 158 | */ 159 | createImmutableStore: function (schema, options, callbacks) { 160 | if (!callbacks) { 161 | callbacks = options; 162 | options = null; 163 | } 164 | 165 | var store = new ImmutableStore(schema, options); 166 | 167 | var proxyStore = { 168 | id: store.id, 169 | get: store.get.bind(store), 170 | publish: store.publish.bind(store), 171 | subscribe: store.subscribe.bind(store), 172 | unsubscribe: store.unsubscribe.bind(store) 173 | }; 174 | 175 | this.storeQueue.push({ 176 | store: store, 177 | callbacks: callbacks 178 | }); 179 | 180 | return proxyStore; 181 | } 182 | }; 183 | 184 | Ballade.Dispatcher = Dispatcher; 185 | 186 | module.exports = Ballade; 187 | -------------------------------------------------------------------------------- /test/mutable/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var actions1 = require('./actions1'); 5 | var actions2 = require('./actions2'); 6 | var store1 = require('./store1'); 7 | var store2 = require('./store2'); 8 | 9 | describe('Ballade mutable test', function () { 10 | describe('actions 1', function () { 11 | describe('updateTitle action 1', function () { 12 | it('should return: [foo is done]', function (done) { 13 | var firstUpdate = function (key) { 14 | var title = store1.get('title'); 15 | 16 | assert.strictEqual(title, 'foo is done'); 17 | store1.unsubscribe('title'); 18 | done(); 19 | }; 20 | 21 | store1.subscribe('title', firstUpdate); 22 | actions1.updateTitle('foo'); 23 | }); 24 | }); 25 | 26 | describe('updateTitle action 2', function () { 27 | it('should return: [bar is done]', function (done) { 28 | var secondUpdate = function () { 29 | var title = store1.get('title'); 30 | 31 | assert.strictEqual(title, 'bar is done'); 32 | store1.unsubscribe(secondUpdate); 33 | done(); 34 | }; 35 | 36 | setTimeout(function () { 37 | store1.subscribe('title', secondUpdate); 38 | actions1.updateTitle('bar'); 39 | }, 1000); 40 | }); 41 | }); 42 | 43 | describe('addMusic action', function () { 44 | it('should add music to playlist', function (done) { 45 | var handlePlaylist = function (key) { 46 | var playlist = store1.get('playlist'); 47 | var newPlaylist; 48 | 49 | assert.strictEqual(playlist[0].name, 'Ballade No.1'); 50 | assert.strictEqual(playlist[0].musician, 'Chopin'); 51 | 52 | // test unwriteable 53 | playlist.push({ 54 | name: 'Suite No.3 in D', 55 | musician: 'Bach' 56 | }); 57 | 58 | newPlaylist = store1.get('playlist'); 59 | 60 | assert.strictEqual(newPlaylist.length, 1); 61 | assert.strictEqual(newPlaylist[0].name, 'Ballade No.1'); 62 | assert.strictEqual(newPlaylist[0].musician, 'Chopin'); 63 | 64 | store1.unsubscribe('playlist'); 65 | done(); 66 | }; 67 | 68 | store1.subscribe('playlist', handlePlaylist); 69 | 70 | actions1.addMusic({ 71 | name: 'Ballade No.1', 72 | musician: 'Chopin' 73 | }); 74 | }); 75 | }); 76 | }); 77 | 78 | describe('actions 2', function () { 79 | describe('updateTitle action', function () { 80 | it('should return: [baz is done]', function (done) { 81 | var firstUpdate = function (key) { 82 | var title = store2.get('title'); 83 | 84 | assert.strictEqual(title, 'baz is done'); 85 | store2.unsubscribe('title'); 86 | done(); 87 | }; 88 | 89 | store2.subscribe('title', firstUpdate); 90 | actions2.updateTitle('baz'); 91 | }); 92 | }); 93 | 94 | describe('addMusic action', function () { 95 | it('should add music to playlist', function (done) { 96 | var handlePlaylist = function (key) { 97 | var playlist = store2.get('playlist'); 98 | var newPlaylist; 99 | 100 | assert.strictEqual(playlist[0].name, 'Suite No.3 in D'); 101 | assert.strictEqual(playlist[0].musician, 'Bach'); 102 | 103 | playlist.push({ 104 | name: 'Ballade No.1', 105 | musician: 'Chopin' 106 | }); 107 | 108 | newPlaylist = store2.get('playlist'); 109 | 110 | assert.strictEqual(newPlaylist.length, 1); 111 | assert.strictEqual(newPlaylist[0].name, 'Suite No.3 in D'); 112 | assert.strictEqual(newPlaylist[0].musician, 'Bach'); 113 | 114 | store2.unsubscribe('playlist'); 115 | done(); 116 | }; 117 | 118 | store2.subscribe('playlist', handlePlaylist); 119 | 120 | actions2.addMusic({ 121 | name: 'Suite No.3 in D', 122 | musician: 'Bach' 123 | }); 124 | }); 125 | }); 126 | }); 127 | 128 | describe('store2 dependence store1', function () { 129 | describe('say hello action', function () { 130 | it('should return: [Hello world]', function (done) { 131 | var sayHello = function (key) { 132 | var greetings = store2.get('greetings'); 133 | 134 | assert.strictEqual(greetings, 'Hello world'); 135 | store2.unsubscribe('greetings'); 136 | done(); 137 | }; 138 | 139 | store2.subscribe('greetings', sayHello); 140 | actions1.sayHello('Hello'); 141 | }); 142 | }); 143 | }); 144 | 145 | describe('$default for store', function () { 146 | it('should return $default value number 0', function (done) { 147 | var count = store1.get('count'); 148 | assert.strictEqual(count, 0); 149 | done(); 150 | }); 151 | 152 | it('should return $default value boolean false', function (done) { 153 | var extended = store1.get('extended'); 154 | assert.strictEqual(extended, false); 155 | done(); 156 | }); 157 | }); 158 | 159 | describe('cache for store', function () { 160 | it('should cache users in store', function (done) { 161 | var i = 0; 162 | 163 | for (; i < 12; i++) { 164 | actions1.addUser({ 165 | id: i, 166 | name: 'Xiaoming Li0' + (i + 1) 167 | }); 168 | } 169 | 170 | actions1.addUser({ 171 | id: 11, 172 | name: 'Xiaoming Li01' 173 | }); 174 | 175 | var users = store1.get('users'); 176 | var user5 = store1.get('users', 5); 177 | 178 | assert.strictEqual(users.length, 10); 179 | assert.strictEqual(user5.id, 5); 180 | assert.strictEqual(user5.name, 'Xiaoming Li06'); 181 | assert.strictEqual(store1.get('users', 11).name, 'Xiaoming Li01'); 182 | actions1.delUser(11); 183 | assert.strictEqual(store1.get('users', 11), undefined); 184 | done(); 185 | }); 186 | }); 187 | }); 188 | -------------------------------------------------------------------------------- /test/immutable/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Immutable = require('immutable'); 4 | var assert = require('assert'); 5 | var actions1 = require('./actions1'); 6 | var actions2 = require('./actions2'); 7 | var store1 = require('./store1'); 8 | var store2 = require('./store2'); 9 | 10 | describe('Ballade immutable test', function () { 11 | describe('actions 1', function () { 12 | describe('updateTitle action 1', function () { 13 | it('should return: [foo is done]', function (done) { 14 | var firstUpdate = function (key) { 15 | var title = store1.get('title'); 16 | 17 | assert.strictEqual(title, 'foo is done'); 18 | store1.unsubscribe('title', firstUpdate); 19 | done(); 20 | }; 21 | 22 | store1.subscribe('title', firstUpdate); 23 | actions1.updateTitle('foo'); 24 | }); 25 | }); 26 | 27 | describe('updateTitle action 2', function () { 28 | it('should return: [bar is done]', function (done) { 29 | var secondUpdate = function (key) { 30 | var title = store1.get('title'); 31 | 32 | assert.strictEqual(title, 'bar is done'); 33 | store1.unsubscribe(secondUpdate); 34 | done(); 35 | }; 36 | 37 | setTimeout(function () { 38 | store1.subscribe('title', secondUpdate); 39 | actions1.updateTitle('bar'); 40 | }, 1000); 41 | }); 42 | }); 43 | 44 | describe('addMusic action', function () { 45 | it('should add music to playlist', function (done) { 46 | var handlePlaylist = function (key) { 47 | var playlist = store1.get('playlist'); 48 | var newPlaylist; 49 | 50 | assert.strictEqual(playlist.getIn([0, 'name']), 'Ballade No.1'); 51 | assert.strictEqual(playlist.getIn([0, 'musician']), 'Chopin'); 52 | 53 | // test unwriteable 54 | playlist.push(Immutable.Map({ 55 | name: 'Suite No.3 in D', 56 | musician: 'Bach' 57 | })); 58 | 59 | newPlaylist = store1.get('playlist'); 60 | 61 | assert.strictEqual(newPlaylist.size, 1); 62 | assert.strictEqual(newPlaylist.getIn([0, 'name']), 'Ballade No.1'); 63 | assert.strictEqual(newPlaylist.getIn([0, 'musician']), 'Chopin'); 64 | 65 | store1.unsubscribe('playlist'); 66 | done(); 67 | }; 68 | 69 | store1.subscribe('playlist', handlePlaylist); 70 | 71 | actions1.addMusic({ 72 | name: 'Ballade No.1', 73 | musician: 'Chopin' 74 | }); 75 | }); 76 | }); 77 | }); 78 | 79 | describe('actions 2', function () { 80 | describe('updateTitle action', function () { 81 | it('should return: [baz is done]', function (done) { 82 | var firstUpdate = function (key) { 83 | var title = store2.get('title'); 84 | 85 | assert.strictEqual(title, 'baz is done'); 86 | store2.unsubscribe('title'); 87 | done(); 88 | }; 89 | 90 | store2.subscribe('title', firstUpdate); 91 | actions2.updateTitle('baz'); 92 | }); 93 | }); 94 | 95 | describe('addMusic action', function () { 96 | it('should add music to playlist', function (done) { 97 | var handlePlaylist = function (key) { 98 | var playlist = store2.get('playlist'); 99 | var newPlaylist; 100 | 101 | assert.strictEqual(playlist.getIn([0, 'name']), 'Suite No.3 in D'); 102 | assert.strictEqual(playlist.getIn([0, 'musician']), 'Bach'); 103 | 104 | playlist.push(Immutable.Map({ 105 | name: 'Ballade No.1', 106 | musician: 'Chopin' 107 | })); 108 | 109 | newPlaylist = store2.get('playlist'); 110 | 111 | assert.strictEqual(newPlaylist.size, 1); 112 | assert.strictEqual(newPlaylist.getIn([0, 'name']), 'Suite No.3 in D'); 113 | assert.strictEqual(newPlaylist.getIn([0, 'musician']), 'Bach'); 114 | 115 | store2.unsubscribe('playlist'); 116 | done(); 117 | }; 118 | 119 | store2.subscribe('playlist', handlePlaylist); 120 | 121 | actions2.addMusic({ 122 | name: 'Suite No.3 in D', 123 | musician: 'Bach' 124 | }); 125 | }); 126 | }); 127 | }); 128 | 129 | describe('store2 dependence store1', function () { 130 | describe('say hello action', function () { 131 | it('should return: [Hello world]', function (done) { 132 | var sayHello = function (key) { 133 | var greetings = store2.get('greetings'); 134 | 135 | assert.strictEqual(greetings, 'Hello world'); 136 | store2.unsubscribe('greetings'); 137 | done(); 138 | }; 139 | 140 | store2.subscribe('greetings', sayHello); 141 | actions1.sayHello('Hello'); 142 | }); 143 | }); 144 | }); 145 | 146 | describe('$default for store', function () { 147 | it('should return $default value number 0', function (done) { 148 | var count = store1.get('count'); 149 | assert.strictEqual(count, 0); 150 | done(); 151 | }); 152 | 153 | it('should return $default value boolean false', function (done) { 154 | var extended = store1.get('extended'); 155 | assert.strictEqual(extended, false); 156 | done(); 157 | }); 158 | }); 159 | 160 | describe('cache for store', function () { 161 | it('should cache users in store', function (done) { 162 | var i = 0; 163 | 164 | for (; i < 12; i++) { 165 | actions1.addUser({ 166 | id: i, 167 | name: 'Xiaoming Li0' + (i + 1) 168 | }); 169 | } 170 | 171 | actions1.addUser({ 172 | id: 11, 173 | name: 'Xiaoming Li01' 174 | }); 175 | 176 | var users = store1.get('users'); 177 | var user5 = store1.get('users', 5); 178 | 179 | assert.strictEqual(users.length, 10); 180 | assert.strictEqual(user5.get('id'), 5); 181 | assert.strictEqual(user5.get('name'), 'Xiaoming Li06'); 182 | assert.strictEqual(store1.get('users', 11).get('name'), 'Xiaoming Li01'); 183 | actions1.delUser(11); 184 | assert.strictEqual(store1.get('users', 11), undefined); 185 | done(); 186 | }); 187 | }); 188 | }); 189 | -------------------------------------------------------------------------------- /schema.md: -------------------------------------------------------------------------------- 1 | # Schema 2 | 3 | ## Why Schama? 4 | 5 | The concept of Schema originates from stored data to database, for reliable data when stored data to database, predefined the data structure and type, only validation data is stored. 6 | 7 | When Ballade used for development App on Browser, we can seen as Store is simply database, the all data from Store in UI Views, Schema make sure the stored operation is controllable, so get data is controllable too. If the App data is controllable, the UI Views is controllable too. 8 | 9 | The design idea of Ballade Schema originates from [Mongoose Schema](http://mongoosejs.com/docs/schematypes.html). 10 | 11 | 12 | ## Usage Schema 13 | 14 | ### Basic Usage 15 | 16 | The core feature of Schema is definition data structure and type. 17 | 18 | ```js 19 | var Schema = require('ballade').Schema; 20 | 21 | var schema1 = new Schema({ 22 | str: String, 23 | num: Number, 24 | bol: Boolean, 25 | date: Date, 26 | strArr: [String], 27 | numArr: [Number], 28 | dateArr: [Date], 29 | objArr: [{ 30 | name: String, 31 | title: String 32 | }], 33 | anyArr: [], 34 | anyObj: {}, 35 | obj: { 36 | votes: Number, 37 | favs: Number, 38 | foo: { 39 | bar: String 40 | } 41 | } 42 | }); 43 | ``` 44 | 45 | For above Schema instance, in actual storage, the data is should like the below. 46 | 47 | ```js 48 | { 49 | str: 'hello', 50 | num: 2, 51 | bol: false, 52 | date: 'Sun Feb 12 2017 09:25:53 GMT+0800 (CST)', 53 | strArr: ['a', 'b'], 54 | numArr: [2, 4, 6], 55 | dateArr: ['Sun Feb 12 2017 09:25:53 GMT+0800 (CST)'], 56 | objArr: [{ 57 | name: 'this is name', 58 | title: 'this is title' 59 | }], 60 | anyArr: [1, '2', false, { foo: 'bar' }], 61 | anyObj: {foo: 'bar', 'a': {b: 'c'}}, 62 | obj: { 63 | votes: 200, 64 | favs: 100, 65 | foo: { 66 | bar: 'bar' 67 | } 68 | } 69 | } 70 | ``` 71 | 72 | #### Schema types: 73 | 74 | * String 75 | * Number 76 | * Boolean 77 | * Date 78 | * Array 79 | * Object 80 | * Mixed 81 | 82 | Array and Object both support nested. 83 | 84 | Mixed is any type, if the data is Mixed type, Schema should not any validation. Mixed not built-in constructor in JavaScript, therefore set data is Mixed, in fact set `Array` or `Object`, above schema1 example, `anyArr: []` and `anyObj: {}` the children is Mixed, `[]` and `{}` same as `Array` and `Object`. 85 | 86 | `anyArr: []` same as `anyArr: Array`. 87 | 88 | `anyObj: {}` same as `anyObj: Object`. 89 | 90 | ### Types Validation 91 | 92 | Above example, can also use `$type`. 93 | 94 | ```js 95 | // simply 96 | new Schema({ 97 | str: String 98 | }); 99 | 100 | // $type 101 | new Schema({ 102 | str: { 103 | $type: String 104 | } 105 | }); 106 | ``` 107 | 108 | What time is use `$type`? Ballade Schema also include others types validation, be used for data stored validation more easily. 109 | 110 | For example, make sure stored string is lowercase letter. 111 | 112 | ```js 113 | new Schema({ 114 | str: { 115 | $type: String, 116 | $lowercase: true // configure the lowercase letter 117 | } 118 | }); 119 | ``` 120 | 121 | If stored `str = 'Hello'`, the final result is `str: 'hello'`. Types validation can multiple simultaneous use. For below example, the vaule is lowercase letter and trimed. 122 | 123 | ```js 124 | new Schema({ 125 | str: { 126 | $type: String, 127 | $lowercase: true, // configure the lowercase letter 128 | $trim: true // configure the trim 129 | } 130 | }); 131 | ``` 132 | 133 | Different data type have different types validation auxiliary options. There are also some options is general. 134 | 135 | Array and Object is not have validation auxiliary options, because Array and Object both is Mixed type, the Mixed type is not validation. 136 | 137 | **General Options** 138 | 139 | * `$required` *Boolean* 140 | 141 | Value is required. 142 | 143 | * `$default` *Any* 144 | 145 | Default value. 146 | 147 | * `$validate` *Function* 148 | 149 | Custom validation function. 150 | 151 | **String Options** 152 | 153 | * `$lowercase` *Boolean* 154 | 155 | The value is converted to lowercase. 156 | 157 | * `$uppercase` *Boolean* 158 | 159 | The value is converted to uppercase. 160 | 161 | * `$trim` *Boolean* 162 | 163 | The value is trimed. 164 | 165 | * `$match` *Regexp* 166 | 167 | Match the given regexp expression. 168 | 169 | * `$enum` *Array* 170 | 171 | Match the list of conditions. 172 | 173 | **Number Options** 174 | 175 | * `$min` *Number* 176 | 177 | Cannot be less than this value. 178 | 179 | * `$max` *Number* 180 | 181 | Cannot be greater than this value. 182 | 183 | **Date Options** 184 | 185 | * `$min` *Date* 186 | 187 | Cannot be less than this date value. 188 | 189 | * `$max` *Date* 190 | 191 | Cannot be greater than this value. 192 | 193 | ### Error Handler 194 | 195 | If data invaild, Schema try to convert base type, whether success or failure will give tips. If the convert still failed will throw `error` message. 196 | 197 | #### Conversion Message Types 198 | 199 | * `warning` Successful conversion will show `warning` type message, the storage can be normal. 200 | 201 | * `error` Conversion is failed will show `error` type message, the storage is failed too. 202 | 203 | `warning` message: 204 | 205 | ```js 206 | // schema 207 | new Schema({ 208 | str: String 209 | }); 210 | 211 | // This storage will throw warning message, but conversion is success. 212 | // condition: str = 2; 213 | // result: str === '2'; 214 | ``` 215 | 216 | `error` message: 217 | 218 | ```js 219 | // schema 220 | new Schema({ 221 | num: Number 222 | }); 223 | 224 | // This storage will failed, because conversion is failed. 225 | // condition: num = 'hello'; 226 | // result: num === undefined; 227 | ``` 228 | 229 | The value is `null` or `undefined`, will throw error message, can't be stored. 230 | 231 | ### Nested Schema 232 | 233 | The actual storage scenario is more complex, for complex scenario, Schema support nested, be uesd for `Array` and `Object` data type. 234 | 235 | Simply types definition for nested schema. 236 | 237 | ```js 238 | new Schema({ 239 | objArr: [{ 240 | name: String, 241 | title: String 242 | }] 243 | }); 244 | ``` 245 | 246 | If want use auxiliary options for types validation, Schema must be nested. 247 | 248 | ```js 249 | var childSchema = new Schema({ 250 | name: { 251 | $type: String, 252 | $lowercase: true 253 | }, 254 | title: { 255 | $type: String, 256 | $uppercase: true 257 | } 258 | }); 259 | 260 | var parentSchema = new Schema({ 261 | objArr: [childSchema] // Nested Schema 262 | }); 263 | ``` 264 | 265 | ### For Immutable Data 266 | 267 | Ballade Schema also support immutable data validation. Immutable data validation same as mutable data, there is no difference. 268 | 269 | ```js 270 | // schema 271 | new Schema({ 272 | foo: { 273 | bar: String, 274 | biz: String 275 | } 276 | }); 277 | 278 | // mutable data 279 | foo = { 280 | bar: 'bar', 281 | biz: 'biz' 282 | }; 283 | 284 | // immutable data 285 | foo = Immutable.Map({ 286 | bar: 'bar', 287 | biz: 'biz' 288 | }); 289 | ``` 290 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var toString = Object.prototype.toString; 4 | var copy = require('./copy'); 5 | var Event = require('./event'); 6 | var Cache = require('./cache'); 7 | var persistence = require('./persistence'); 8 | 9 | var baseTypes = { 10 | 'string': true, 11 | 'number': true, 12 | 'null': true, 13 | 'undefind': true, 14 | 'boolean': true 15 | }; 16 | 17 | var _typeof = function (subject) { 18 | return toString.call(subject).slice(8, -1); 19 | }; 20 | 21 | var outputImmutableData = function (data, _Immutable) { 22 | var type = _typeof(data); 23 | 24 | if (type === 'Array' || type === 'Object') { 25 | return _Immutable.fromJS(data); 26 | } 27 | 28 | return data; 29 | }; 30 | 31 | /** 32 | * Store Class 33 | * @param {Object} store schema 34 | * @param {Object} store options 35 | * options.cache set cache in store 36 | * options.error schema validator error 37 | * var Store = new MStore({foo: 'bar'}); 38 | * @Store.mutable: Store data 39 | * @Store.event: Event instance 40 | */ 41 | var Store = function (schema, options, _Immutable) { 42 | Event.call(this); 43 | options = options || {}; 44 | 45 | var defaultData = schema.defaultData; 46 | var cacheOptions = options.cache; 47 | var self = this; 48 | 49 | this.store = {}; 50 | this.cache = {}; 51 | this.schema = schema; 52 | this.Immutable = _Immutable; 53 | this.options = options; 54 | this.id = 'BalladeStore-' + (+new Date() + Math.floor(Math.random() * 999999)).toString(36); 55 | 56 | Object.keys(schema.dataTypes).forEach(function (key) { 57 | var hasCache = cacheOptions && key in cacheOptions; 58 | var hasIdCache = false; 59 | var value; 60 | 61 | if (hasCache && cacheOptions[key].id) { 62 | self.cache[key] = new Cache(cacheOptions[key]); 63 | hasIdCache = true; 64 | } 65 | 66 | if (hasCache && cacheOptions[key].persistence) { 67 | value = persistence.get(cacheOptions[key].persistence.prefix + '.' + key, cacheOptions[key].persistence.type); 68 | } 69 | 70 | if (value === null || value === undefined) { 71 | value = defaultData[key]; 72 | } 73 | 74 | if (value !== null && value !== undefined) { 75 | if (_Immutable) { 76 | value = outputImmutableData(value, _Immutable); 77 | } 78 | 79 | if (hasIdCache) { 80 | self.cache[key].set(value, !!_Immutable); 81 | } 82 | else { 83 | self.store[key] = value; 84 | } 85 | } 86 | }); 87 | }; 88 | 89 | Store.prototype = Object.create(Event.prototype, { 90 | constructor: { 91 | value: Store, 92 | enumerable: false, 93 | writable: true, 94 | configurable: true 95 | } 96 | }); 97 | 98 | /** 99 | * Set data in store. 100 | * If the key not in schema, set operation should failed. 101 | * @param {String} object key 102 | * @param {Any} data 103 | * @param {Boolean} If pureSet is true, do not publish data change event. 104 | * @return {String} object key 105 | */ 106 | Store.prototype.set = function (key, value, pureSet) { 107 | var options = this.options; 108 | var cacheOptions = options.cache; 109 | var isImmutable = this.Immutable && _typeof(value.toJS) === 'Function'; 110 | var result = this.schema.validator(key, value, isImmutable); 111 | var errors = []; 112 | var newValue; 113 | 114 | if (result.messages) { 115 | result.messages.forEach(function (item) { 116 | if (item.type === 'warning') { 117 | console.warn('Schema Validation Warning: ' + item.message + ', path is `' + item.path + '`, value is ', item.originalValue); 118 | } 119 | else if (item.type === 'error') { 120 | console.error('Schema Validation Error: ' + item.message + ', path is `' + item.path + '`, value is ', item.originalValue); 121 | errors.push(item); 122 | } 123 | }); 124 | 125 | if (options && options.error) { 126 | options.error({ 127 | key: key, 128 | type: 'SCHEMA_VALIDATION_ERROR', 129 | messages: errors 130 | }, this); 131 | } 132 | } 133 | 134 | if ('value' in result) { 135 | if (this.Immutable) { 136 | newValue = isImmutable ? result.value : outputImmutableData(result.value, this.Immutable); 137 | } 138 | else { 139 | newValue = result.value; 140 | } 141 | 142 | if (key in this.cache) { 143 | this.cache[key].set(newValue, !!this.Immutable); 144 | } 145 | else { 146 | this.store[key] = newValue; 147 | } 148 | 149 | if (cacheOptions && cacheOptions[key] && cacheOptions[key].persistence) { 150 | persistence.set( 151 | cacheOptions[key].persistence.prefix + '.' + key, 152 | newValue, 153 | cacheOptions[key].persistence.type 154 | ); 155 | } 156 | 157 | if (!pureSet) { 158 | this.publish(key, newValue); 159 | } 160 | 161 | return key; 162 | } 163 | }; 164 | 165 | /** 166 | * Get data from store. 167 | * If data is reference type, should return copies of data 168 | * @param {String} object key 169 | * @param {String} Cache id 170 | * @return {Any} data 171 | */ 172 | Store.prototype.get = function (key, id) { 173 | var isImmutable = !!this.Immutable; 174 | var result; 175 | var type; 176 | 177 | if (key in this.cache) { 178 | if (id !== undefined) { 179 | result = this.cache[key].get(id, isImmutable); 180 | } 181 | else { 182 | result = this.cache[key].cacheStore; 183 | } 184 | } 185 | else { 186 | result = this.store[key]; 187 | } 188 | 189 | if (isImmutable) { 190 | return result; 191 | } 192 | 193 | type = typeof result; 194 | 195 | if (baseTypes[type]) { 196 | return result; 197 | } 198 | 199 | return copy(result); 200 | }; 201 | 202 | /** 203 | * Delete data from store. 204 | * @param {String} object key 205 | * @param {String} Cache id 206 | * @return {String} object key 207 | */ 208 | Store.prototype.delete = function (key, id) { 209 | var cacheOptions = this.options.cache; 210 | 211 | if (id && key in this.cache) { 212 | this.cache[key].delete(id, !!this.Immutable); 213 | } 214 | else { 215 | delete this.store[key]; 216 | } 217 | 218 | if (cacheOptions && cacheOptions[key] && cacheOptions[key].persistence) { 219 | persistence.delete( 220 | cacheOptions[key].persistence.prefix + '.' + key, 221 | cacheOptions[key].persistence.type 222 | ); 223 | } 224 | 225 | this.publish(key, this.get(key, id)); 226 | return key; 227 | }; 228 | 229 | /** 230 | * clean cache data 231 | * @param {String} object key 232 | * @return {String} object key 233 | */ 234 | Store.prototype.cleanCache = function (key) { 235 | var cache = this.cache[key]; 236 | 237 | if (cache) { 238 | cache.cacheStore.length = 0; 239 | Object.keys(cache.idKeys).forEach(function (item) { 240 | delete cache.idKeys[item]; 241 | }); 242 | } 243 | 244 | return key; 245 | }; 246 | 247 | module.exports = Store; 248 | -------------------------------------------------------------------------------- /test/schema-immutable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var Immutable = require('immutable'); 5 | var Schema = require('../src/schema'); 6 | var fromJS = Immutable.fromJS; 7 | var Map = Immutable.Map; 8 | var List = Immutable.List; 9 | 10 | describe('Schema validator test immutable data', function () { 11 | describe('schema validator & cast', function () { 12 | var schema1 = new Schema({ 13 | str: String, 14 | num: Number, 15 | strArr: [String], 16 | numArr: [Number], 17 | dateArr: [Date], 18 | objArr: [{ 19 | name: String, 20 | title: String 21 | }], 22 | anyArr: [], 23 | anyObj: {}, 24 | obj: { 25 | votes: Number, 26 | favs: Number, 27 | foo: { 28 | bar: String 29 | } 30 | } 31 | }); 32 | 33 | describe('schema type common validator', function () { 34 | it('all of data should vaild', function (done) { 35 | var date = new Date(); 36 | 37 | var objArr = fromJS([{name: 'Ballade', title: 'Ballade test'}]); 38 | var objArrResult = schema1.validator('objArr', objArr, true); 39 | 40 | var anyArr = fromJS([1, '2', false, {name: 'Ballade', title: 'Ballade test'}]); 41 | var anyArrResult = schema1.validator('anyArr', anyArr, true); 42 | 43 | var anyObj = Map({foo: 'bar', bar: 'biz', age: 23}); 44 | var anyObjResult = schema1.validator('anyObj', anyObj, true); 45 | 46 | var obj = fromJS({votes: 2, favs: 100, foo: {bar: 'biz'}, redundant: 'Redundant'}); 47 | var objResult = schema1.validator('obj', obj, true); 48 | 49 | var strArr = List(['1', 'hello', 'world']); 50 | var numArr = List([3, 5, 7]); 51 | var dateArr = List([date, date]); 52 | 53 | assert.strictEqual(schema1.validator('strArr', strArr, true).value, strArr); 54 | assert.strictEqual(schema1.validator('numArr', numArr, true).value, numArr); 55 | assert.strictEqual(schema1.validator('dateArr', dateArr, true).value, dateArr); 56 | assert.strictEqual(objArrResult.value, objArr); 57 | assert.strictEqual(anyArrResult.value, anyArr); 58 | assert.strictEqual(anyObjResult.value, anyObj); 59 | assert(Immutable.is(objResult.value, obj.delete('redundant'))); 60 | 61 | done(); 62 | }); 63 | }); 64 | 65 | describe('schema type cast validator', function () { 66 | it('data is valid, but will throw warning', function (done) { 67 | var strResult = schema1.validator('str', 1, true); 68 | var numResult = schema1.validator('num', '2', true); 69 | 70 | assert.strictEqual(strResult.value, '1'); 71 | assert.strictEqual(strResult.messages[0].path, 'str'); 72 | assert.strictEqual(strResult.messages[0].originalValue, 1); 73 | assert.strictEqual(strResult.messages[0].type, 'warning'); 74 | 75 | assert.strictEqual(numResult.value, 2); 76 | assert.strictEqual(numResult.messages[0].path, 'num'); 77 | assert.strictEqual(numResult.messages[0].originalValue, '2'); 78 | assert.strictEqual(numResult.messages[0].type, 'warning'); 79 | 80 | done(); 81 | }); 82 | 83 | it('data is invalid, will throw error', function (done) { 84 | var numResult = schema1.validator('num', 'hello'); 85 | var obj = Map({foo: 'bar'}); 86 | var strResult = schema1.validator('str', obj, true); 87 | 88 | assert.strictEqual(numResult.messages[0].path, 'num'); 89 | assert.strictEqual(numResult.messages[0].originalValue, 'hello'); 90 | assert.strictEqual(numResult.messages[0].type, 'error'); 91 | 92 | assert.strictEqual(strResult.messages[0].path, 'str'); 93 | assert.strictEqual(strResult.messages[0].originalValue, obj); 94 | assert.strictEqual(strResult.messages[0].type, 'error'); 95 | 96 | done(); 97 | }); 98 | }); 99 | }); 100 | 101 | describe('schema options', function () { 102 | var schema2 = new Schema({ 103 | bar: { 104 | biz: { 105 | $type: String, 106 | $required: true 107 | }, 108 | count: Number 109 | }, 110 | obj: { 111 | votes: { 112 | $type: Number, 113 | $default: 1 114 | }, 115 | favs: Number 116 | } 117 | }); 118 | 119 | it('required option is work', function (done) { 120 | var obj = Map({ 121 | count: 100 122 | }); 123 | 124 | var result1 = schema2.validator('bar', obj, true); 125 | obj = obj.set('biz', 'biz'); 126 | var result2 = schema2.validator('bar', obj, true); 127 | 128 | assert.strictEqual(result1.value.get('count'), 100); 129 | assert.strictEqual(result1.messages[0].path, 'bar.biz'); 130 | assert.strictEqual(result1.messages[0].originalValue, undefined); 131 | assert.strictEqual(result1.messages[0].type, 'error'); 132 | assert.strictEqual(result2.value.get('biz'), 'biz'); 133 | 134 | done(); 135 | }); 136 | 137 | it('default option is work', function (done) { 138 | var obj = Map({ 139 | favs: 1 140 | }); 141 | 142 | var result1 = schema2.validator('obj', obj, true); 143 | 144 | obj = obj.set('favs', 2); 145 | obj = obj.set('votes', 2); 146 | 147 | var result2 = schema2.validator('obj', obj, true); 148 | 149 | assert.strictEqual(result1.value.get('favs'), 1); 150 | assert.strictEqual(result1.value.get('votes'), 1); 151 | assert.strictEqual(result2.value.get('favs'), 2); 152 | assert.strictEqual(result2.value.get('votes'), 2); 153 | 154 | done(); 155 | }); 156 | }); 157 | 158 | describe('schema nested', function () { 159 | var schema3 = new Schema({ 160 | foo: { 161 | bar: { 162 | $type: String, 163 | $default: 'bar' 164 | }, 165 | biz: String 166 | }, 167 | count: Number 168 | }); 169 | 170 | var schema4 = new Schema({ 171 | arr: [schema3] 172 | }); 173 | 174 | var schema5 = new Schema({ 175 | str: { 176 | $type: String, 177 | $uppercase: true 178 | }, 179 | num: Number 180 | }); 181 | 182 | var schema6 = new Schema({ 183 | meta: schema5 184 | }); 185 | 186 | it('child schema is work', function (done) { 187 | var arr = fromJS([{ 188 | foo: { 189 | biz: 'biz' 190 | }, 191 | count: 100 192 | }, { 193 | foo: { 194 | bar: 'hello', 195 | biz: 1 196 | }, 197 | count: '122' 198 | }]); 199 | 200 | var result = schema4.validator('arr', arr, true); 201 | 202 | assert.strictEqual(result.value.getIn([0, 'foo', 'bar']), 'bar'); 203 | assert.strictEqual(result.value.getIn([0, 'foo', 'biz']), 'biz'); 204 | assert.strictEqual(result.value.getIn([0, 'count']), 100); 205 | 206 | assert.strictEqual(result.value.getIn([1, 'foo', 'bar']), 'hello'); 207 | assert.strictEqual(result.value.getIn([1, 'foo', 'biz']), '1'); 208 | assert.strictEqual(result.value.getIn([1, 'count']), 122); 209 | 210 | assert.strictEqual(result.messages[0].path, 'arr[1].foo.biz'); 211 | assert.strictEqual(result.messages[0].originalValue, 1); 212 | assert.strictEqual(result.messages[0].type, 'warning'); 213 | 214 | assert.strictEqual(result.messages[1].path, 'arr[1].count'); 215 | assert.strictEqual(result.messages[1].originalValue, '122'); 216 | assert.strictEqual(result.messages[1].type, 'warning'); 217 | 218 | var result2 = schema6.validator('meta', Map({ 219 | str: 'hello', 220 | num: 100 221 | }), true); 222 | 223 | assert.strictEqual(result2.value.get('str'), 'HELLO'); 224 | assert.strictEqual(result2.value.get('num'), 100); 225 | assert.strictEqual(result2.messages, undefined); 226 | 227 | done(); 228 | }); 229 | }); 230 | }); 231 | -------------------------------------------------------------------------------- /examples/ballade-mutable-todomvc/src/js/babel-external-helpers.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { 2 | if (typeof define === "function" && define.amd) { 3 | define(["exports"], factory); 4 | } else if (typeof exports === "object") { 5 | factory(exports); 6 | } else { 7 | factory(root.babelHelpers = {}); 8 | } 9 | })(this, function (global) { 10 | var babelHelpers = global; 11 | babelHelpers.typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { 12 | return typeof obj; 13 | } : function (obj) { 14 | return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; 15 | }; 16 | 17 | babelHelpers.jsx = function () { 18 | var REACT_ELEMENT_TYPE = typeof Symbol === "function" && Symbol.for && Symbol.for("react.element") || 0xeac7; 19 | return function createRawReactElement(type, props, key, children) { 20 | var defaultProps = type && type.defaultProps; 21 | var childrenLength = arguments.length - 3; 22 | 23 | if (!props && childrenLength !== 0) { 24 | props = {}; 25 | } 26 | 27 | if (props && defaultProps) { 28 | for (var propName in defaultProps) { 29 | if (props[propName] === void 0) { 30 | props[propName] = defaultProps[propName]; 31 | } 32 | } 33 | } else if (!props) { 34 | props = defaultProps || {}; 35 | } 36 | 37 | if (childrenLength === 1) { 38 | props.children = children; 39 | } else if (childrenLength > 1) { 40 | var childArray = Array(childrenLength); 41 | 42 | for (var i = 0; i < childrenLength; i++) { 43 | childArray[i] = arguments[i + 3]; 44 | } 45 | 46 | props.children = childArray; 47 | } 48 | 49 | return { 50 | $$typeof: REACT_ELEMENT_TYPE, 51 | type: type, 52 | key: key === undefined ? null : '' + key, 53 | ref: null, 54 | props: props, 55 | _owner: null 56 | }; 57 | }; 58 | }(); 59 | 60 | babelHelpers.asyncToGenerator = function (fn) { 61 | return function () { 62 | var gen = fn.apply(this, arguments); 63 | return new Promise(function (resolve, reject) { 64 | function step(key, arg) { 65 | try { 66 | var info = gen[key](arg); 67 | var value = info.value; 68 | } catch (error) { 69 | reject(error); 70 | return; 71 | } 72 | 73 | if (info.done) { 74 | resolve(value); 75 | } else { 76 | return Promise.resolve(value).then(function (value) { 77 | return step("next", value); 78 | }, function (err) { 79 | return step("throw", err); 80 | }); 81 | } 82 | } 83 | 84 | return step("next"); 85 | }); 86 | }; 87 | }; 88 | 89 | babelHelpers.classCallCheck = function (instance, Constructor) { 90 | if (!(instance instanceof Constructor)) { 91 | throw new TypeError("Cannot call a class as a function"); 92 | } 93 | }; 94 | 95 | babelHelpers.createClass = function () { 96 | function defineProperties(target, props) { 97 | for (var i = 0; i < props.length; i++) { 98 | var descriptor = props[i]; 99 | descriptor.enumerable = descriptor.enumerable || false; 100 | descriptor.configurable = true; 101 | if ("value" in descriptor) descriptor.writable = true; 102 | Object.defineProperty(target, descriptor.key, descriptor); 103 | } 104 | } 105 | 106 | return function (Constructor, protoProps, staticProps) { 107 | if (protoProps) defineProperties(Constructor.prototype, protoProps); 108 | if (staticProps) defineProperties(Constructor, staticProps); 109 | return Constructor; 110 | }; 111 | }(); 112 | 113 | babelHelpers.defineEnumerableProperties = function (obj, descs) { 114 | for (var key in descs) { 115 | var desc = descs[key]; 116 | desc.configurable = desc.enumerable = true; 117 | if ("value" in desc) desc.writable = true; 118 | Object.defineProperty(obj, key, desc); 119 | } 120 | 121 | return obj; 122 | }; 123 | 124 | babelHelpers.defaults = function (obj, defaults) { 125 | var keys = Object.getOwnPropertyNames(defaults); 126 | 127 | for (var i = 0; i < keys.length; i++) { 128 | var key = keys[i]; 129 | var value = Object.getOwnPropertyDescriptor(defaults, key); 130 | 131 | if (value && value.configurable && obj[key] === undefined) { 132 | Object.defineProperty(obj, key, value); 133 | } 134 | } 135 | 136 | return obj; 137 | }; 138 | 139 | babelHelpers.defineProperty = function (obj, key, value) { 140 | if (key in obj) { 141 | Object.defineProperty(obj, key, { 142 | value: value, 143 | enumerable: true, 144 | configurable: true, 145 | writable: true 146 | }); 147 | } else { 148 | obj[key] = value; 149 | } 150 | 151 | return obj; 152 | }; 153 | 154 | babelHelpers.extends = Object.assign || function (target) { 155 | for (var i = 1; i < arguments.length; i++) { 156 | var source = arguments[i]; 157 | 158 | for (var key in source) { 159 | if (Object.prototype.hasOwnProperty.call(source, key)) { 160 | target[key] = source[key]; 161 | } 162 | } 163 | } 164 | 165 | return target; 166 | }; 167 | 168 | babelHelpers.get = function get(object, property, receiver) { 169 | if (object === null) object = Function.prototype; 170 | var desc = Object.getOwnPropertyDescriptor(object, property); 171 | 172 | if (desc === undefined) { 173 | var parent = Object.getPrototypeOf(object); 174 | 175 | if (parent === null) { 176 | return undefined; 177 | } else { 178 | return get(parent, property, receiver); 179 | } 180 | } else if ("value" in desc) { 181 | return desc.value; 182 | } else { 183 | var getter = desc.get; 184 | 185 | if (getter === undefined) { 186 | return undefined; 187 | } 188 | 189 | return getter.call(receiver); 190 | } 191 | }; 192 | 193 | babelHelpers.inherits = function (subClass, superClass) { 194 | if (typeof superClass !== "function" && superClass !== null) { 195 | throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); 196 | } 197 | 198 | subClass.prototype = Object.create(superClass && superClass.prototype, { 199 | constructor: { 200 | value: subClass, 201 | enumerable: false, 202 | writable: true, 203 | configurable: true 204 | } 205 | }); 206 | if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; 207 | }; 208 | 209 | babelHelpers.instanceof = function (left, right) { 210 | if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { 211 | return right[Symbol.hasInstance](left); 212 | } else { 213 | return left instanceof right; 214 | } 215 | }; 216 | 217 | babelHelpers.interopRequireDefault = function (obj) { 218 | return obj && obj.__esModule ? obj : { 219 | default: obj 220 | }; 221 | }; 222 | 223 | babelHelpers.interopRequireWildcard = function (obj) { 224 | if (obj && obj.__esModule) { 225 | return obj; 226 | } else { 227 | var newObj = {}; 228 | 229 | if (obj != null) { 230 | for (var key in obj) { 231 | if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; 232 | } 233 | } 234 | 235 | newObj.default = obj; 236 | return newObj; 237 | } 238 | }; 239 | 240 | babelHelpers.newArrowCheck = function (innerThis, boundThis) { 241 | if (innerThis !== boundThis) { 242 | throw new TypeError("Cannot instantiate an arrow function"); 243 | } 244 | }; 245 | 246 | babelHelpers.objectDestructuringEmpty = function (obj) { 247 | if (obj == null) throw new TypeError("Cannot destructure undefined"); 248 | }; 249 | 250 | babelHelpers.objectWithoutProperties = function (obj, keys) { 251 | var target = {}; 252 | 253 | for (var i in obj) { 254 | if (keys.indexOf(i) >= 0) continue; 255 | if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; 256 | target[i] = obj[i]; 257 | } 258 | 259 | return target; 260 | }; 261 | 262 | babelHelpers.possibleConstructorReturn = function (self, call) { 263 | if (!self) { 264 | throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); 265 | } 266 | 267 | return call && (typeof call === "object" || typeof call === "function") ? call : self; 268 | }; 269 | 270 | babelHelpers.selfGlobal = typeof global === "undefined" ? self : global; 271 | 272 | babelHelpers.set = function set(object, property, value, receiver) { 273 | var desc = Object.getOwnPropertyDescriptor(object, property); 274 | 275 | if (desc === undefined) { 276 | var parent = Object.getPrototypeOf(object); 277 | 278 | if (parent !== null) { 279 | set(parent, property, value, receiver); 280 | } 281 | } else if ("value" in desc && desc.writable) { 282 | desc.value = value; 283 | } else { 284 | var setter = desc.set; 285 | 286 | if (setter !== undefined) { 287 | setter.call(receiver, value); 288 | } 289 | } 290 | 291 | return value; 292 | }; 293 | 294 | babelHelpers.slicedToArray = function () { 295 | function sliceIterator(arr, i) { 296 | var _arr = []; 297 | var _n = true; 298 | var _d = false; 299 | var _e = undefined; 300 | 301 | try { 302 | for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { 303 | _arr.push(_s.value); 304 | 305 | if (i && _arr.length === i) break; 306 | } 307 | } catch (err) { 308 | _d = true; 309 | _e = err; 310 | } finally { 311 | try { 312 | if (!_n && _i["return"]) _i["return"](); 313 | } finally { 314 | if (_d) throw _e; 315 | } 316 | } 317 | 318 | return _arr; 319 | } 320 | 321 | return function (arr, i) { 322 | if (Array.isArray(arr)) { 323 | return arr; 324 | } else if (Symbol.iterator in Object(arr)) { 325 | return sliceIterator(arr, i); 326 | } else { 327 | throw new TypeError("Invalid attempt to destructure non-iterable instance"); 328 | } 329 | }; 330 | }(); 331 | 332 | babelHelpers.slicedToArrayLoose = function (arr, i) { 333 | if (Array.isArray(arr)) { 334 | return arr; 335 | } else if (Symbol.iterator in Object(arr)) { 336 | var _arr = []; 337 | 338 | for (var _iterator = arr[Symbol.iterator](), _step; !(_step = _iterator.next()).done;) { 339 | _arr.push(_step.value); 340 | 341 | if (i && _arr.length === i) break; 342 | } 343 | 344 | return _arr; 345 | } else { 346 | throw new TypeError("Invalid attempt to destructure non-iterable instance"); 347 | } 348 | }; 349 | 350 | babelHelpers.taggedTemplateLiteral = function (strings, raw) { 351 | return Object.freeze(Object.defineProperties(strings, { 352 | raw: { 353 | value: Object.freeze(raw) 354 | } 355 | })); 356 | }; 357 | 358 | babelHelpers.taggedTemplateLiteralLoose = function (strings, raw) { 359 | strings.raw = raw; 360 | return strings; 361 | }; 362 | 363 | babelHelpers.temporalRef = function (val, name, undef) { 364 | if (val === undef) { 365 | throw new ReferenceError(name + " is not defined - temporal dead zone"); 366 | } else { 367 | return val; 368 | } 369 | }; 370 | 371 | babelHelpers.temporalUndefined = {}; 372 | 373 | babelHelpers.toArray = function (arr) { 374 | return Array.isArray(arr) ? arr : Array.from(arr); 375 | }; 376 | 377 | babelHelpers.toConsumableArray = function (arr) { 378 | if (Array.isArray(arr)) { 379 | for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; 380 | 381 | return arr2; 382 | } else { 383 | return Array.from(arr); 384 | } 385 | }; 386 | }); 387 | -------------------------------------------------------------------------------- /examples/ballade-immutable-todomvc/src/js/babel-external-helpers.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { 2 | if (typeof define === "function" && define.amd) { 3 | define(["exports"], factory); 4 | } else if (typeof exports === "object") { 5 | factory(exports); 6 | } else { 7 | factory(root.babelHelpers = {}); 8 | } 9 | })(this, function (global) { 10 | var babelHelpers = global; 11 | babelHelpers.typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { 12 | return typeof obj; 13 | } : function (obj) { 14 | return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; 15 | }; 16 | 17 | babelHelpers.jsx = function () { 18 | var REACT_ELEMENT_TYPE = typeof Symbol === "function" && Symbol.for && Symbol.for("react.element") || 0xeac7; 19 | return function createRawReactElement(type, props, key, children) { 20 | var defaultProps = type && type.defaultProps; 21 | var childrenLength = arguments.length - 3; 22 | 23 | if (!props && childrenLength !== 0) { 24 | props = {}; 25 | } 26 | 27 | if (props && defaultProps) { 28 | for (var propName in defaultProps) { 29 | if (props[propName] === void 0) { 30 | props[propName] = defaultProps[propName]; 31 | } 32 | } 33 | } else if (!props) { 34 | props = defaultProps || {}; 35 | } 36 | 37 | if (childrenLength === 1) { 38 | props.children = children; 39 | } else if (childrenLength > 1) { 40 | var childArray = Array(childrenLength); 41 | 42 | for (var i = 0; i < childrenLength; i++) { 43 | childArray[i] = arguments[i + 3]; 44 | } 45 | 46 | props.children = childArray; 47 | } 48 | 49 | return { 50 | $$typeof: REACT_ELEMENT_TYPE, 51 | type: type, 52 | key: key === undefined ? null : '' + key, 53 | ref: null, 54 | props: props, 55 | _owner: null 56 | }; 57 | }; 58 | }(); 59 | 60 | babelHelpers.asyncToGenerator = function (fn) { 61 | return function () { 62 | var gen = fn.apply(this, arguments); 63 | return new Promise(function (resolve, reject) { 64 | function step(key, arg) { 65 | try { 66 | var info = gen[key](arg); 67 | var value = info.value; 68 | } catch (error) { 69 | reject(error); 70 | return; 71 | } 72 | 73 | if (info.done) { 74 | resolve(value); 75 | } else { 76 | return Promise.resolve(value).then(function (value) { 77 | return step("next", value); 78 | }, function (err) { 79 | return step("throw", err); 80 | }); 81 | } 82 | } 83 | 84 | return step("next"); 85 | }); 86 | }; 87 | }; 88 | 89 | babelHelpers.classCallCheck = function (instance, Constructor) { 90 | if (!(instance instanceof Constructor)) { 91 | throw new TypeError("Cannot call a class as a function"); 92 | } 93 | }; 94 | 95 | babelHelpers.createClass = function () { 96 | function defineProperties(target, props) { 97 | for (var i = 0; i < props.length; i++) { 98 | var descriptor = props[i]; 99 | descriptor.enumerable = descriptor.enumerable || false; 100 | descriptor.configurable = true; 101 | if ("value" in descriptor) descriptor.writable = true; 102 | Object.defineProperty(target, descriptor.key, descriptor); 103 | } 104 | } 105 | 106 | return function (Constructor, protoProps, staticProps) { 107 | if (protoProps) defineProperties(Constructor.prototype, protoProps); 108 | if (staticProps) defineProperties(Constructor, staticProps); 109 | return Constructor; 110 | }; 111 | }(); 112 | 113 | babelHelpers.defineEnumerableProperties = function (obj, descs) { 114 | for (var key in descs) { 115 | var desc = descs[key]; 116 | desc.configurable = desc.enumerable = true; 117 | if ("value" in desc) desc.writable = true; 118 | Object.defineProperty(obj, key, desc); 119 | } 120 | 121 | return obj; 122 | }; 123 | 124 | babelHelpers.defaults = function (obj, defaults) { 125 | var keys = Object.getOwnPropertyNames(defaults); 126 | 127 | for (var i = 0; i < keys.length; i++) { 128 | var key = keys[i]; 129 | var value = Object.getOwnPropertyDescriptor(defaults, key); 130 | 131 | if (value && value.configurable && obj[key] === undefined) { 132 | Object.defineProperty(obj, key, value); 133 | } 134 | } 135 | 136 | return obj; 137 | }; 138 | 139 | babelHelpers.defineProperty = function (obj, key, value) { 140 | if (key in obj) { 141 | Object.defineProperty(obj, key, { 142 | value: value, 143 | enumerable: true, 144 | configurable: true, 145 | writable: true 146 | }); 147 | } else { 148 | obj[key] = value; 149 | } 150 | 151 | return obj; 152 | }; 153 | 154 | babelHelpers.extends = Object.assign || function (target) { 155 | for (var i = 1; i < arguments.length; i++) { 156 | var source = arguments[i]; 157 | 158 | for (var key in source) { 159 | if (Object.prototype.hasOwnProperty.call(source, key)) { 160 | target[key] = source[key]; 161 | } 162 | } 163 | } 164 | 165 | return target; 166 | }; 167 | 168 | babelHelpers.get = function get(object, property, receiver) { 169 | if (object === null) object = Function.prototype; 170 | var desc = Object.getOwnPropertyDescriptor(object, property); 171 | 172 | if (desc === undefined) { 173 | var parent = Object.getPrototypeOf(object); 174 | 175 | if (parent === null) { 176 | return undefined; 177 | } else { 178 | return get(parent, property, receiver); 179 | } 180 | } else if ("value" in desc) { 181 | return desc.value; 182 | } else { 183 | var getter = desc.get; 184 | 185 | if (getter === undefined) { 186 | return undefined; 187 | } 188 | 189 | return getter.call(receiver); 190 | } 191 | }; 192 | 193 | babelHelpers.inherits = function (subClass, superClass) { 194 | if (typeof superClass !== "function" && superClass !== null) { 195 | throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); 196 | } 197 | 198 | subClass.prototype = Object.create(superClass && superClass.prototype, { 199 | constructor: { 200 | value: subClass, 201 | enumerable: false, 202 | writable: true, 203 | configurable: true 204 | } 205 | }); 206 | if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; 207 | }; 208 | 209 | babelHelpers.instanceof = function (left, right) { 210 | if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { 211 | return right[Symbol.hasInstance](left); 212 | } else { 213 | return left instanceof right; 214 | } 215 | }; 216 | 217 | babelHelpers.interopRequireDefault = function (obj) { 218 | return obj && obj.__esModule ? obj : { 219 | default: obj 220 | }; 221 | }; 222 | 223 | babelHelpers.interopRequireWildcard = function (obj) { 224 | if (obj && obj.__esModule) { 225 | return obj; 226 | } else { 227 | var newObj = {}; 228 | 229 | if (obj != null) { 230 | for (var key in obj) { 231 | if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; 232 | } 233 | } 234 | 235 | newObj.default = obj; 236 | return newObj; 237 | } 238 | }; 239 | 240 | babelHelpers.newArrowCheck = function (innerThis, boundThis) { 241 | if (innerThis !== boundThis) { 242 | throw new TypeError("Cannot instantiate an arrow function"); 243 | } 244 | }; 245 | 246 | babelHelpers.objectDestructuringEmpty = function (obj) { 247 | if (obj == null) throw new TypeError("Cannot destructure undefined"); 248 | }; 249 | 250 | babelHelpers.objectWithoutProperties = function (obj, keys) { 251 | var target = {}; 252 | 253 | for (var i in obj) { 254 | if (keys.indexOf(i) >= 0) continue; 255 | if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; 256 | target[i] = obj[i]; 257 | } 258 | 259 | return target; 260 | }; 261 | 262 | babelHelpers.possibleConstructorReturn = function (self, call) { 263 | if (!self) { 264 | throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); 265 | } 266 | 267 | return call && (typeof call === "object" || typeof call === "function") ? call : self; 268 | }; 269 | 270 | babelHelpers.selfGlobal = typeof global === "undefined" ? self : global; 271 | 272 | babelHelpers.set = function set(object, property, value, receiver) { 273 | var desc = Object.getOwnPropertyDescriptor(object, property); 274 | 275 | if (desc === undefined) { 276 | var parent = Object.getPrototypeOf(object); 277 | 278 | if (parent !== null) { 279 | set(parent, property, value, receiver); 280 | } 281 | } else if ("value" in desc && desc.writable) { 282 | desc.value = value; 283 | } else { 284 | var setter = desc.set; 285 | 286 | if (setter !== undefined) { 287 | setter.call(receiver, value); 288 | } 289 | } 290 | 291 | return value; 292 | }; 293 | 294 | babelHelpers.slicedToArray = function () { 295 | function sliceIterator(arr, i) { 296 | var _arr = []; 297 | var _n = true; 298 | var _d = false; 299 | var _e = undefined; 300 | 301 | try { 302 | for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { 303 | _arr.push(_s.value); 304 | 305 | if (i && _arr.length === i) break; 306 | } 307 | } catch (err) { 308 | _d = true; 309 | _e = err; 310 | } finally { 311 | try { 312 | if (!_n && _i["return"]) _i["return"](); 313 | } finally { 314 | if (_d) throw _e; 315 | } 316 | } 317 | 318 | return _arr; 319 | } 320 | 321 | return function (arr, i) { 322 | if (Array.isArray(arr)) { 323 | return arr; 324 | } else if (Symbol.iterator in Object(arr)) { 325 | return sliceIterator(arr, i); 326 | } else { 327 | throw new TypeError("Invalid attempt to destructure non-iterable instance"); 328 | } 329 | }; 330 | }(); 331 | 332 | babelHelpers.slicedToArrayLoose = function (arr, i) { 333 | if (Array.isArray(arr)) { 334 | return arr; 335 | } else if (Symbol.iterator in Object(arr)) { 336 | var _arr = []; 337 | 338 | for (var _iterator = arr[Symbol.iterator](), _step; !(_step = _iterator.next()).done;) { 339 | _arr.push(_step.value); 340 | 341 | if (i && _arr.length === i) break; 342 | } 343 | 344 | return _arr; 345 | } else { 346 | throw new TypeError("Invalid attempt to destructure non-iterable instance"); 347 | } 348 | }; 349 | 350 | babelHelpers.taggedTemplateLiteral = function (strings, raw) { 351 | return Object.freeze(Object.defineProperties(strings, { 352 | raw: { 353 | value: Object.freeze(raw) 354 | } 355 | })); 356 | }; 357 | 358 | babelHelpers.taggedTemplateLiteralLoose = function (strings, raw) { 359 | strings.raw = raw; 360 | return strings; 361 | }; 362 | 363 | babelHelpers.temporalRef = function (val, name, undef) { 364 | if (val === undef) { 365 | throw new ReferenceError(name + " is not defined - temporal dead zone"); 366 | } else { 367 | return val; 368 | } 369 | }; 370 | 371 | babelHelpers.temporalUndefined = {}; 372 | 373 | babelHelpers.toArray = function (arr) { 374 | return Array.isArray(arr) ? arr : Array.from(arr); 375 | }; 376 | 377 | babelHelpers.toConsumableArray = function (arr) { 378 | if (Array.isArray(arr)) { 379 | for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; 380 | 381 | return arr2; 382 | } else { 383 | return Array.from(arr); 384 | } 385 | }; 386 | }); 387 | -------------------------------------------------------------------------------- /test/schema-mutable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var Schema = require('../src/schema'); 5 | 6 | describe('Schema validator test mutable data', function () { 7 | describe('schema validator & cast', function () { 8 | var schema1 = new Schema({ 9 | str: String, 10 | num: Number, 11 | bol: Boolean, 12 | date: Date, 13 | strArr: [String], 14 | numArr: [Number], 15 | dateArr: [Date], 16 | objArr: [{ 17 | name: String, 18 | title: String 19 | }], 20 | anyArr: [], 21 | anyObj: {}, 22 | obj: { 23 | votes: Number, 24 | favs: Number, 25 | foo: { 26 | bar: String 27 | } 28 | } 29 | }); 30 | 31 | describe('schema type common validator', function () { 32 | it('all of data should vaild', function (done) { 33 | var date = new Date(); 34 | var dateStr = '2017-04-03T08:16:02.616Z'; 35 | var objArrResult = schema1.validator('objArr', [{name: 'Ballade', title: 'Ballade test'}]); 36 | var anyArrResult = schema1.validator('anyArr', [1, '2', false, {name: 'Ballade', title: 'Ballade test'}]); 37 | var anyObjResult = schema1.validator('anyObj', {foo: 'bar', bar: 'biz', age: 23}); 38 | var objResult = schema1.validator('obj', {votes: 2, favs: 100, foo: {bar: 'biz'}, redundant: 'Redundant'}); 39 | 40 | assert.strictEqual(schema1.validator('str', '1').value, '1'); 41 | assert.strictEqual(schema1.validator('num', 1).value, 1); 42 | assert.strictEqual(schema1.validator('bol', false).value, false); 43 | assert.strictEqual(schema1.validator('date', date).value, date); 44 | assert.strictEqual(schema1.validator('date', dateStr).value.getTime(), new Date(dateStr).getTime()); 45 | 46 | assert.deepStrictEqual(schema1.validator('strArr', ['1', 'hello', 'world']).value, ['1', 'hello', 'world']); 47 | assert.deepStrictEqual(schema1.validator('numArr', [3, 5, 7]).value, [3, 5, 7]); 48 | assert.deepStrictEqual(schema1.validator('dateArr', [date, date]).value, [date, date]); 49 | assert.deepStrictEqual(objArrResult.value, [{name: 'Ballade', title: 'Ballade test'}]); 50 | assert.deepStrictEqual(anyArrResult.value, [1, '2', false, {name: 'Ballade', title: 'Ballade test'}]); 51 | assert.deepStrictEqual(anyObjResult.value, {foo: 'bar', bar: 'biz', age: 23}); 52 | assert.deepStrictEqual(objResult.value, {votes: 2, favs: 100, foo: {bar: 'biz'}}); 53 | 54 | done(); 55 | }); 56 | }); 57 | 58 | describe('schema type cast validator', function () { 59 | it('data is valid, but will throw warning', function (done) { 60 | var strResult = schema1.validator('str', 1); 61 | var numResult = schema1.validator('num', '2') 62 | 63 | assert.strictEqual(strResult.value, '1'); 64 | assert.strictEqual(strResult.messages[0].path, 'str'); 65 | assert.strictEqual(strResult.messages[0].originalValue, 1); 66 | assert.strictEqual(strResult.messages[0].type, 'warning'); 67 | 68 | assert.strictEqual(numResult.value, 2); 69 | assert.strictEqual(numResult.messages[0].path, 'num'); 70 | assert.strictEqual(numResult.messages[0].originalValue, '2'); 71 | assert.strictEqual(numResult.messages[0].type, 'warning'); 72 | 73 | done(); 74 | }); 75 | 76 | it('data is invalid, will throw error', function (done) { 77 | var numResult = schema1.validator('num', 'hello'); 78 | var strResult = schema1.validator('str', {foo: 'bar'}); 79 | 80 | assert.strictEqual(numResult.messages[0].path, 'num'); 81 | assert.strictEqual(numResult.messages[0].originalValue, 'hello'); 82 | assert.strictEqual(numResult.messages[0].type, 'error'); 83 | 84 | assert.strictEqual(strResult.messages[0].path, 'str'); 85 | assert.deepStrictEqual(strResult.messages[0].originalValue, {foo: 'bar'}); 86 | assert.strictEqual(strResult.messages[0].type, 'error'); 87 | 88 | done(); 89 | }); 90 | }); 91 | 92 | describe('schema is Mixed', function () { 93 | it('data type is Mixed, and default data is empty', function (done) { 94 | assert.strictEqual(schema1.dataTypes.anyArr.__schemaType__, 'Mixed'); 95 | assert.strictEqual(schema1.dataTypes.anyObj.__schemaType__, 'Mixed'); 96 | assert.strictEqual(Array.isArray(schema1.defaultData.anyArr), true); 97 | assert.strictEqual(Object.keys(schema1.defaultData.anyObj).length, 0); 98 | done(); 99 | }); 100 | }); 101 | }); 102 | 103 | describe('schema options', function () { 104 | var schema2 = new Schema({ 105 | str1: { 106 | $type: String, 107 | $lowercase: true 108 | }, 109 | str2: { 110 | $type: String, 111 | $uppercase: true, 112 | $trim: true 113 | }, 114 | str3: { 115 | $type: String, 116 | $match: /abcd\w+/ 117 | }, 118 | str4: { 119 | $type: String, 120 | $lowercase: true, 121 | $enum: ['js', 'javascript'] 122 | }, 123 | num1: { 124 | $type: Number, 125 | $min: 0 126 | }, 127 | num2: { 128 | $type: Number, 129 | $max: 10 130 | }, 131 | num3: { 132 | $type: Number, 133 | $min: 5, 134 | $max: 10 135 | }, 136 | bol: { 137 | $type: Boolean 138 | }, 139 | foo: { 140 | $type: String, 141 | $required: true 142 | }, 143 | bar: { 144 | biz: { 145 | $type: String, 146 | $required: true 147 | }, 148 | count: Number 149 | }, 150 | obj: { 151 | votes: { 152 | $type: Number, 153 | $default: 1 154 | }, 155 | favs: Number 156 | } 157 | }); 158 | 159 | it('string type data is correct convert', function (done) { 160 | assert.strictEqual(schema2.validator('str1', 'Ballade').value, 'ballade'); 161 | assert.strictEqual(schema2.validator('str2', 'ballade ').value, 'BALLADE'); 162 | assert.strictEqual(schema2.validator('str3', 'abcdefg').value, 'abcdefg'); 163 | assert.strictEqual(schema2.validator('str4', 'js').value, 'js'); 164 | assert.strictEqual(schema2.validator('str4', 'java').value, undefined); 165 | assert.strictEqual(schema2.validator('str4', 'javascript').value, 'javascript'); 166 | assert.strictEqual(schema2.validator('str4', 'JavaScript').value, 'javascript'); 167 | 168 | done(); 169 | }); 170 | 171 | it('number type data is correct convert', function (done) { 172 | assert.strictEqual(schema2.validator('num1', 5).value, 5); 173 | assert.strictEqual(schema2.validator('num2', '5').value, 5); 174 | assert.strictEqual(schema2.validator('num3', 8).value, 8); 175 | assert.strictEqual(schema2.validator('num3', 4).value, undefined); 176 | done(); 177 | }); 178 | 179 | it('boolean type data valid', function (done) { 180 | assert.strictEqual(schema2.validator('bol', false).value, false); 181 | done(); 182 | }); 183 | 184 | it('required option is work', function (done) { 185 | var result1 = schema2.validator('bar', { 186 | count: 100 187 | }); 188 | 189 | var result2 = schema2.validator('bar', { 190 | count: 100, 191 | biz: 'biz' 192 | }); 193 | 194 | assert.strictEqual(schema2.validator('foo', 'bar').value, 'bar'); 195 | assert.strictEqual(result1.value.count, 100); 196 | assert.strictEqual(result1.messages[0].path, 'bar.biz'); 197 | assert.strictEqual(result1.messages[0].originalValue, undefined); 198 | assert.strictEqual(result1.messages[0].type, 'error'); 199 | assert.strictEqual(result2.value.biz, 'biz'); 200 | 201 | done(); 202 | }); 203 | 204 | it('default option is work', function (done) { 205 | var result1 = schema2.validator('obj', { 206 | favs: 1 207 | }); 208 | 209 | var result2 = schema2.validator('obj', { 210 | votes: 2, 211 | favs: 2 212 | }); 213 | 214 | assert.strictEqual(result1.value.favs, 1); 215 | assert.strictEqual(result1.value.votes, 1); 216 | assert.strictEqual(result2.value.favs, 2); 217 | assert.strictEqual(result2.value.votes, 2); 218 | 219 | done(); 220 | }); 221 | }); 222 | 223 | describe('schema nested', function () { 224 | var schema3 = new Schema({ 225 | foo: { 226 | bar: { 227 | $type: String, 228 | $default: 'bar' 229 | }, 230 | biz: String 231 | }, 232 | count: Number 233 | }); 234 | 235 | var schema4 = new Schema({ 236 | arr: [schema3] 237 | }); 238 | 239 | var schema5 = new Schema({ 240 | str: { 241 | $type: String, 242 | $uppercase: true 243 | }, 244 | num: Number 245 | }); 246 | 247 | var schema6 = new Schema({ 248 | meta: schema5 249 | }); 250 | 251 | it('child schema is work', function (done) { 252 | var result = schema4.validator('arr', [{ 253 | foo: { 254 | biz: 'biz' 255 | }, 256 | count: 100 257 | }, { 258 | foo: { 259 | bar: 'hello', 260 | biz: 1 261 | }, 262 | count: '122' 263 | }]); 264 | 265 | assert.strictEqual(result.value[0].foo.bar, 'bar'); 266 | assert.strictEqual(result.value[0].foo.biz, 'biz'); 267 | assert.strictEqual(result.value[0].count, 100); 268 | 269 | assert.strictEqual(result.value[1].foo.bar, 'hello'); 270 | assert.strictEqual(result.value[1].foo.biz, '1'); 271 | assert.strictEqual(result.value[1].count, 122); 272 | 273 | assert.strictEqual(result.messages[0].path, 'arr[1].foo.biz'); 274 | assert.strictEqual(result.messages[0].originalValue, 1); 275 | assert.strictEqual(result.messages[0].type, 'warning'); 276 | 277 | assert.strictEqual(result.messages[1].path, 'arr[1].count'); 278 | assert.strictEqual(result.messages[1].originalValue, '122'); 279 | assert.strictEqual(result.messages[1].type, 'warning'); 280 | 281 | var result2 = schema6.validator('meta', { 282 | str: 'hello', 283 | num: 100 284 | }); 285 | 286 | assert.strictEqual(result2.value.str, 'HELLO'); 287 | assert.strictEqual(result2.value.num, 100); 288 | assert.strictEqual(result2.messages, undefined); 289 | 290 | done(); 291 | }); 292 | }); 293 | 294 | describe('schema default data', function () { 295 | it('schema has default data', function (done) { 296 | var todoSchema = new Schema({ 297 | id: { 298 | $type: String, 299 | $default: (+new Date() + Math.floor(Math.random() * 999999)).toString(36) 300 | }, 301 | complete: { 302 | $type: Boolean, 303 | $default: false 304 | }, 305 | text: { 306 | $type: String, 307 | $default: "Ballade Getting Started" 308 | } 309 | }); 310 | 311 | var todosSchema = new Schema({ 312 | todos: [todoSchema] 313 | }); 314 | 315 | var todoDefault = todosSchema.defaultData.todos[0]; 316 | 317 | assert.strictEqual(typeof todoDefault.id, 'string'); 318 | assert.strictEqual(todoDefault.complete, false); 319 | assert.strictEqual(todoDefault.text, "Ballade Getting Started"); 320 | done(); 321 | }); 322 | }); 323 | }); 324 | -------------------------------------------------------------------------------- /dist/ballade.min.js: -------------------------------------------------------------------------------- 1 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var t;t="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,t.Ballade=e()}}(function(){return function e(t,r,n){function s(a,o){if(!r[a]){if(!t[a]){var c="function"==typeof require&&require;if(!o&&c)return c(a,!0);if(i)return i(a,!0);var u=new Error("Cannot find module '"+a+"'");throw u.code="MODULE_NOT_FOUND",u}var l=r[a]={exports:{}};t[a][0].call(l.exports,function(e){var r=t[a][1][e];return s(r?r:e)},l,l.exports,e,t,r,n)}return r[a].exports}for(var i="function"==typeof require&&require,a=0;a-1;i--)if(r=n[i],s(r,a,t)===o)return r},"delete":function(e,t){var r,n=this.cacheStore,i=n.length-1,a=this.id,o=e;if(this.idKeys[o])for(;i>-1;i--)if(r=n[i],s(r,a,t)===o){n.splice(i,1),delete this.idKeys[o];break}}},t.exports=a},{"./accessor":1}],5:[function(e,t,r){function n(e,t,r){if(!(e instanceof Object))return e;var s,i=Object.prototype.toString.call(e).slice(8,-1);switch(i){case"Array":s=[];break;case"Date":s=new Date(e.getTime());break;case"RegExp":s=new RegExp(e);break;case"Function":break;case"Uint8Array":case"Uint8ClampedArray":case"Uint16Array":case"Uint32Array":case"Int8Array":case"Int16Array":case"Int32Array":case"Float32Array":case"Float64Array":s=e.subarray();break;default:s={}}if(t.push(e),r.push(s),e instanceof Array)for(var a=0;a0&&l===c[0])c.shift();else if(Object.prototype.hasOwnProperty.call(e,l)){var h=e[l],f=t.indexOf(h);s[l]=-1!==f?r[f]:n(e[l],t,r)}}return s}t.exports=function(e){return n(e,[],[])}},{}],6:[function(e,t,r){"use strict";var n=function(){this.handlers=[]};n.prototype={publish:function(e,t){this.handlers.forEach(function(r){r.type===e&&r.handler(t)})},subscribe:function(e,t){this.handlers.push({type:e,handler:t})},unsubscribe:function(e,t){"function"==typeof e&&(t=e,e=null);for(var r,n=0,s=!1;n=t?e:void 0},$max:function(e,t){return t>=e?e:void 0}}};y.Date=y.Number;var m=function(e,t,r){var n={};if(null===t||void 0===t)return n;try{"Date"!==r[o]?(n.message={path:e,originalValue:t,type:"warning",message:"Expect type is "+r[o]+", not "+d(t)},n.value=r[h](t)):n.value=new Date(t)}catch(s){n.message={path:e,originalValue:t,type:"error",message:"Cast to "+r[o]+" failed, "+s.message}}return"Number"===r[o]?isFinite(n.value)||(n.message={path:e,originalValue:t,type:"error",message:"Cast to "+r[o]+" failed"},delete n.value):"String"===r[o]&&"object"==typeof t&&(n.message={path:e,originalValue:t,type:"error",message:"Cast to "+r[o]+" failed"},delete n.value),n},v=function(e,t,r){var n=Array.isArray(e),s=n?e.slice(0,1):Object.keys(e);s.forEach(function(s){var i=n?s:e[s];if(s=n?l:s,t[s]={},"function"==typeof i){if("Array"===i.name||"Object"===i.name)return t[s][o]="Mixed",void("Array"===i.name?r[s]=[]:r[s]={});t[s][o]=i.name,t[s][h]=i}else if(Array.isArray(i)){if(!i.length)return t[s][o]="Mixed",void(r[s]=[]);t[s][u]="Array",r[s]=[],v(i,t[s],r[s])}else{if("Object"!==d(i))throw new Error("Set `"+s+"` schema error, may be forget set `$type` property or `type Constructor Function`");if(!Object.keys(i).length)return t[s][o]="Mixed",void(r[s]={});if("function"==typeof i.$type){if("Array"===i.$type.name||"Object"===i.$type.name)return t[s][o]="Mixed",void("Array"===i.$type.name?r[s]=[]:r[s]={});t[s][o]=i.$type.name,t[s][h]=i.$type,t[s][c]=[],Object.keys(i).forEach(function(e){"$type"===e||"$default"!==e&&!i[e]||(t[s][c].push({key:e,value:i[e]}),"$default"===e&&("Function"===d(i[e])?s===l?r.push(i.$default()):r[s]=i.$default():s===l?r.push(i.$default):r[s]=i.$default))}),t[s][c].length||delete t[s][c]}else i instanceof k?(t[s][o]="Schema",t[s][f]=i,s===l?Object.keys(i.defaultData).length&&r.push(i.defaultData):r[s]=i.defaultData):(t[s][u]="Object",r[s]={},v(i,t[s],r[s]))}})},g=function(e,t){var r={},n=t[o];return t[c].forEach(function(t){if(!r.message){var s=t.key,i=t.value,a=y[s]||y[n][s];a&&(e=a(e,i),void 0===e&&(r.message="Value convert faild for `"+s+"` schema options"))}}),r.value=e,r},b=function(e,t,r,n){var l,h={},f=[],p=this,y=0;return Object.keys(t).forEach(function(l){if("__schema"!==l.slice(0,8)){y++;var h,v,b,_=t[l],w=i(e,l,n),k=r+"."+l;_[u]?(v=p.validator(l,w,n,_,k),"value"in v&&(e=s(e,l,v.value,n)),v.messages&&(f=f.concat(v.messages))):(void 0!==w&&"Mixed"!==_[o]&&d(w)!==_[o]&&(v=m(k,w,_),e="value"in v?s(e,l,v.value,n):a(e,l,n),v.message&&f.push(v.message)),_[c]&&(b=i(e,l,n),h=g(b,_),e=s(e,l,h.value,n),h.message&&(f.push({path:k,originalValue:b,type:"error",message:h.message}),e=a(e,l,n))))}}),n?e.forEach(function(r,n){n in t||(e=a(e,n,!0))}):(l=Object.keys(e),l.length>y&&l.forEach(function(r){r in t||delete e[r]}),l=null),h.value=e,f.length&&(h.messages=f),h},_=function(e,t,r,n){var h={},p=[],y=this,v=t[l],_=v[u],w=v[o],k=v[f],S=v[c];return e.forEach(function(t,o){var c,u,h,f,x=!0,O=r+"["+o+"]";_?(u=y.validator(l,t,n,v,O),e=s(e,o,u.value,n),u.messages&&(p=p.concat(u.messages))):"Schema"===w?(f=b.call(y,t,k.dataTypes,O,n),"value"in f&&(e=s(e,o,f.value,n)),"messages"in f&&(p=p.concat(f.messages))):(d(t)!==w&&(u=m(O,t,v),"value"in u?e=s(e,o,u.value,n):(a(e,o,n),x=!1),u.message&&p.push(u.message)),x&&S&&(h=i(e,o,n),c=g(h,v),e=s(e,o,c.value,n),c.message&&(p.push({path:O,originalValue:h,type:"error",message:c.message}),a(e,o,n))))}),h.value=e,p.length&&(h.messages=p),h},w=function(e,t,r){var n,s,i={},a=[],u=d(e);return t[o]===u?i.value=e:(s=m(r,e,t),"value"in s&&(i.value=s.value),s.message&&a.push(s.message)),"value"in i&&t[c]&&(n=g(i.value,t),i.value=n.value,n.message&&(a.push({path:r,originalValue:i.value,type:"error",message:n.message}),delete i.value)),a.length&&(i.messages=a),i},k=function(e){if("Object"!==d(e))throw new Error("Schema type must be plain Object");this.dataTypes={},this.defaultData={},v(e,this.dataTypes,this.defaultData)};k.prototype={validator:function(e,t,r,n,s){if(n=n||this.dataTypes[e],s=s||e,void 0===t)return{};var i=n[u],a=d(t,r);return n?"Mixed"===n[o]?{value:t}:"Schema"===n[o]?b.call(this,t,n[f].dataTypes,s,r):"Array"===i&&"Array"===a?_.call(this,t,n,s,r):"Object"===i&&"Object"===a?b.call(this,t,n,s,r):w.call(this,t,n,s):[{path:s,originalValue:t,type:"error",message:"Not declared in Schema"}]}},t.exports=k},{"./accessor":1}],10:[function(e,t,r){"use strict";var n=Object.prototype.toString,s=e("./copy"),i=e("./event"),a=e("./cache"),o=e("./persistence"),c={string:!0,number:!0,"null":!0,undefind:!0,"boolean":!0},u=function(e){return n.call(e).slice(8,-1)},l=function(e,t){var r=u(e);return"Array"===r||"Object"===r?t.fromJS(e):e},h=function(e,t,r){i.call(this),t=t||{};var n=e.defaultData,s=t.cache,c=this;this.store={},this.cache={},this.schema=e,this.Immutable=r,this.options=t,this.id="BalladeStore-"+(+new Date+Math.floor(999999*Math.random())).toString(36),Object.keys(e.dataTypes).forEach(function(e){var t,i=s&&e in s,u=!1;i&&s[e].id&&(c.cache[e]=new a(s[e]),u=!0),i&&s[e].persistence&&(t=o.get(s[e].persistence.prefix+"."+e,s[e].persistence.type)),(null===t||void 0===t)&&(t=n[e]),null!==t&&void 0!==t&&(r&&(t=l(t,r)),u?c.cache[e].set(t,!!r):c.store[e]=t)})};h.prototype=Object.create(i.prototype,{constructor:{value:h,enumerable:!1,writable:!0,configurable:!0}}),h.prototype.set=function(e,t,r){var n,s=this.options,i=s.cache,a=this.Immutable&&"Function"===u(t.toJS),c=this.schema.validator(e,t,a),h=[];return c.messages&&(c.messages.forEach(function(e){"warning"===e.type||"error"===e.type&&h.push(e)}),s&&s.error&&s.error({key:e,type:"SCHEMA_VALIDATION_ERROR",messages:h},this)),"value"in c?(n=this.Immutable?a?c.value:l(c.value,this.Immutable):c.value,e in this.cache?this.cache[e].set(n,!!this.Immutable):this.store[e]=n,i&&i[e]&&i[e].persistence&&o.set(i[e].persistence.prefix+"."+e,n,i[e].persistence.type),r||this.publish(e,n),e):void 0},h.prototype.get=function(e,t){var r,n,i=!!this.Immutable;return r=e in this.cache?void 0!==t?this.cache[e].get(t,i):this.cache[e].cacheStore:this.store[e],i?r:(n=typeof r,c[n]?r:s(r))},h.prototype["delete"]=function(e,t){var r=this.options.cache;return t&&e in this.cache?this.cache[e]["delete"](t,!!this.Immutable):delete this.store[e],r&&r[e]&&r[e].persistence&&o["delete"](r[e].persistence.prefix+"."+e,r[e].persistence.type),this.publish(e,this.get(e,t)),e},h.prototype.cleanCache=function(e){var t=this.cache[e];return t&&(t.cacheStore.length=0,Object.keys(t.idKeys).forEach(function(e){delete t.idKeys[e]})),e},t.exports=h},{"./cache":4,"./copy":5,"./event":6,"./persistence":7}]},{},[2])(2)}); -------------------------------------------------------------------------------- /dist/ballade.immutable.min.js: -------------------------------------------------------------------------------- 1 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var t;t="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,t.Ballade=e()}}(function(){return function e(t,r,n){function s(i,o){if(!r[i]){if(!t[i]){var u="function"==typeof require&&require;if(!o&&u)return u(i,!0);if(a)return a(i,!0);var c=new Error("Cannot find module '"+i+"'");throw c.code="MODULE_NOT_FOUND",c}var l=r[i]={exports:{}};t[i][0].call(l.exports,function(e){var r=t[i][1][e];return s(r?r:e)},l,l.exports,e,t,r,n)}return r[i].exports}for(var a="function"==typeof require&&require,i=0;i-1;a--)if(r=n[a],s(r,i,t)===o)return r},"delete":function(e,t){var r,n=this.cacheStore,a=n.length-1,i=this.id,o=e;if(this.idKeys[o])for(;a>-1;a--)if(r=n[a],s(r,i,t)===o){n.splice(a,1),delete this.idKeys[o];break}}},t.exports=i},{"./accessor":2}],6:[function(e,t,r){function n(e,t,r){if(!(e instanceof Object))return e;var s,a=Object.prototype.toString.call(e).slice(8,-1);switch(a){case"Array":s=[];break;case"Date":s=new Date(e.getTime());break;case"RegExp":s=new RegExp(e);break;case"Function":break;case"Uint8Array":case"Uint8ClampedArray":case"Uint16Array":case"Uint32Array":case"Int8Array":case"Int16Array":case"Int32Array":case"Float32Array":case"Float64Array":s=e.subarray();break;default:s={}}if(t.push(e),r.push(s),e instanceof Array)for(var i=0;i0&&l===u[0])u.shift();else if(Object.prototype.hasOwnProperty.call(e,l)){var f=e[l],h=t.indexOf(f);s[l]=-1!==h?r[h]:n(e[l],t,r)}}return s}t.exports=function(e){return n(e,[],[])}},{}],7:[function(e,t,r){"use strict";var n=function(){this.handlers=[]};n.prototype={publish:function(e,t){this.handlers.forEach(function(r){r.type===e&&r.handler(t)})},subscribe:function(e,t){this.handlers.push({type:e,handler:t})},unsubscribe:function(e,t){"function"==typeof e&&(t=e,e=null);for(var r,n=0,s=!1;n=t?e:void 0},$max:function(e,t){return t>=e?e:void 0}}};m.Date=m.Number;var y=function(e,t,r){var n={};if(null===t||void 0===t)return n;try{"Date"!==r[o]?(n.message={path:e,originalValue:t,type:"warning",message:"Expect type is "+r[o]+", not "+d(t)},n.value=r[f](t)):n.value=new Date(t)}catch(s){n.message={path:e,originalValue:t,type:"error",message:"Cast to "+r[o]+" failed, "+s.message}}return"Number"===r[o]?isFinite(n.value)||(n.message={path:e,originalValue:t,type:"error",message:"Cast to "+r[o]+" failed"},delete n.value):"String"===r[o]&&"object"==typeof t&&(n.message={path:e,originalValue:t,type:"error",message:"Cast to "+r[o]+" failed"},delete n.value),n},b=function(e,t,r){var n=Array.isArray(e),s=n?e.slice(0,1):Object.keys(e);s.forEach(function(s){var a=n?s:e[s];if(s=n?l:s,t[s]={},"function"==typeof a){if("Array"===a.name||"Object"===a.name)return t[s][o]="Mixed",void("Array"===a.name?r[s]=[]:r[s]={});t[s][o]=a.name,t[s][f]=a}else if(Array.isArray(a)){if(!a.length)return t[s][o]="Mixed",void(r[s]=[]);t[s][c]="Array",r[s]=[],b(a,t[s],r[s])}else{if("Object"!==d(a))throw new Error("Set `"+s+"` schema error, may be forget set `$type` property or `type Constructor Function`");if(!Object.keys(a).length)return t[s][o]="Mixed",void(r[s]={});if("function"==typeof a.$type){if("Array"===a.$type.name||"Object"===a.$type.name)return t[s][o]="Mixed",void("Array"===a.$type.name?r[s]=[]:r[s]={});t[s][o]=a.$type.name,t[s][f]=a.$type,t[s][u]=[],Object.keys(a).forEach(function(e){"$type"===e||"$default"!==e&&!a[e]||(t[s][u].push({key:e,value:a[e]}),"$default"===e&&("Function"===d(a[e])?s===l?r.push(a.$default()):r[s]=a.$default():s===l?r.push(a.$default):r[s]=a.$default))}),t[s][u].length||delete t[s][u]}else a instanceof k?(t[s][o]="Schema",t[s][h]=a,s===l?Object.keys(a.defaultData).length&&r.push(a.defaultData):r[s]=a.defaultData):(t[s][c]="Object",r[s]={},b(a,t[s],r[s]))}})},g=function(e,t){var r={},n=t[o];return t[u].forEach(function(t){if(!r.message){var s=t.key,a=t.value,i=m[s]||m[n][s];i&&(e=i(e,a),void 0===e&&(r.message="Value convert faild for `"+s+"` schema options"))}}),r.value=e,r},v=function(e,t,r,n){var l,f={},h=[],p=this,m=0;return Object.keys(t).forEach(function(l){if("__schema"!==l.slice(0,8)){m++;var f,b,v,_=t[l],w=a(e,l,n),k=r+"."+l;_[c]?(b=p.validator(l,w,n,_,k),"value"in b&&(e=s(e,l,b.value,n)),b.messages&&(h=h.concat(b.messages))):(void 0!==w&&"Mixed"!==_[o]&&d(w)!==_[o]&&(b=y(k,w,_),e="value"in b?s(e,l,b.value,n):i(e,l,n),b.message&&h.push(b.message)),_[u]&&(v=a(e,l,n),f=g(v,_),e=s(e,l,f.value,n),f.message&&(h.push({path:k,originalValue:v,type:"error",message:f.message}),e=i(e,l,n))))}}),n?e.forEach(function(r,n){n in t||(e=i(e,n,!0))}):(l=Object.keys(e),l.length>m&&l.forEach(function(r){r in t||delete e[r]}),l=null),f.value=e,h.length&&(f.messages=h),f},_=function(e,t,r,n){var f={},p=[],m=this,b=t[l],_=b[c],w=b[o],k=b[h],S=b[u];return e.forEach(function(t,o){var u,c,f,h,x=!0,O=r+"["+o+"]";_?(c=m.validator(l,t,n,b,O),e=s(e,o,c.value,n),c.messages&&(p=p.concat(c.messages))):"Schema"===w?(h=v.call(m,t,k.dataTypes,O,n),"value"in h&&(e=s(e,o,h.value,n)),"messages"in h&&(p=p.concat(h.messages))):(d(t)!==w&&(c=y(O,t,b),"value"in c?e=s(e,o,c.value,n):(i(e,o,n),x=!1),c.message&&p.push(c.message)),x&&S&&(f=a(e,o,n),u=g(f,b),e=s(e,o,u.value,n),u.message&&(p.push({path:O,originalValue:f,type:"error",message:u.message}),i(e,o,n))))}),f.value=e,p.length&&(f.messages=p),f},w=function(e,t,r){var n,s,a={},i=[],c=d(e);return t[o]===c?a.value=e:(s=y(r,e,t),"value"in s&&(a.value=s.value),s.message&&i.push(s.message)),"value"in a&&t[u]&&(n=g(a.value,t),a.value=n.value,n.message&&(i.push({path:r,originalValue:a.value,type:"error",message:n.message}),delete a.value)),i.length&&(a.messages=i),a},k=function(e){if("Object"!==d(e))throw new Error("Schema type must be plain Object");this.dataTypes={},this.defaultData={},b(e,this.dataTypes,this.defaultData)};k.prototype={validator:function(e,t,r,n,s){if(n=n||this.dataTypes[e],s=s||e,void 0===t)return{};var a=n[c],i=d(t,r);return n?"Mixed"===n[o]?{value:t}:"Schema"===n[o]?v.call(this,t,n[h].dataTypes,s,r):"Array"===a&&"Array"===i?_.call(this,t,n,s,r):"Object"===a&&"Object"===i?v.call(this,t,n,s,r):w.call(this,t,n,s):[{path:s,originalValue:t,type:"error",message:"Not declared in Schema"}]}},t.exports=k},{"./accessor":2}],13:[function(e,t,r){"use strict";var n=Object.prototype.toString,s=e("./copy"),a=e("./event"),i=e("./cache"),o=e("./persistence"),u={string:!0,number:!0,"null":!0,undefind:!0,"boolean":!0},c=function(e){return n.call(e).slice(8,-1)},l=function(e,t){var r=c(e);return"Array"===r||"Object"===r?t.fromJS(e):e},f=function(e,t,r){a.call(this),t=t||{};var n=e.defaultData,s=t.cache,u=this;this.store={},this.cache={},this.schema=e,this.Immutable=r,this.options=t,this.id="BalladeStore-"+(+new Date+Math.floor(999999*Math.random())).toString(36),Object.keys(e.dataTypes).forEach(function(e){var t,a=s&&e in s,c=!1;a&&s[e].id&&(u.cache[e]=new i(s[e]),c=!0),a&&s[e].persistence&&(t=o.get(s[e].persistence.prefix+"."+e,s[e].persistence.type)),(null===t||void 0===t)&&(t=n[e]),null!==t&&void 0!==t&&(r&&(t=l(t,r)),c?u.cache[e].set(t,!!r):u.store[e]=t)})};f.prototype=Object.create(a.prototype,{constructor:{value:f,enumerable:!1,writable:!0,configurable:!0}}),f.prototype.set=function(e,t,r){var n,s=this.options,a=s.cache,i=this.Immutable&&"Function"===c(t.toJS),u=this.schema.validator(e,t,i),f=[];return u.messages&&(u.messages.forEach(function(e){"warning"===e.type||"error"===e.type&&f.push(e)}),s&&s.error&&s.error({key:e,type:"SCHEMA_VALIDATION_ERROR",messages:f},this)),"value"in u?(n=this.Immutable?i?u.value:l(u.value,this.Immutable):u.value,e in this.cache?this.cache[e].set(n,!!this.Immutable):this.store[e]=n,a&&a[e]&&a[e].persistence&&o.set(a[e].persistence.prefix+"."+e,n,a[e].persistence.type),r||this.publish(e,n),e):void 0},f.prototype.get=function(e,t){var r,n,a=!!this.Immutable;return r=e in this.cache?void 0!==t?this.cache[e].get(t,a):this.cache[e].cacheStore:this.store[e],a?r:(n=typeof r,u[n]?r:s(r))},f.prototype["delete"]=function(e,t){var r=this.options.cache;return t&&e in this.cache?this.cache[e]["delete"](t,!!this.Immutable):delete this.store[e],r&&r[e]&&r[e].persistence&&o["delete"](r[e].persistence.prefix+"."+e,r[e].persistence.type),this.publish(e,this.get(e,t)),e},f.prototype.cleanCache=function(e){var t=this.cache[e];return t&&(t.cacheStore.length=0,Object.keys(t.idKeys).forEach(function(e){delete t.idKeys[e]})),e},t.exports=f},{"./cache":5,"./copy":6,"./event":7,"./persistence":10}]},{},[3])(3)}); --------------------------------------------------------------------------------