├── .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 |
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 |
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 |
--------------------------------------------------------------------------------
/examples/ballade-mutable-todomvc/src/fonts/iconfont.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |
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 |
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)});
--------------------------------------------------------------------------------