├── examples └── todomvc │ ├── asset │ ├── css │ │ └── app.css │ ├── jsx │ │ └── index.jsx │ └── js │ │ └── index.js │ ├── README.md │ ├── index.html │ ├── package.json │ └── Gruntfile.coffee ├── asset └── delorean-logo.png ├── src ├── delorean.coffee ├── store.coffee ├── flux.coffee ├── dispatcher.coffee └── mixin.coffee ├── package.json ├── bower.json ├── .gitignore ├── Gruntfile.coffee ├── README.md └── dist ├── delorean.min.js └── delorean.js /examples/todomvc/asset/css/app.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /asset/delorean-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krasimir/delorean/master/asset/delorean-logo.png -------------------------------------------------------------------------------- /examples/todomvc/README.md: -------------------------------------------------------------------------------- 1 | # DeLorean.js TodoMVC Example 2 | 3 | ```bash 4 | grunt 5 | open index.html 6 | ``` 7 | -------------------------------------------------------------------------------- /src/delorean.coffee: -------------------------------------------------------------------------------- 1 | # Exporting flux as `DeLorean.Flux` 2 | # This file is the entry point of the library. 3 | DeLorean = 4 | Flux: require './flux.coffee' 5 | 6 | module.exports = DeLorean 7 | -------------------------------------------------------------------------------- /examples/todomvc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React + Flux with DeLorean.js 6 | 7 | 8 | Add Random Todo 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/todomvc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "delorean-todomvc", 3 | "version": "0.0.0", 4 | "description": "DeLorean.js TodoMVC Example", 5 | "author": "Fatih Kadir Akin", 6 | "license": "MIT", 7 | "devDependencies": { 8 | "grunt": "^0.4.5", 9 | "grunt-browserify": "^2.1.4", 10 | "grunt-react": "^0.9.0" 11 | }, 12 | "dependencies": { 13 | "director": "^1.2.3", 14 | "react": "^0.11.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/todomvc/Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt)-> 2 | 3 | grunt.loadNpmTasks 'grunt-react' 4 | grunt.loadNpmTasks 'grunt-browserify' 5 | 6 | grunt.initConfig 7 | 8 | react: 9 | example: 10 | files: 11 | 'asset/js/index.js': 'asset/jsx/index.jsx' 12 | 13 | browserify: 14 | example: 15 | files: 16 | 'asset/js/app.js': ['asset/js/index.js'] 17 | 18 | grunt.registerTask 'default', ['react:example', 'browserify:example'] 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "delorean.js", 3 | "version": "0.2.4", 4 | "description": "Flux Library", 5 | "main": "dist/delorean.min.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Fatih Kadir Akin", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "coffeeify": "^0.7.0", 13 | "grunt": "^0.4.5", 14 | "grunt-browserify": "^2.1.4", 15 | "grunt-contrib-uglify": "^0.5.1", 16 | "grunt-react": "^0.9.0" 17 | }, 18 | "dependencies": { 19 | "es6-promise": "^1.0.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "delorean", 3 | "version": "0.0.0", 4 | "homepage": "https://github.com/f/delorean", 5 | "authors": [ 6 | "f " 7 | ], 8 | "description": "Flux library", 9 | "main": "dist/delorean.min.js", 10 | "moduleType": [ 11 | "node" 12 | ], 13 | "keywords": [ 14 | "flux", 15 | "delorean", 16 | "react" 17 | ], 18 | "license": "MIT", 19 | "ignore": [ 20 | "**/.*", 21 | "node_modules", 22 | "bower_components", 23 | "test", 24 | "tests", 25 | "**/*.coffee", 26 | "src", 27 | "example" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /src/store.coffee: -------------------------------------------------------------------------------- 1 | {EventEmitter} = require 'events' 2 | 3 | # Stores are simple observable structures. 4 | class Store extends EventEmitter 5 | 6 | constructor: (@store)-> 7 | super 8 | @bindActions store.actions 9 | 10 | bindActions: (actions)-> 11 | @store.emit = @emit.bind this 12 | 13 | for own actionName, callback of actions 14 | @on "action:#{actionName}", @store[callback].bind @store 15 | 16 | dispatchAction: (actionName, data)-> 17 | @emit "action:#{actionName}", data 18 | 19 | onChange: (callback)-> 20 | @on 'change', callback 21 | 22 | module.exports = Store 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Deployed apps should consider commenting this line out: 24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 25 | node_modules 26 | -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt)-> 2 | 3 | grunt.loadNpmTasks 'grunt-contrib-uglify' 4 | grunt.loadNpmTasks 'grunt-react' 5 | grunt.loadNpmTasks 'grunt-browserify' 6 | 7 | grunt.initConfig 8 | 9 | # Uglification 10 | uglify: 11 | build: 12 | files: 13 | 'dist/delorean.min.js': ['dist/delorean.js'] 14 | 15 | browserify: 16 | coffeeify: 17 | options: 18 | bundleOptions: 19 | standalone: 'DeLorean' 20 | transform: ['coffeeify'] 21 | 22 | files: 23 | 'dist/delorean.js': ['src/delorean.coffee'] 24 | 25 | grunt.registerTask 'default', ['browserify:coffeeify', 'uglify'] 26 | -------------------------------------------------------------------------------- /src/flux.coffee: -------------------------------------------------------------------------------- 1 | Store = require './store.coffee' 2 | Dispatcher = require './dispatcher.coffee' 3 | 4 | class Flux 5 | 6 | # Generates new store using Store class 7 | # Stores are global and not connected directly 8 | # to the Views 9 | @createStore: (store)-> 10 | new Store store 11 | 12 | # Dispatchers are actually Action sets, it has getStores 13 | # function. There should be stores for a complete 14 | # Flux structure. 15 | @createDispatcher: (actions)-> 16 | dispatcher = new Dispatcher actions.getStores?() 17 | for own action, callback of actions 18 | unless action is 'getStores' 19 | dispatcher.registerAction action, callback 20 | dispatcher 21 | 22 | # Mixins can be defined in `mixin.coffee` file. 23 | Flux.mixins = require './mixin.coffee' 24 | module.exports = Flux 25 | -------------------------------------------------------------------------------- /src/dispatcher.coffee: -------------------------------------------------------------------------------- 1 | # Dispatchers are actually Action sets, it has getStores 2 | # function. There should be stores for a complete 3 | # Flux structure. 4 | {EventEmitter} = require 'events' 5 | {Promise} = require 'es6-promise' 6 | 7 | class Dispatcher extends EventEmitter 8 | 9 | constructor: (@stores)-> 10 | 11 | # This method can be called from an action method 12 | # Sends action to the all related stores. 13 | dispatch: (actionName, data)-> 14 | deferred = @waitFor (store for storeName, store of @stores) 15 | for own storeName, store of @stores 16 | store.dispatchAction actionName, data 17 | 18 | return deferred 19 | 20 | waitFor: (stores)-> 21 | promises = for store in stores 22 | new Promise (resolve, reject)-> 23 | store.once 'change', resolve 24 | 25 | Promise.all(promises).then => 26 | @emit 'change:all' 27 | 28 | # Generates new method on the instance 29 | registerAction: (actionName, callback)-> 30 | @[actionName] = callback.bind this 31 | 32 | module.exports = Dispatcher 33 | -------------------------------------------------------------------------------- /src/mixin.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | # It should be inserted to the React components which 3 | # used in Flux. 4 | # Simply `mixin: [Flux.mixins.storeListener]` will work. 5 | storeListener: 6 | # After the component mounted, listen changes of the related stores 7 | componentDidMount: -> 8 | for own storeName, store of @stores 9 | do (store, storeName) => 10 | store.onChange => 11 | # call the components `storeDidChanged` method 12 | @storeDidChanged? storeName 13 | # change state 14 | if state = store.store.getState?() 15 | @state.stores[storeName] = state 16 | @forceUpdate() 17 | 18 | getInitialState: -> 19 | # Some shortcuts 20 | @dispatcher = @props.dispatcher 21 | @dispatcher.on 'change:all', => 22 | @storesDidChanged?() 23 | 24 | @stores = @dispatcher.stores 25 | 26 | state = stores: {} 27 | # more shortcuts for the state 28 | for own storeName of @stores 29 | state.stores[storeName] = @stores[storeName].store.getState?() 30 | state 31 | -------------------------------------------------------------------------------- /examples/todomvc/asset/jsx/index.jsx: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | var React = require('react'); 4 | var Flux = require('../../../../').Flux; 5 | var Router = require('director').Router; 6 | 7 | /* Generate Generic Store */ 8 | 9 | var TodoStore = Flux.createStore({ 10 | 11 | todos: [ 12 | {text: 'hello'}, 13 | {text: 'world'} 14 | ], 15 | 16 | actions: { 17 | 'todo:add': 'addTodo', 18 | 'todo:remove': 'removeTodo' 19 | }, 20 | 21 | addTodo: function (todo) { 22 | this.todos.push({text: todo.text}); 23 | this.emit('change'); 24 | }, 25 | 26 | removeTodo: function (todoToComplete) { 27 | this.todos = this.todos.filter(function (todo) { 28 | return todoToComplete.text !== todo.text 29 | }); 30 | this.emit('change'); 31 | }, 32 | 33 | getState: function () { 34 | return { 35 | todos: this.todos 36 | } 37 | } 38 | }); 39 | 40 | /* Generate List dispatcher with TodoStore. */ 41 | 42 | var TodoListDispatcher = Flux.createDispatcher({ 43 | 44 | removeTodo: function (todo) { 45 | if (confirm('Do you really want to delete this todo?')) { 46 | this.dispatch('todo:remove', todo) 47 | .then(function () { 48 | alert('Item is deleted successfully.'); 49 | }); 50 | } 51 | }, 52 | 53 | getStores: function () { 54 | return { 55 | todoStore: TodoStore 56 | } 57 | } 58 | 59 | }); 60 | 61 | /* Generate Todo Form dispatcher with TodoStore. */ 62 | 63 | var TodoFormDispatcher = Flux.createDispatcher({ 64 | 65 | addTodo: function (todo) { 66 | this.dispatch('todo:add', todo); 67 | }, 68 | 69 | getStores: function () { 70 | return { 71 | todoStore: TodoStore 72 | } 73 | } 74 | 75 | }); 76 | 77 | /* Static Dispatcher */ 78 | 79 | var TodoDispatcher = Flux.createDispatcher({ 80 | 81 | getStores: function () { 82 | return { 83 | todoStore: TodoStore 84 | } 85 | } 86 | 87 | }); 88 | 89 | /* React Components */ 90 | 91 | var TodoItemView = React.createClass({ 92 | 93 | render: function (todo) { 94 | return
  • {this.props.todo.text}
  • 95 | }, 96 | 97 | handleClick: function () { 98 | this.props.dispatcher.removeTodo(this.props.todo); 99 | } 100 | 101 | }); 102 | 103 | var TodoListView = React.createClass({ 104 | 105 | mixins: [Flux.mixins.storeListener], 106 | 107 | render: function () { 108 | var self = this; 109 | 110 | return 115 | } 116 | 117 | }); 118 | 119 | var TodoFormView = React.createClass({ 120 | 121 | mixins: [Flux.mixins.storeListener], 122 | 123 | render: function () { 124 | var self = this; 125 | return
    126 | 127 |
    128 | }, 129 | 130 | handleChange: function (e) { 131 | this.setState({todo: e.target.value}); 132 | }, 133 | 134 | handleSubmit: function (e) { 135 | e.preventDefault(); 136 | this.dispatcher.addTodo({text: this.state.todo}); 137 | this.setState({todo: ''}); 138 | } 139 | 140 | }); 141 | 142 | var ApplicationView = React.createClass({ 143 | 144 | mixins: [Flux.mixins.storeListener], 145 | 146 | render: function () { 147 | var self = this; 148 | return
    149 | 150 | 151 | There are {this.stores.todoStore.store.todos.length} todos. 152 |
    153 | } 154 | 155 | }); 156 | 157 | var mainView = React.renderComponent(, 158 | document.getElementById('main')) 159 | 160 | var appRouter = new Router({ 161 | '/random': function () { 162 | mainView.dispatcher.dispatch('todo:add', {text: Math.random()}); 163 | location.hash = '/'; 164 | } 165 | }); 166 | 167 | appRouter.init('/'); 168 | -------------------------------------------------------------------------------- /examples/todomvc/asset/js/index.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 | var React = require('react'); 4 | var Flux = require('../../../../').Flux; 5 | var Router = require('director').Router; 6 | 7 | /* Generate Generic Store */ 8 | 9 | var TodoStore = Flux.createStore({ 10 | 11 | todos: [ 12 | {text: 'hello'}, 13 | {text: 'world'} 14 | ], 15 | 16 | actions: { 17 | 'todo:add': 'addTodo', 18 | 'todo:remove': 'removeTodo' 19 | }, 20 | 21 | addTodo: function (todo) { 22 | this.todos.push({text: todo.text}); 23 | this.emit('change'); 24 | }, 25 | 26 | removeTodo: function (todoToComplete) { 27 | this.todos = this.todos.filter(function (todo) { 28 | return todoToComplete.text !== todo.text 29 | }); 30 | this.emit('change'); 31 | }, 32 | 33 | getState: function () { 34 | return { 35 | todos: this.todos 36 | } 37 | } 38 | }); 39 | 40 | /* Generate List dispatcher with TodoStore. */ 41 | 42 | var TodoListDispatcher = Flux.createDispatcher({ 43 | 44 | removeTodo: function (todo) { 45 | if (confirm('Do you really want to delete this todo?')) { 46 | this.dispatch('todo:remove', todo) 47 | .then(function () { 48 | alert('Item is deleted successfully.'); 49 | }); 50 | } 51 | }, 52 | 53 | getStores: function () { 54 | return { 55 | todoStore: TodoStore 56 | } 57 | } 58 | 59 | }); 60 | 61 | /* Generate Todo Form dispatcher with TodoStore. */ 62 | 63 | var TodoFormDispatcher = Flux.createDispatcher({ 64 | 65 | addTodo: function (todo) { 66 | this.dispatch('todo:add', todo); 67 | }, 68 | 69 | getStores: function () { 70 | return { 71 | todoStore: TodoStore 72 | } 73 | } 74 | 75 | }); 76 | 77 | /* Static Dispatcher */ 78 | 79 | var TodoDispatcher = Flux.createDispatcher({ 80 | 81 | getStores: function () { 82 | return { 83 | todoStore: TodoStore 84 | } 85 | } 86 | 87 | }); 88 | 89 | /* React Components */ 90 | 91 | var TodoItemView = React.createClass({displayName: 'TodoItemView', 92 | 93 | render: function (todo) { 94 | return React.DOM.li({onClick: this.handleClick}, this.props.todo.text) 95 | }, 96 | 97 | handleClick: function () { 98 | this.props.dispatcher.removeTodo(this.props.todo); 99 | } 100 | 101 | }); 102 | 103 | var TodoListView = React.createClass({displayName: 'TodoListView', 104 | 105 | mixins: [Flux.mixins.storeListener], 106 | 107 | render: function () { 108 | var self = this; 109 | 110 | return React.DOM.ul(null, 111 | this.stores.todoStore.store.todos.map(function (todo) { 112 | return TodoItemView({dispatcher: self.dispatcher, todo: todo}) 113 | }) 114 | ) 115 | } 116 | 117 | }); 118 | 119 | var TodoFormView = React.createClass({displayName: 'TodoFormView', 120 | 121 | mixins: [Flux.mixins.storeListener], 122 | 123 | render: function () { 124 | var self = this; 125 | return React.DOM.form({onSubmit: this.handleSubmit}, 126 | React.DOM.input({value: this.state.todo, onChange: this.handleChange}) 127 | ) 128 | }, 129 | 130 | handleChange: function (e) { 131 | this.setState({todo: e.target.value}); 132 | }, 133 | 134 | handleSubmit: function (e) { 135 | e.preventDefault(); 136 | this.dispatcher.addTodo({text: this.state.todo}); 137 | this.setState({todo: ''}); 138 | } 139 | 140 | }); 141 | 142 | var ApplicationView = React.createClass({displayName: 'ApplicationView', 143 | 144 | mixins: [Flux.mixins.storeListener], 145 | 146 | render: function () { 147 | var self = this; 148 | return React.DOM.div(null, 149 | TodoListView({dispatcher: TodoListDispatcher}), 150 | TodoFormView({dispatcher: TodoFormDispatcher}), 151 | React.DOM.span(null, "There are ", this.stores.todoStore.store.todos.length, " todos.") 152 | ) 153 | } 154 | 155 | }); 156 | 157 | var mainView = React.renderComponent(ApplicationView({dispatcher: TodoDispatcher}), 158 | document.getElementById('main')) 159 | 160 | var appRouter = new Router({ 161 | '/random': function () { 162 | mainView.dispatcher.dispatch('todo:add', {text: Math.random()}); 163 | location.hash = '/'; 164 | } 165 | }); 166 | 167 | appRouter.init('/'); 168 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![DeLorean Logo](https://raw.githubusercontent.com/f/delorean/master/asset/delorean-logo.png) 2 | 3 | # DeLorean.js 4 | 5 | DeLorean is a tiny Flux pattern implementation. 6 | 7 | ## What is Flux 8 | 9 | Data in a Flux application flows in a single direction, in a cycle: 10 | 11 | ``` 12 | Views ---> (actions) ----> Dispatcher ---> (registered callback) ---> Stores -------+ 13 | Ʌ | 14 | | V 15 | +-- (Controller-Views "change" event handlers) ---- (Stores emit "change" events) --+ 16 | ``` 17 | 18 | ## Install 19 | 20 | You can install **DeLorean** with Bower: 21 | 22 | ```bash 23 | bower install delorean 24 | ``` 25 | 26 | You can also install by NPM to use with **Browserify** *(recommended)* 27 | 28 | ```bash 29 | npm install delorean.js 30 | ``` 31 | 32 | ## Usage 33 | 34 | ```js 35 | var Flux = require('delorean.js').Flux; 36 | ``` 37 | 38 | ## Stores 39 | 40 | > Stores contain the application state and logic. Their role is somewhat similar 41 | > to a model in a traditional MVC, but they manage the state of many objects — 42 | > they are not instances of one object. Nor are they the same as Backbone's 43 | > collections. More than simply managing a collection of ORM-style objects, 44 | > stores manage the application state for a particular domain within the application. 45 | 46 | ### `Flux.createStore` 47 | 48 | ```js 49 | var TodoStore = Flux.createStore({ 50 | 51 | todos: [ 52 | {text: 'hello'}, 53 | {text: 'world'} 54 | ], 55 | 56 | actions: { 57 | 'todo:add': 'addTodo', 58 | 'todo:remove': 'removeTodo' 59 | }, 60 | 61 | addTodo: function (todo) { 62 | this.todos.push({text: todo.text}); 63 | this.emit('change'); 64 | }, 65 | 66 | removeTodo: function (todoToComplete) { 67 | this.todos = this.todos.filter(function (todo) { 68 | return todoToComplete.text !== todo.text 69 | }); 70 | this.emit('change'); 71 | }, 72 | 73 | getState: function () { 74 | return { 75 | todos: this.todos 76 | } 77 | } 78 | }); 79 | ``` 80 | 81 | ## Dispatcher 82 | 83 | > The dispatcher is the central hub that manages all data flow in a Flux application. 84 | > It is essentially a registry of callbacks into the stores. Each store registers 85 | > itself and provides a callback. When the dispatcher responds to an action, 86 | > all stores in the application are sent the data payload provided by the 87 | > action via the callbacks in the registry. 88 | 89 | ### `Flux.createDispatcher` 90 | 91 | ```js 92 | var TodoListApp = Flux.createDispatcher({ 93 | 94 | removeTodo: function (todo) { 95 | if (confirm('Do you really want to delete this todo?')) { 96 | this.dispatch('todo:remove', todo); 97 | } 98 | }, 99 | 100 | getStores: function () { 101 | return { 102 | todoStore: TodoStore 103 | } 104 | } 105 | 106 | }); 107 | ``` 108 | 109 | #### Action `dispatch` 110 | 111 | When an action is dispatched, all the stores know about the status and they 112 | process the data asynchronously. When all of them are finished the dispatcher 113 | emits `change:all` event, also `dispatch` method returns a promise. 114 | 115 | ```js 116 | var TodoListApp = Flux.createDispatcher({ 117 | 118 | removeTodo: function (todo) { 119 | if (confirm('Do you really want to delete this todo?')) { 120 | this.dispatch('todo:remove', todo) 121 | .then(function () { 122 | // All of the stores finished the process 123 | // about 'todo:remove' action 124 | alert('Item removed successfully'); 125 | }); 126 | } 127 | }, 128 | 129 | getStores: function () { 130 | return { 131 | todoStore: TodoStore 132 | } 133 | } 134 | 135 | }); 136 | ``` 137 | 138 | ## Combining to React 139 | 140 | You may bring all the flow together with the Views, actually *the Action generators*. 141 | You should use **`Flux.mixins.storeListener`** mixin to get a view into the Flux system. 142 | Also you should pass `dispatcher={DispatcherName}` attribute to React view. 143 | 144 | ```js 145 | // Child views don't have to have storeListener. 146 | 147 | var TodoItemView = React.createClass({ 148 | 149 | render: function (todo) { 150 | return
  • {this.props.todo.text}
  • 151 | }, 152 | 153 | handleClick: function () { 154 | this.props.dispatcher.removeTodo(this.props.todo); 155 | } 156 | 157 | }); 158 | 159 | var TodoListView = React.createClass({ 160 | 161 | mixins: [Flux.mixins.storeListener], 162 | 163 | render: function () { 164 | var self = this; 165 | return 170 | } 171 | 172 | }); 173 | ``` 174 | 175 | ### `storeDidChanged` and `storesDidChanged` 176 | 177 | Two functions are triggered when a store changed and all stores are changed. You can use 178 | these functions if your application needs. 179 | 180 | ```js 181 | var TodoListView = React.createClass({ 182 | 183 | mixins: [Flux.mixins.storeListener], 184 | 185 | // when all stores are updated 186 | storesDidChanged: function () { 187 | console.log("All stores are now updated."); 188 | }, 189 | 190 | // when a store updates 191 | storeDidChanged: function (storeName) { 192 | console.log(storeName + " store is now updated."); 193 | }, 194 | 195 | render: function () { 196 | // ... 197 | } 198 | 199 | }); 200 | ``` 201 | 202 | ## Routing 203 | 204 | You can use any Router tool with DeLorean. In the example I use `director` as the router. 205 | 206 | ```js 207 | var Router = require('director').Router; 208 | ``` 209 | 210 | You may trig the action from View. So you can just do something like that: 211 | 212 | ```js 213 | var mainView = React.renderComponent(, 214 | document.getElementById('main')) 215 | 216 | var appRouter = new Router({ 217 | '/random': function () { 218 | mainView.dispatcher.dispatch('todo:add', {text: Math.random()}); 219 | location.hash = '/'; 220 | } 221 | }); 222 | ``` 223 | 224 | ## Running the TodoMVC example 225 | 226 | There is a simple TodoMVC example working with DeLorean.js 227 | 228 | ```bash 229 | cd examples/todomvc 230 | grunt 231 | open index.html 232 | ``` 233 | 234 | ## Todo 235 | 236 | - Improve Readme. 237 | 238 | ## Name 239 | 240 | The **flux capacitor** was the core component of Doctor Emmett Brown's time traveling **DeLorean time machine** 241 | 242 | ## License 243 | 244 | [MIT License](http://f.mit-license.org) 245 | -------------------------------------------------------------------------------- /dist/delorean.min.js: -------------------------------------------------------------------------------- 1 | !function(a){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=a();else if("function"==typeof define&&define.amd)define([],a);else{var b;"undefined"!=typeof window?b=window:"undefined"!=typeof global?b=global:"undefined"!=typeof self&&(b=self),b.DeLorean=a()}}(function(){return function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);throw new Error("Cannot find module '"+g+"'")}var j=c[g]={exports:{}};b[g][0].call(j.exports,function(a){var c=b[g][1][a];return e(c?c:a)},j,j.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;ga||isNaN(a))throw TypeError("n must be a positive number");return this._maxListeners=a,this},c.prototype.emit=function(a){var b,c,e,h,i,j;if(this._events||(this._events={}),"error"===a&&(!this._events.error||f(this._events.error)&&!this._events.error.length))throw b=arguments[1],b instanceof Error?b:TypeError('Uncaught, unspecified "error" event.');if(c=this._events[a],g(c))return!1;if(d(c))switch(arguments.length){case 1:c.call(this);break;case 2:c.call(this,arguments[1]);break;case 3:c.call(this,arguments[1],arguments[2]);break;default:for(e=arguments.length,h=new Array(e-1),i=1;e>i;i++)h[i-1]=arguments[i];c.apply(this,h)}else if(f(c)){for(e=arguments.length,h=new Array(e-1),i=1;e>i;i++)h[i-1]=arguments[i];for(j=c.slice(),e=j.length,i=0;e>i;i++)j[i].apply(this,h)}return!0},c.prototype.addListener=function(a,b){var e;if(!d(b))throw TypeError("listener must be a function");if(this._events||(this._events={}),this._events.newListener&&this.emit("newListener",a,d(b.listener)?b.listener:b),this._events[a]?f(this._events[a])?this._events[a].push(b):this._events[a]=[this._events[a],b]:this._events[a]=b,f(this._events[a])&&!this._events[a].warned){var e;e=g(this._maxListeners)?c.defaultMaxListeners:this._maxListeners,e&&e>0&&this._events[a].length>e&&(this._events[a].warned=!0,console.error("(node) warning: possible EventEmitter memory leak detected. %d listeners added. Use emitter.setMaxListeners() to increase limit.",this._events[a].length),"function"==typeof console.trace&&console.trace())}return this},c.prototype.on=c.prototype.addListener,c.prototype.once=function(a,b){function c(){this.removeListener(a,c),e||(e=!0,b.apply(this,arguments))}if(!d(b))throw TypeError("listener must be a function");var e=!1;return c.listener=b,this.on(a,c),this},c.prototype.removeListener=function(a,b){var c,e,g,h;if(!d(b))throw TypeError("listener must be a function");if(!this._events||!this._events[a])return this;if(c=this._events[a],g=c.length,e=-1,c===b||d(c.listener)&&c.listener===b)delete this._events[a],this._events.removeListener&&this.emit("removeListener",a,b);else if(f(c)){for(h=g;h-->0;)if(c[h]===b||c[h].listener&&c[h].listener===b){e=h;break}if(0>e)return this;1===c.length?(c.length=0,delete this._events[a]):c.splice(e,1),this._events.removeListener&&this.emit("removeListener",a,b)}return this},c.prototype.removeAllListeners=function(a){var b,c;if(!this._events)return this;if(!this._events.removeListener)return 0===arguments.length?this._events={}:this._events[a]&&delete this._events[a],this;if(0===arguments.length){for(b in this._events)"removeListener"!==b&&this.removeAllListeners(b);return this.removeAllListeners("removeListener"),this._events={},this}if(c=this._events[a],d(c))this.removeListener(a,c);else for(;c.length;)this.removeListener(a,c[c.length-1]);return delete this._events[a],this},c.prototype.listeners=function(a){var b;return b=this._events&&this._events[a]?d(this._events[a])?[this._events[a]]:this._events[a].slice():[]},c.listenerCount=function(a,b){var c;return c=a._events&&a._events[b]?d(a._events[b])?1:a._events[b].length:0}},{}],12:[function(a,b){function c(){}var d=b.exports={};d.nextTick=function(){var a="undefined"!=typeof window&&window.setImmediate,b="undefined"!=typeof window&&window.postMessage&&window.addEventListener;if(a)return function(a){return window.setImmediate(a)};if(b){var c=[];return window.addEventListener("message",function(a){var b=a.source;if((b===window||null===b)&&"process-tick"===a.data&&(a.stopPropagation(),c.length>0)){var d=c.shift();d()}},!0),function(a){c.push(a),window.postMessage("process-tick","*")}}return function(a){setTimeout(a,0)}}(),d.title="browser",d.browser=!0,d.env={},d.argv=[],d.on=c,d.addListener=c,d.once=c,d.off=c,d.removeListener=c,d.removeAllListeners=c,d.emit=c,d.binding=function(){throw new Error("process.binding is not supported")},d.cwd=function(){return"/"},d.chdir=function(){throw new Error("process.chdir is not supported")}},{}],13:[function(a,b){var c;c={Flux:a("./flux.coffee")},b.exports=c},{"./flux.coffee":15}],14:[function(a,b){var c,d,e,f={}.hasOwnProperty,g=function(a,b){function c(){this.constructor=a}for(var d in b)f.call(b,d)&&(a[d]=b[d]);return c.prototype=b.prototype,a.prototype=new c,a.__super__=b.prototype,a};d=a("events").EventEmitter,e=a("es6-promise").Promise,c=function(a){function b(a){this.stores=a}return g(b,a),b.prototype.dispatch=function(a,b){var c,d,e,g;c=this.waitFor(function(){var a,b;a=this.stores,b=[];for(e in a)d=a[e],b.push(d);return b}.call(this)),g=this.stores;for(e in g)f.call(g,e)&&(d=g[e],d.dispatchAction(a,b));return c},b.prototype.waitFor=function(a){var b,c;return b=function(){var b,d,f;for(f=[],b=0,d=a.length;d>b;b++)c=a[b],f.push(new e(function(a){return c.once("change",a)}));return f}(),e.all(b).then(function(a){return function(){return a.emit("change:all")}}(this))},b.prototype.registerAction=function(a,b){return this[a]=b.bind(this)},b}(d),b.exports=c},{"es6-promise":1,events:11}],15:[function(a,b){var c,d,e,f={}.hasOwnProperty;e=a("./store.coffee"),c=a("./dispatcher.coffee"),d=function(){function a(){}return a.createStore=function(a){return new e(a)},a.createDispatcher=function(a){var b,d,e;e=new c("function"==typeof a.getStores?a.getStores():void 0);for(b in a)f.call(a,b)&&(d=a[b],"getStores"!==b&&e.registerAction(b,d));return e},a}(),d.mixins=a("./mixin.coffee"),b.exports=d},{"./dispatcher.coffee":14,"./mixin.coffee":16,"./store.coffee":17}],16:[function(a,b){var c={}.hasOwnProperty;b.exports={storeListener:{componentDidMount:function(){var a,b,d,e;d=this.stores,e=[];for(b in d)c.call(d,b)&&(a=d[b],e.push(function(a){return function(b,c){return b.onChange(function(){var d,e;return"function"==typeof a.storeDidChanged&&a.storeDidChanged(c),(d="function"==typeof(e=b.store).getState?e.getState():void 0)?(a.state.stores[c]=d,a.forceUpdate()):void 0})}}(this)(a,b)));return e},getInitialState:function(){var a,b,d,e;this.dispatcher=this.props.dispatcher,this.dispatcher.on("change:all",function(a){return function(){return"function"==typeof a.storesDidChanged?a.storesDidChanged():void 0}}(this)),this.stores=this.dispatcher.stores,a={stores:{}},e=this.stores;for(b in e)c.call(e,b)&&(a.stores[b]="function"==typeof(d=this.stores[b].store).getState?d.getState():void 0);return a}}}},{}],17:[function(a,b){var c,d,e={}.hasOwnProperty,f=function(a,b){function c(){this.constructor=a}for(var d in b)e.call(b,d)&&(a[d]=b[d]);return c.prototype=b.prototype,a.prototype=new c,a.__super__=b.prototype,a};c=a("events").EventEmitter,d=function(a){function b(a){this.store=a,b.__super__.constructor.apply(this,arguments),this.bindActions(a.actions)}return f(b,a),b.prototype.bindActions=function(a){var b,c,d;this.store.emit=this.emit.bind(this),d=[];for(b in a)e.call(a,b)&&(c=a[b],d.push(this.on("action:"+b,this.store[c].bind(this.store))));return d},b.prototype.dispatchAction=function(a,b){return this.emit("action:"+a,b)},b.prototype.onChange=function(a){return this.on("change",a)},b}(c),b.exports=d},{events:11}]},{},[13])(13)}); -------------------------------------------------------------------------------- /dist/delorean.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 f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.DeLorean=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0 && this._events[type].length > m) { 754 | this._events[type].warned = true; 755 | console.error('(node) warning: possible EventEmitter memory ' + 756 | 'leak detected. %d listeners added. ' + 757 | 'Use emitter.setMaxListeners() to increase limit.', 758 | this._events[type].length); 759 | if (typeof console.trace === 'function') { 760 | // not supported in IE 10 761 | console.trace(); 762 | } 763 | } 764 | } 765 | 766 | return this; 767 | }; 768 | 769 | EventEmitter.prototype.on = EventEmitter.prototype.addListener; 770 | 771 | EventEmitter.prototype.once = function(type, listener) { 772 | if (!isFunction(listener)) 773 | throw TypeError('listener must be a function'); 774 | 775 | var fired = false; 776 | 777 | function g() { 778 | this.removeListener(type, g); 779 | 780 | if (!fired) { 781 | fired = true; 782 | listener.apply(this, arguments); 783 | } 784 | } 785 | 786 | g.listener = listener; 787 | this.on(type, g); 788 | 789 | return this; 790 | }; 791 | 792 | // emits a 'removeListener' event iff the listener was removed 793 | EventEmitter.prototype.removeListener = function(type, listener) { 794 | var list, position, length, i; 795 | 796 | if (!isFunction(listener)) 797 | throw TypeError('listener must be a function'); 798 | 799 | if (!this._events || !this._events[type]) 800 | return this; 801 | 802 | list = this._events[type]; 803 | length = list.length; 804 | position = -1; 805 | 806 | if (list === listener || 807 | (isFunction(list.listener) && list.listener === listener)) { 808 | delete this._events[type]; 809 | if (this._events.removeListener) 810 | this.emit('removeListener', type, listener); 811 | 812 | } else if (isObject(list)) { 813 | for (i = length; i-- > 0;) { 814 | if (list[i] === listener || 815 | (list[i].listener && list[i].listener === listener)) { 816 | position = i; 817 | break; 818 | } 819 | } 820 | 821 | if (position < 0) 822 | return this; 823 | 824 | if (list.length === 1) { 825 | list.length = 0; 826 | delete this._events[type]; 827 | } else { 828 | list.splice(position, 1); 829 | } 830 | 831 | if (this._events.removeListener) 832 | this.emit('removeListener', type, listener); 833 | } 834 | 835 | return this; 836 | }; 837 | 838 | EventEmitter.prototype.removeAllListeners = function(type) { 839 | var key, listeners; 840 | 841 | if (!this._events) 842 | return this; 843 | 844 | // not listening for removeListener, no need to emit 845 | if (!this._events.removeListener) { 846 | if (arguments.length === 0) 847 | this._events = {}; 848 | else if (this._events[type]) 849 | delete this._events[type]; 850 | return this; 851 | } 852 | 853 | // emit removeListener for all listeners on all events 854 | if (arguments.length === 0) { 855 | for (key in this._events) { 856 | if (key === 'removeListener') continue; 857 | this.removeAllListeners(key); 858 | } 859 | this.removeAllListeners('removeListener'); 860 | this._events = {}; 861 | return this; 862 | } 863 | 864 | listeners = this._events[type]; 865 | 866 | if (isFunction(listeners)) { 867 | this.removeListener(type, listeners); 868 | } else { 869 | // LIFO order 870 | while (listeners.length) 871 | this.removeListener(type, listeners[listeners.length - 1]); 872 | } 873 | delete this._events[type]; 874 | 875 | return this; 876 | }; 877 | 878 | EventEmitter.prototype.listeners = function(type) { 879 | var ret; 880 | if (!this._events || !this._events[type]) 881 | ret = []; 882 | else if (isFunction(this._events[type])) 883 | ret = [this._events[type]]; 884 | else 885 | ret = this._events[type].slice(); 886 | return ret; 887 | }; 888 | 889 | EventEmitter.listenerCount = function(emitter, type) { 890 | var ret; 891 | if (!emitter._events || !emitter._events[type]) 892 | ret = 0; 893 | else if (isFunction(emitter._events[type])) 894 | ret = 1; 895 | else 896 | ret = emitter._events[type].length; 897 | return ret; 898 | }; 899 | 900 | function isFunction(arg) { 901 | return typeof arg === 'function'; 902 | } 903 | 904 | function isNumber(arg) { 905 | return typeof arg === 'number'; 906 | } 907 | 908 | function isObject(arg) { 909 | return typeof arg === 'object' && arg !== null; 910 | } 911 | 912 | function isUndefined(arg) { 913 | return arg === void 0; 914 | } 915 | 916 | },{}],12:[function(_dereq_,module,exports){ 917 | // shim for using process in browser 918 | 919 | var process = module.exports = {}; 920 | 921 | process.nextTick = (function () { 922 | var canSetImmediate = typeof window !== 'undefined' 923 | && window.setImmediate; 924 | var canPost = typeof window !== 'undefined' 925 | && window.postMessage && window.addEventListener 926 | ; 927 | 928 | if (canSetImmediate) { 929 | return function (f) { return window.setImmediate(f) }; 930 | } 931 | 932 | if (canPost) { 933 | var queue = []; 934 | window.addEventListener('message', function (ev) { 935 | var source = ev.source; 936 | if ((source === window || source === null) && ev.data === 'process-tick') { 937 | ev.stopPropagation(); 938 | if (queue.length > 0) { 939 | var fn = queue.shift(); 940 | fn(); 941 | } 942 | } 943 | }, true); 944 | 945 | return function nextTick(fn) { 946 | queue.push(fn); 947 | window.postMessage('process-tick', '*'); 948 | }; 949 | } 950 | 951 | return function nextTick(fn) { 952 | setTimeout(fn, 0); 953 | }; 954 | })(); 955 | 956 | process.title = 'browser'; 957 | process.browser = true; 958 | process.env = {}; 959 | process.argv = []; 960 | 961 | function noop() {} 962 | 963 | process.on = noop; 964 | process.addListener = noop; 965 | process.once = noop; 966 | process.off = noop; 967 | process.removeListener = noop; 968 | process.removeAllListeners = noop; 969 | process.emit = noop; 970 | 971 | process.binding = function (name) { 972 | throw new Error('process.binding is not supported'); 973 | } 974 | 975 | // TODO(shtylman) 976 | process.cwd = function () { return '/' }; 977 | process.chdir = function (dir) { 978 | throw new Error('process.chdir is not supported'); 979 | }; 980 | 981 | },{}],13:[function(_dereq_,module,exports){ 982 | var DeLorean; 983 | 984 | DeLorean = { 985 | Flux: _dereq_('./flux.coffee') 986 | }; 987 | 988 | module.exports = DeLorean; 989 | 990 | 991 | 992 | },{"./flux.coffee":15}],14:[function(_dereq_,module,exports){ 993 | var Dispatcher, EventEmitter, Promise, 994 | __hasProp = {}.hasOwnProperty, 995 | __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; 996 | 997 | EventEmitter = _dereq_('events').EventEmitter; 998 | 999 | Promise = _dereq_('es6-promise').Promise; 1000 | 1001 | Dispatcher = (function(_super) { 1002 | __extends(Dispatcher, _super); 1003 | 1004 | function Dispatcher(stores) { 1005 | this.stores = stores; 1006 | } 1007 | 1008 | Dispatcher.prototype.dispatch = function(actionName, data) { 1009 | var deferred, store, storeName, _ref; 1010 | deferred = this.waitFor((function() { 1011 | var _ref, _results; 1012 | _ref = this.stores; 1013 | _results = []; 1014 | for (storeName in _ref) { 1015 | store = _ref[storeName]; 1016 | _results.push(store); 1017 | } 1018 | return _results; 1019 | }).call(this)); 1020 | _ref = this.stores; 1021 | for (storeName in _ref) { 1022 | if (!__hasProp.call(_ref, storeName)) continue; 1023 | store = _ref[storeName]; 1024 | store.dispatchAction(actionName, data); 1025 | } 1026 | return deferred; 1027 | }; 1028 | 1029 | Dispatcher.prototype.waitFor = function(stores) { 1030 | var promises, store; 1031 | promises = (function() { 1032 | var _i, _len, _results; 1033 | _results = []; 1034 | for (_i = 0, _len = stores.length; _i < _len; _i++) { 1035 | store = stores[_i]; 1036 | _results.push(new Promise(function(resolve, reject) { 1037 | return store.once('change', resolve); 1038 | })); 1039 | } 1040 | return _results; 1041 | })(); 1042 | return Promise.all(promises).then((function(_this) { 1043 | return function() { 1044 | return _this.emit('change:all'); 1045 | }; 1046 | })(this)); 1047 | }; 1048 | 1049 | Dispatcher.prototype.registerAction = function(actionName, callback) { 1050 | return this[actionName] = callback.bind(this); 1051 | }; 1052 | 1053 | return Dispatcher; 1054 | 1055 | })(EventEmitter); 1056 | 1057 | module.exports = Dispatcher; 1058 | 1059 | 1060 | 1061 | },{"es6-promise":1,"events":11}],15:[function(_dereq_,module,exports){ 1062 | var Dispatcher, Flux, Store, 1063 | __hasProp = {}.hasOwnProperty; 1064 | 1065 | Store = _dereq_('./store.coffee'); 1066 | 1067 | Dispatcher = _dereq_('./dispatcher.coffee'); 1068 | 1069 | Flux = (function() { 1070 | function Flux() {} 1071 | 1072 | Flux.createStore = function(store) { 1073 | return new Store(store); 1074 | }; 1075 | 1076 | Flux.createDispatcher = function(actions) { 1077 | var action, callback, dispatcher; 1078 | dispatcher = new Dispatcher(typeof actions.getStores === "function" ? actions.getStores() : void 0); 1079 | for (action in actions) { 1080 | if (!__hasProp.call(actions, action)) continue; 1081 | callback = actions[action]; 1082 | if (action !== 'getStores') { 1083 | dispatcher.registerAction(action, callback); 1084 | } 1085 | } 1086 | return dispatcher; 1087 | }; 1088 | 1089 | return Flux; 1090 | 1091 | })(); 1092 | 1093 | Flux.mixins = _dereq_('./mixin.coffee'); 1094 | 1095 | module.exports = Flux; 1096 | 1097 | 1098 | 1099 | },{"./dispatcher.coffee":14,"./mixin.coffee":16,"./store.coffee":17}],16:[function(_dereq_,module,exports){ 1100 | var __hasProp = {}.hasOwnProperty; 1101 | 1102 | module.exports = { 1103 | storeListener: { 1104 | componentDidMount: function() { 1105 | var store, storeName, _ref, _results; 1106 | _ref = this.stores; 1107 | _results = []; 1108 | for (storeName in _ref) { 1109 | if (!__hasProp.call(_ref, storeName)) continue; 1110 | store = _ref[storeName]; 1111 | _results.push((function(_this) { 1112 | return function(store, storeName) { 1113 | return store.onChange(function() { 1114 | var state, _base; 1115 | if (typeof _this.storeDidChanged === "function") { 1116 | _this.storeDidChanged(storeName); 1117 | } 1118 | if (state = typeof (_base = store.store).getState === "function" ? _base.getState() : void 0) { 1119 | _this.state.stores[storeName] = state; 1120 | return _this.forceUpdate(); 1121 | } 1122 | }); 1123 | }; 1124 | })(this)(store, storeName)); 1125 | } 1126 | return _results; 1127 | }, 1128 | getInitialState: function() { 1129 | var state, storeName, _base, _ref; 1130 | this.dispatcher = this.props.dispatcher; 1131 | this.dispatcher.on('change:all', (function(_this) { 1132 | return function() { 1133 | return typeof _this.storesDidChanged === "function" ? _this.storesDidChanged() : void 0; 1134 | }; 1135 | })(this)); 1136 | this.stores = this.dispatcher.stores; 1137 | state = { 1138 | stores: {} 1139 | }; 1140 | _ref = this.stores; 1141 | for (storeName in _ref) { 1142 | if (!__hasProp.call(_ref, storeName)) continue; 1143 | state.stores[storeName] = typeof (_base = this.stores[storeName].store).getState === "function" ? _base.getState() : void 0; 1144 | } 1145 | return state; 1146 | } 1147 | } 1148 | }; 1149 | 1150 | 1151 | 1152 | },{}],17:[function(_dereq_,module,exports){ 1153 | var EventEmitter, Store, 1154 | __hasProp = {}.hasOwnProperty, 1155 | __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; 1156 | 1157 | EventEmitter = _dereq_('events').EventEmitter; 1158 | 1159 | Store = (function(_super) { 1160 | __extends(Store, _super); 1161 | 1162 | function Store(store) { 1163 | this.store = store; 1164 | Store.__super__.constructor.apply(this, arguments); 1165 | this.bindActions(store.actions); 1166 | } 1167 | 1168 | Store.prototype.bindActions = function(actions) { 1169 | var actionName, callback, _results; 1170 | this.store.emit = this.emit.bind(this); 1171 | _results = []; 1172 | for (actionName in actions) { 1173 | if (!__hasProp.call(actions, actionName)) continue; 1174 | callback = actions[actionName]; 1175 | _results.push(this.on("action:" + actionName, this.store[callback].bind(this.store))); 1176 | } 1177 | return _results; 1178 | }; 1179 | 1180 | Store.prototype.dispatchAction = function(actionName, data) { 1181 | return this.emit("action:" + actionName, data); 1182 | }; 1183 | 1184 | Store.prototype.onChange = function(callback) { 1185 | return this.on('change', callback); 1186 | }; 1187 | 1188 | return Store; 1189 | 1190 | })(EventEmitter); 1191 | 1192 | module.exports = Store; 1193 | 1194 | 1195 | 1196 | },{"events":11}]},{},[13]) 1197 | (13) 1198 | }); --------------------------------------------------------------------------------