├── .gitignore ├── README.md ├── build └── release.sh ├── config ├── rollup.config.common.js └── rollup.config.js ├── dist ├── vuecommander.common.js ├── vuecommander.esm.js ├── vuecommander.js └── vuecommander.min.js ├── examples ├── counter │ ├── Context.js │ ├── CountCommand.js │ ├── CountModel.js │ ├── Counter.vue │ ├── app.js │ └── index.html ├── dist │ ├── counter.js │ ├── shared.js │ └── todos.js ├── index.html ├── server.js ├── styles.css ├── todos │ ├── Context.js │ ├── Todo.vue │ ├── TodoCommands.js │ ├── Todos.vue │ ├── TodosModel.js │ ├── app.js │ └── index.html ├── webpack.config.js └── webpack.config.prod.js ├── logo.png ├── package.json ├── src ├── .babelrc ├── Context.js ├── EventManager.js ├── Interface.js ├── Mapper.js ├── index.js └── install.js ├── test └── unit │ ├── context.js │ ├── eventManager.js │ ├── history.js │ ├── interface.js │ └── mapper.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | node_modules 3 | yarn-error.log -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VueCommander 2 | 3 | ![VueCommander Logo](logo.png "VueCommander Logo") 4 | 5 | See [VueCommander.com](http://vuecommander.com) for documentation and examples. 6 | 7 | VueCommander is a Vuejs application framework utilizing the classic ([GoF](https://en.wikipedia.org/wiki/Design_Patterns)) [command](https://en.wikipedia.org/wiki/Command_pattern) and [observer](https://en.wikipedia.org/wiki/Observer_pattern) design patterns, as well as dependency injection (which comes free with JavaScript) for easy and opinionated management of large applications. It provides a clear separation of concerns. VueCommander lets you parameterize methods with different requests, delay or queue a request execution, and support undoable operations. 8 | 9 | ## Benefits 10 | By using the command design pattern, each action a user takes is self contained in an instance. This has huge benefits in that it can be stored in it's current state and reversed later. Storage of commands is extremely light weight. By creating a class for each command you can separate your business logic into proper methods and have a self contained unit of operation. See Getting Started below. 11 | 12 | Undoing is historically implemented using either the command or memento design patterns. The command pattern has the benefit of being lighter weight (from a storage standpoint) because you can choose what you store and how to undo it. 13 | 14 | ## Getting Started 15 | 16 | See the examples directory for a list of examples. 17 | 18 | Clone the repo and run 19 | 20 | ``` 21 | yarn build:examples 22 | ``` 23 | 24 | Compile the source 25 | 26 | ``` 27 | yarn build 28 | ``` 29 | 30 | Run the tests 31 | 32 | ``` 33 | yarn test 34 | ``` 35 | 36 | `Context`: A Context describes commands you would like to map to what events. 37 | 38 | `Commands`: A class with an execute method that cannot receive any parameters. It is a self contained unit of execution. You can use the command class to do all your business logic. You can inject any models you want to change into this command. Those models should be object literals which will act as singletons. 39 | 40 | `Models`: Are object literals which act as singletons. -------------------------------------------------------------------------------- /build/release.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | echo "Enter release version: " 3 | read VERSION 4 | 5 | read -p "Releasing $VERSION - are you sure? (y/n)" -n 1 -r 6 | echo # (optional) move to a new line 7 | if [[ $REPLY =~ ^[Yy]$ ]] 8 | then 9 | echo "Releasing $VERSION ..." 10 | 11 | # run tests 12 | npm test 2>/dev/null 13 | 14 | # build 15 | VERSION=$VERSION npm run build 16 | VERSION=$VERSION npm run build:examples 17 | 18 | # commit 19 | git add -A 20 | git commit -m "[build] $VERSION" 21 | npm version $VERSION --message "[release] $VERSION" 22 | 23 | # publish 24 | git push origin refs/tags/v$VERSION 25 | git push 26 | npm publish 27 | fi -------------------------------------------------------------------------------- /config/rollup.config.common.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import resolve from 'rollup-plugin-node-resolve'; 3 | 4 | export default { 5 | input: 'src/index.js', 6 | output: { 7 | file: 'dist/vuecommander.common.js', 8 | format: 'cjs', 9 | name: 'VueCommander' 10 | }, 11 | plugins: [ 12 | resolve(), 13 | babel({ 14 | exclude: 'node_modules/**' 15 | }) 16 | ] 17 | }; -------------------------------------------------------------------------------- /config/rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonConfig from './rollup.config.common'; 2 | import resolve from 'rollup-plugin-node-resolve'; 3 | import babel from 'rollup-plugin-babel'; 4 | import vue from 'rollup-plugin-vue'; 5 | import commonjs from 'rollup-plugin-commonjs'; 6 | import { uglify } from "rollup-plugin-uglify"; 7 | 8 | const configs = [ 9 | { 10 | input: 'src/index.js', 11 | output: { 12 | file: 'dist/vuecommander.js', 13 | format: 'umd', 14 | name: 'VueCommander' 15 | }, 16 | plugins: [ 17 | resolve(), 18 | babel({ 19 | exclude: 'node_modules/**' 20 | }) 21 | ] 22 | }, 23 | { 24 | input: 'src/index.js', 25 | output: { 26 | file: 'dist/vuecommander.min.js', 27 | format: 'umd', 28 | compact: true, 29 | name: 'VueCommander' 30 | }, 31 | plugins: [ 32 | uglify(), 33 | resolve(), 34 | babel({ 35 | exclude: 'node_modules/**' 36 | }) 37 | ] 38 | }, 39 | { 40 | input: 'src/index.js', 41 | output: { 42 | file: 'dist/vuecommander.esm.js', 43 | format: 'es', 44 | name: 'VueCommander' 45 | }, 46 | plugins: [ 47 | resolve(), 48 | babel({ 49 | exclude: 'node_modules/**' 50 | }) 51 | ] 52 | } 53 | ]; 54 | 55 | configs.push(commonConfig); 56 | 57 | export default configs; -------------------------------------------------------------------------------- /dist/vuecommander.common.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function _classCallCheck(instance, Constructor) { 4 | if (!(instance instanceof Constructor)) { 5 | throw new TypeError("Cannot call a class as a function"); 6 | } 7 | } 8 | 9 | function _defineProperties(target, props) { 10 | for (var i = 0; i < props.length; i++) { 11 | var descriptor = props[i]; 12 | descriptor.enumerable = descriptor.enumerable || false; 13 | descriptor.configurable = true; 14 | if ("value" in descriptor) descriptor.writable = true; 15 | Object.defineProperty(target, descriptor.key, descriptor); 16 | } 17 | } 18 | 19 | function _createClass(Constructor, protoProps, staticProps) { 20 | if (protoProps) _defineProperties(Constructor.prototype, protoProps); 21 | if (staticProps) _defineProperties(Constructor, staticProps); 22 | return Constructor; 23 | } 24 | 25 | var Interface = 26 | /*#__PURE__*/ 27 | function () { 28 | function Interface() { 29 | _classCallCheck(this, Interface); 30 | 31 | for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { 32 | args[_key] = arguments[_key]; 33 | } 34 | 35 | this.fields = args; 36 | } 37 | 38 | _createClass(Interface, [{ 39 | key: "implementedBy", 40 | value: function implementedBy(cls) { 41 | this.fields.forEach(function (arg) { 42 | if (!cls[arg]) { 43 | throw new Error("Class ".concat(cls.constructor.name, " does not implement method \"").concat(arg, "\"")); 44 | } 45 | }); 46 | } 47 | }]); 48 | 49 | return Interface; 50 | }(); 51 | 52 | var HistoryManager = 53 | /*#__PURE__*/ 54 | function () { 55 | function HistoryManager() { 56 | _classCallCheck(this, HistoryManager); 57 | 58 | this.history = []; 59 | } 60 | 61 | _createClass(HistoryManager, [{ 62 | key: "clear", 63 | value: function clear() { 64 | this.history = []; 65 | } 66 | }, { 67 | key: "push", 68 | value: function push(command) { 69 | return this.history.push(command); 70 | } 71 | }, { 72 | key: "pop", 73 | value: function pop() { 74 | return this.history.pop(); 75 | } 76 | }, { 77 | key: "length", 78 | get: function get() { 79 | return this.history.length; 80 | } 81 | }]); 82 | 83 | return HistoryManager; 84 | }(); 85 | var EventManager = 86 | /*#__PURE__*/ 87 | function () { 88 | function EventManager() { 89 | _classCallCheck(this, EventManager); 90 | 91 | this.listeners = {}; 92 | } 93 | 94 | _createClass(EventManager, [{ 95 | key: "subscribe", 96 | value: function subscribe(eventType, listener) { 97 | if (!this.listeners[eventType]) { 98 | this.listeners[eventType] = []; 99 | } 100 | 101 | this.listeners[eventType].push(listener); 102 | } 103 | }, { 104 | key: "unsubscribe", 105 | value: function unsubscribe(eventType, listener) { 106 | var listeners = this.listeners[eventType]; 107 | if (!listeners) return -1; 108 | var index = listeners.indexOf(listener); 109 | 110 | if (index > -1) { 111 | listeners.splice(index, 1); 112 | } 113 | 114 | this.listeners[eventType] = listeners; 115 | return index; 116 | } 117 | }, { 118 | key: "notify", 119 | value: function notify(eventType, data) { 120 | var listeners = this.listeners[eventType]; 121 | if (!listeners) return -1; 122 | var notified = []; 123 | listeners.forEach(function (listener) { 124 | var payload = { 125 | eventType: eventType, 126 | data: data 127 | }; 128 | listener.update(payload); 129 | notified.push({ 130 | listener: listener, 131 | eventType: eventType, 132 | data: data 133 | }); 134 | }); 135 | return notified; 136 | } 137 | }]); 138 | 139 | return EventManager; 140 | }(); 141 | var IEventListener = new Interface('update'); 142 | 143 | var Mapper = 144 | /*#__PURE__*/ 145 | function () { 146 | function Mapper(events, history) { 147 | _classCallCheck(this, Mapper); 148 | 149 | this.events = events; 150 | this.history = history; 151 | this.maps = {}; 152 | IEventListener.implementedBy(this); 153 | } 154 | 155 | _createClass(Mapper, [{ 156 | key: "update", 157 | value: function update(e) { 158 | var command = new this.maps[e.eventType](e); 159 | 160 | if (this.history && command.saveState) { 161 | command.saveState(); 162 | this.history.push(command); 163 | } 164 | 165 | command.execute(); 166 | } 167 | }, { 168 | key: "undo", 169 | value: function undo() { 170 | var command; 171 | 172 | if (this.history) { 173 | command = this.history.pop(); 174 | } 175 | 176 | if (command.undo) { 177 | this.history.pop().undo(); 178 | } 179 | } 180 | }, { 181 | key: "mapCommand", 182 | value: function mapCommand(command, eventType) { 183 | this.events.subscribe(eventType, this); 184 | this.maps[eventType] = command; 185 | } 186 | }, { 187 | key: "unmapCommand", 188 | value: function unmapCommand(command, eventType) { 189 | this.events.unsubscribe(eventType, this); 190 | delete this.maps[eventType]; 191 | } 192 | }]); 193 | 194 | return Mapper; 195 | }(); 196 | 197 | var Context = 198 | /*#__PURE__*/ 199 | function () { 200 | function Context(map) { 201 | _classCallCheck(this, Context); 202 | 203 | this.events = new EventManager(); 204 | this.history = new HistoryManager(); 205 | this.mapper = new Mapper(this.events, this.history); 206 | this.mapCommands(map); 207 | } 208 | 209 | _createClass(Context, [{ 210 | key: "mapCommands", 211 | value: function mapCommands(map) { 212 | var _this = this; 213 | 214 | Object.keys(map).forEach(function (key, index) { 215 | _this.mapper.mapCommand(map[key], key); 216 | }); 217 | } 218 | }]); 219 | 220 | return Context; 221 | }(); 222 | 223 | function install (Vue) { 224 | Vue.mixin({ 225 | beforeCreate: function beforeCreate() { 226 | var options = this.$options; 227 | 228 | if (options.context) { 229 | this.$context = typeof options.context === 'function' ? options.context() : options.context; 230 | } else if (options.parent && options.parent.$context) { 231 | this.$context = options.parent.$context; 232 | } 233 | } 234 | }); 235 | } 236 | 237 | var index = { 238 | Context: Context, 239 | IEventListener: IEventListener, 240 | EventManager: EventManager, 241 | HistoryManager: HistoryManager, 242 | Mapper: Mapper, 243 | Interface: Interface, 244 | install: install 245 | }; 246 | 247 | module.exports = index; 248 | -------------------------------------------------------------------------------- /dist/vuecommander.esm.js: -------------------------------------------------------------------------------- 1 | function _classCallCheck(instance, Constructor) { 2 | if (!(instance instanceof Constructor)) { 3 | throw new TypeError("Cannot call a class as a function"); 4 | } 5 | } 6 | 7 | function _defineProperties(target, props) { 8 | for (var i = 0; i < props.length; i++) { 9 | var descriptor = props[i]; 10 | descriptor.enumerable = descriptor.enumerable || false; 11 | descriptor.configurable = true; 12 | if ("value" in descriptor) descriptor.writable = true; 13 | Object.defineProperty(target, descriptor.key, descriptor); 14 | } 15 | } 16 | 17 | function _createClass(Constructor, protoProps, staticProps) { 18 | if (protoProps) _defineProperties(Constructor.prototype, protoProps); 19 | if (staticProps) _defineProperties(Constructor, staticProps); 20 | return Constructor; 21 | } 22 | 23 | var Interface = 24 | /*#__PURE__*/ 25 | function () { 26 | function Interface() { 27 | _classCallCheck(this, Interface); 28 | 29 | for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { 30 | args[_key] = arguments[_key]; 31 | } 32 | 33 | this.fields = args; 34 | } 35 | 36 | _createClass(Interface, [{ 37 | key: "implementedBy", 38 | value: function implementedBy(cls) { 39 | this.fields.forEach(function (arg) { 40 | if (!cls[arg]) { 41 | throw new Error("Class ".concat(cls.constructor.name, " does not implement method \"").concat(arg, "\"")); 42 | } 43 | }); 44 | } 45 | }]); 46 | 47 | return Interface; 48 | }(); 49 | 50 | var HistoryManager = 51 | /*#__PURE__*/ 52 | function () { 53 | function HistoryManager() { 54 | _classCallCheck(this, HistoryManager); 55 | 56 | this.history = []; 57 | } 58 | 59 | _createClass(HistoryManager, [{ 60 | key: "clear", 61 | value: function clear() { 62 | this.history = []; 63 | } 64 | }, { 65 | key: "push", 66 | value: function push(command) { 67 | return this.history.push(command); 68 | } 69 | }, { 70 | key: "pop", 71 | value: function pop() { 72 | return this.history.pop(); 73 | } 74 | }, { 75 | key: "length", 76 | get: function get() { 77 | return this.history.length; 78 | } 79 | }]); 80 | 81 | return HistoryManager; 82 | }(); 83 | var EventManager = 84 | /*#__PURE__*/ 85 | function () { 86 | function EventManager() { 87 | _classCallCheck(this, EventManager); 88 | 89 | this.listeners = {}; 90 | } 91 | 92 | _createClass(EventManager, [{ 93 | key: "subscribe", 94 | value: function subscribe(eventType, listener) { 95 | if (!this.listeners[eventType]) { 96 | this.listeners[eventType] = []; 97 | } 98 | 99 | this.listeners[eventType].push(listener); 100 | } 101 | }, { 102 | key: "unsubscribe", 103 | value: function unsubscribe(eventType, listener) { 104 | var listeners = this.listeners[eventType]; 105 | if (!listeners) return -1; 106 | var index = listeners.indexOf(listener); 107 | 108 | if (index > -1) { 109 | listeners.splice(index, 1); 110 | } 111 | 112 | this.listeners[eventType] = listeners; 113 | return index; 114 | } 115 | }, { 116 | key: "notify", 117 | value: function notify(eventType, data) { 118 | var listeners = this.listeners[eventType]; 119 | if (!listeners) return -1; 120 | var notified = []; 121 | listeners.forEach(function (listener) { 122 | var payload = { 123 | eventType: eventType, 124 | data: data 125 | }; 126 | listener.update(payload); 127 | notified.push({ 128 | listener: listener, 129 | eventType: eventType, 130 | data: data 131 | }); 132 | }); 133 | return notified; 134 | } 135 | }]); 136 | 137 | return EventManager; 138 | }(); 139 | var IEventListener = new Interface('update'); 140 | 141 | var Mapper = 142 | /*#__PURE__*/ 143 | function () { 144 | function Mapper(events, history) { 145 | _classCallCheck(this, Mapper); 146 | 147 | this.events = events; 148 | this.history = history; 149 | this.maps = {}; 150 | IEventListener.implementedBy(this); 151 | } 152 | 153 | _createClass(Mapper, [{ 154 | key: "update", 155 | value: function update(e) { 156 | var command = new this.maps[e.eventType](e); 157 | 158 | if (this.history && command.saveState) { 159 | command.saveState(); 160 | this.history.push(command); 161 | } 162 | 163 | command.execute(); 164 | } 165 | }, { 166 | key: "undo", 167 | value: function undo() { 168 | var command; 169 | 170 | if (this.history) { 171 | command = this.history.pop(); 172 | } 173 | 174 | if (command.undo) { 175 | this.history.pop().undo(); 176 | } 177 | } 178 | }, { 179 | key: "mapCommand", 180 | value: function mapCommand(command, eventType) { 181 | this.events.subscribe(eventType, this); 182 | this.maps[eventType] = command; 183 | } 184 | }, { 185 | key: "unmapCommand", 186 | value: function unmapCommand(command, eventType) { 187 | this.events.unsubscribe(eventType, this); 188 | delete this.maps[eventType]; 189 | } 190 | }]); 191 | 192 | return Mapper; 193 | }(); 194 | 195 | var Context = 196 | /*#__PURE__*/ 197 | function () { 198 | function Context(map) { 199 | _classCallCheck(this, Context); 200 | 201 | this.events = new EventManager(); 202 | this.history = new HistoryManager(); 203 | this.mapper = new Mapper(this.events, this.history); 204 | this.mapCommands(map); 205 | } 206 | 207 | _createClass(Context, [{ 208 | key: "mapCommands", 209 | value: function mapCommands(map) { 210 | var _this = this; 211 | 212 | Object.keys(map).forEach(function (key, index) { 213 | _this.mapper.mapCommand(map[key], key); 214 | }); 215 | } 216 | }]); 217 | 218 | return Context; 219 | }(); 220 | 221 | function install (Vue) { 222 | Vue.mixin({ 223 | beforeCreate: function beforeCreate() { 224 | var options = this.$options; 225 | 226 | if (options.context) { 227 | this.$context = typeof options.context === 'function' ? options.context() : options.context; 228 | } else if (options.parent && options.parent.$context) { 229 | this.$context = options.parent.$context; 230 | } 231 | } 232 | }); 233 | } 234 | 235 | var index = { 236 | Context: Context, 237 | IEventListener: IEventListener, 238 | EventManager: EventManager, 239 | HistoryManager: HistoryManager, 240 | Mapper: Mapper, 241 | Interface: Interface, 242 | install: install 243 | }; 244 | 245 | export default index; 246 | -------------------------------------------------------------------------------- /dist/vuecommander.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 3 | typeof define === 'function' && define.amd ? define(factory) : 4 | (global = global || self, global.VueCommander = factory()); 5 | }(this, function () { 'use strict'; 6 | 7 | function _classCallCheck(instance, Constructor) { 8 | if (!(instance instanceof Constructor)) { 9 | throw new TypeError("Cannot call a class as a function"); 10 | } 11 | } 12 | 13 | function _defineProperties(target, props) { 14 | for (var i = 0; i < props.length; i++) { 15 | var descriptor = props[i]; 16 | descriptor.enumerable = descriptor.enumerable || false; 17 | descriptor.configurable = true; 18 | if ("value" in descriptor) descriptor.writable = true; 19 | Object.defineProperty(target, descriptor.key, descriptor); 20 | } 21 | } 22 | 23 | function _createClass(Constructor, protoProps, staticProps) { 24 | if (protoProps) _defineProperties(Constructor.prototype, protoProps); 25 | if (staticProps) _defineProperties(Constructor, staticProps); 26 | return Constructor; 27 | } 28 | 29 | var Interface = 30 | /*#__PURE__*/ 31 | function () { 32 | function Interface() { 33 | _classCallCheck(this, Interface); 34 | 35 | for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { 36 | args[_key] = arguments[_key]; 37 | } 38 | 39 | this.fields = args; 40 | } 41 | 42 | _createClass(Interface, [{ 43 | key: "implementedBy", 44 | value: function implementedBy(cls) { 45 | this.fields.forEach(function (arg) { 46 | if (!cls[arg]) { 47 | throw new Error("Class ".concat(cls.constructor.name, " does not implement method \"").concat(arg, "\"")); 48 | } 49 | }); 50 | } 51 | }]); 52 | 53 | return Interface; 54 | }(); 55 | 56 | var HistoryManager = 57 | /*#__PURE__*/ 58 | function () { 59 | function HistoryManager() { 60 | _classCallCheck(this, HistoryManager); 61 | 62 | this.history = []; 63 | } 64 | 65 | _createClass(HistoryManager, [{ 66 | key: "clear", 67 | value: function clear() { 68 | this.history = []; 69 | } 70 | }, { 71 | key: "push", 72 | value: function push(command) { 73 | return this.history.push(command); 74 | } 75 | }, { 76 | key: "pop", 77 | value: function pop() { 78 | return this.history.pop(); 79 | } 80 | }, { 81 | key: "length", 82 | get: function get() { 83 | return this.history.length; 84 | } 85 | }]); 86 | 87 | return HistoryManager; 88 | }(); 89 | var EventManager = 90 | /*#__PURE__*/ 91 | function () { 92 | function EventManager() { 93 | _classCallCheck(this, EventManager); 94 | 95 | this.listeners = {}; 96 | } 97 | 98 | _createClass(EventManager, [{ 99 | key: "subscribe", 100 | value: function subscribe(eventType, listener) { 101 | if (!this.listeners[eventType]) { 102 | this.listeners[eventType] = []; 103 | } 104 | 105 | this.listeners[eventType].push(listener); 106 | } 107 | }, { 108 | key: "unsubscribe", 109 | value: function unsubscribe(eventType, listener) { 110 | var listeners = this.listeners[eventType]; 111 | if (!listeners) return -1; 112 | var index = listeners.indexOf(listener); 113 | 114 | if (index > -1) { 115 | listeners.splice(index, 1); 116 | } 117 | 118 | this.listeners[eventType] = listeners; 119 | return index; 120 | } 121 | }, { 122 | key: "notify", 123 | value: function notify(eventType, data) { 124 | var listeners = this.listeners[eventType]; 125 | if (!listeners) return -1; 126 | var notified = []; 127 | listeners.forEach(function (listener) { 128 | var payload = { 129 | eventType: eventType, 130 | data: data 131 | }; 132 | listener.update(payload); 133 | notified.push({ 134 | listener: listener, 135 | eventType: eventType, 136 | data: data 137 | }); 138 | }); 139 | return notified; 140 | } 141 | }]); 142 | 143 | return EventManager; 144 | }(); 145 | var IEventListener = new Interface('update'); 146 | 147 | var Mapper = 148 | /*#__PURE__*/ 149 | function () { 150 | function Mapper(events, history) { 151 | _classCallCheck(this, Mapper); 152 | 153 | this.events = events; 154 | this.history = history; 155 | this.maps = {}; 156 | IEventListener.implementedBy(this); 157 | } 158 | 159 | _createClass(Mapper, [{ 160 | key: "update", 161 | value: function update(e) { 162 | var command = new this.maps[e.eventType](e); 163 | 164 | if (this.history && command.saveState) { 165 | command.saveState(); 166 | this.history.push(command); 167 | } 168 | 169 | command.execute(); 170 | } 171 | }, { 172 | key: "undo", 173 | value: function undo() { 174 | var command; 175 | 176 | if (this.history) { 177 | command = this.history.pop(); 178 | } 179 | 180 | if (command.undo) { 181 | this.history.pop().undo(); 182 | } 183 | } 184 | }, { 185 | key: "mapCommand", 186 | value: function mapCommand(command, eventType) { 187 | this.events.subscribe(eventType, this); 188 | this.maps[eventType] = command; 189 | } 190 | }, { 191 | key: "unmapCommand", 192 | value: function unmapCommand(command, eventType) { 193 | this.events.unsubscribe(eventType, this); 194 | delete this.maps[eventType]; 195 | } 196 | }]); 197 | 198 | return Mapper; 199 | }(); 200 | 201 | var Context = 202 | /*#__PURE__*/ 203 | function () { 204 | function Context(map) { 205 | _classCallCheck(this, Context); 206 | 207 | this.events = new EventManager(); 208 | this.history = new HistoryManager(); 209 | this.mapper = new Mapper(this.events, this.history); 210 | this.mapCommands(map); 211 | } 212 | 213 | _createClass(Context, [{ 214 | key: "mapCommands", 215 | value: function mapCommands(map) { 216 | var _this = this; 217 | 218 | Object.keys(map).forEach(function (key, index) { 219 | _this.mapper.mapCommand(map[key], key); 220 | }); 221 | } 222 | }]); 223 | 224 | return Context; 225 | }(); 226 | 227 | function install (Vue) { 228 | Vue.mixin({ 229 | beforeCreate: function beforeCreate() { 230 | var options = this.$options; 231 | 232 | if (options.context) { 233 | this.$context = typeof options.context === 'function' ? options.context() : options.context; 234 | } else if (options.parent && options.parent.$context) { 235 | this.$context = options.parent.$context; 236 | } 237 | } 238 | }); 239 | } 240 | 241 | var index = { 242 | Context: Context, 243 | IEventListener: IEventListener, 244 | EventManager: EventManager, 245 | HistoryManager: HistoryManager, 246 | Mapper: Mapper, 247 | Interface: Interface, 248 | install: install 249 | }; 250 | 251 | return index; 252 | 253 | })); 254 | -------------------------------------------------------------------------------- /dist/vuecommander.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).VueCommander=e()}(this,function(){"use strict";function s(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(t,e){for(var n=0;n 2 |
3 |
4 |
5 |
6 | Image 7 |
8 |
9 |
10 |
11 |

12 | John Smith @johnsmith 31m 13 |
14 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean efficitur sit amet massa fringilla egestas. Nullam condimentum luctus turpis. 15 |

16 |
17 | 39 |
40 | 41 |
42 |
43 | 44 | -------------------------------------------------------------------------------- /examples/counter/app.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import context from './Context'; 3 | import Counter from './Counter.vue'; 4 | 5 | new Vue({ 6 | el: '#app', 7 | context: context, 8 | render: h => h(Counter) 9 | }); -------------------------------------------------------------------------------- /examples/counter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Examples 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |

Examples

13 |
14 |
15 | 31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/dist/counter.js: -------------------------------------------------------------------------------- 1 | /******/ (function(modules) { // webpackBootstrap 2 | /******/ // install a JSONP callback for chunk loading 3 | /******/ function webpackJsonpCallback(data) { 4 | /******/ var chunkIds = data[0]; 5 | /******/ var moreModules = data[1]; 6 | /******/ var executeModules = data[2]; 7 | /******/ 8 | /******/ // add "moreModules" to the modules object, 9 | /******/ // then flag all "chunkIds" as loaded and fire callback 10 | /******/ var moduleId, chunkId, i = 0, resolves = []; 11 | /******/ for(;i < chunkIds.length; i++) { 12 | /******/ chunkId = chunkIds[i]; 13 | /******/ if(installedChunks[chunkId]) { 14 | /******/ resolves.push(installedChunks[chunkId][0]); 15 | /******/ } 16 | /******/ installedChunks[chunkId] = 0; 17 | /******/ } 18 | /******/ for(moduleId in moreModules) { 19 | /******/ if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { 20 | /******/ modules[moduleId] = moreModules[moduleId]; 21 | /******/ } 22 | /******/ } 23 | /******/ if(parentJsonpFunction) parentJsonpFunction(data); 24 | /******/ 25 | /******/ while(resolves.length) { 26 | /******/ resolves.shift()(); 27 | /******/ } 28 | /******/ 29 | /******/ // add entry modules from loaded chunk to deferred list 30 | /******/ deferredModules.push.apply(deferredModules, executeModules || []); 31 | /******/ 32 | /******/ // run deferred modules when all chunks ready 33 | /******/ return checkDeferredModules(); 34 | /******/ }; 35 | /******/ function checkDeferredModules() { 36 | /******/ var result; 37 | /******/ for(var i = 0; i < deferredModules.length; i++) { 38 | /******/ var deferredModule = deferredModules[i]; 39 | /******/ var fulfilled = true; 40 | /******/ for(var j = 1; j < deferredModule.length; j++) { 41 | /******/ var depId = deferredModule[j]; 42 | /******/ if(installedChunks[depId] !== 0) fulfilled = false; 43 | /******/ } 44 | /******/ if(fulfilled) { 45 | /******/ deferredModules.splice(i--, 1); 46 | /******/ result = __webpack_require__(__webpack_require__.s = deferredModule[0]); 47 | /******/ } 48 | /******/ } 49 | /******/ return result; 50 | /******/ } 51 | /******/ 52 | /******/ // The module cache 53 | /******/ var installedModules = {}; 54 | /******/ 55 | /******/ // object to store loaded and loading chunks 56 | /******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched 57 | /******/ // Promise = chunk loading, 0 = chunk loaded 58 | /******/ var installedChunks = { 59 | /******/ "counter": 0 60 | /******/ }; 61 | /******/ 62 | /******/ var deferredModules = []; 63 | /******/ 64 | /******/ // The require function 65 | /******/ function __webpack_require__(moduleId) { 66 | /******/ 67 | /******/ // Check if module is in cache 68 | /******/ if(installedModules[moduleId]) { 69 | /******/ return installedModules[moduleId].exports; 70 | /******/ } 71 | /******/ // Create a new module (and put it into the cache) 72 | /******/ var module = installedModules[moduleId] = { 73 | /******/ i: moduleId, 74 | /******/ l: false, 75 | /******/ exports: {} 76 | /******/ }; 77 | /******/ 78 | /******/ // Execute the module function 79 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 80 | /******/ 81 | /******/ // Flag the module as loaded 82 | /******/ module.l = true; 83 | /******/ 84 | /******/ // Return the exports of the module 85 | /******/ return module.exports; 86 | /******/ } 87 | /******/ 88 | /******/ 89 | /******/ // expose the modules object (__webpack_modules__) 90 | /******/ __webpack_require__.m = modules; 91 | /******/ 92 | /******/ // expose the module cache 93 | /******/ __webpack_require__.c = installedModules; 94 | /******/ 95 | /******/ // define getter function for harmony exports 96 | /******/ __webpack_require__.d = function(exports, name, getter) { 97 | /******/ if(!__webpack_require__.o(exports, name)) { 98 | /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); 99 | /******/ } 100 | /******/ }; 101 | /******/ 102 | /******/ // define __esModule on exports 103 | /******/ __webpack_require__.r = function(exports) { 104 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { 105 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 106 | /******/ } 107 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 108 | /******/ }; 109 | /******/ 110 | /******/ // create a fake namespace object 111 | /******/ // mode & 1: value is a module id, require it 112 | /******/ // mode & 2: merge all properties of value into the ns 113 | /******/ // mode & 4: return value when already ns object 114 | /******/ // mode & 8|1: behave like require 115 | /******/ __webpack_require__.t = function(value, mode) { 116 | /******/ if(mode & 1) value = __webpack_require__(value); 117 | /******/ if(mode & 8) return value; 118 | /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; 119 | /******/ var ns = Object.create(null); 120 | /******/ __webpack_require__.r(ns); 121 | /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); 122 | /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); 123 | /******/ return ns; 124 | /******/ }; 125 | /******/ 126 | /******/ // getDefaultExport function for compatibility with non-harmony modules 127 | /******/ __webpack_require__.n = function(module) { 128 | /******/ var getter = module && module.__esModule ? 129 | /******/ function getDefault() { return module['default']; } : 130 | /******/ function getModuleExports() { return module; }; 131 | /******/ __webpack_require__.d(getter, 'a', getter); 132 | /******/ return getter; 133 | /******/ }; 134 | /******/ 135 | /******/ // Object.prototype.hasOwnProperty.call 136 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 137 | /******/ 138 | /******/ // __webpack_public_path__ 139 | /******/ __webpack_require__.p = ""; 140 | /******/ 141 | /******/ var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || []; 142 | /******/ var oldJsonpFunction = jsonpArray.push.bind(jsonpArray); 143 | /******/ jsonpArray.push = webpackJsonpCallback; 144 | /******/ jsonpArray = jsonpArray.slice(); 145 | /******/ for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]); 146 | /******/ var parentJsonpFunction = oldJsonpFunction; 147 | /******/ 148 | /******/ 149 | /******/ // add entry module to deferred list 150 | /******/ deferredModules.push(["./examples/counter/app.js","shared"]); 151 | /******/ // run deferred modules when ready 152 | /******/ return checkDeferredModules(); 153 | /******/ }) 154 | /************************************************************************/ 155 | /******/ ([]); -------------------------------------------------------------------------------- /examples/dist/todos.js: -------------------------------------------------------------------------------- 1 | /******/ (function(modules) { // webpackBootstrap 2 | /******/ // install a JSONP callback for chunk loading 3 | /******/ function webpackJsonpCallback(data) { 4 | /******/ var chunkIds = data[0]; 5 | /******/ var moreModules = data[1]; 6 | /******/ var executeModules = data[2]; 7 | /******/ 8 | /******/ // add "moreModules" to the modules object, 9 | /******/ // then flag all "chunkIds" as loaded and fire callback 10 | /******/ var moduleId, chunkId, i = 0, resolves = []; 11 | /******/ for(;i < chunkIds.length; i++) { 12 | /******/ chunkId = chunkIds[i]; 13 | /******/ if(installedChunks[chunkId]) { 14 | /******/ resolves.push(installedChunks[chunkId][0]); 15 | /******/ } 16 | /******/ installedChunks[chunkId] = 0; 17 | /******/ } 18 | /******/ for(moduleId in moreModules) { 19 | /******/ if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { 20 | /******/ modules[moduleId] = moreModules[moduleId]; 21 | /******/ } 22 | /******/ } 23 | /******/ if(parentJsonpFunction) parentJsonpFunction(data); 24 | /******/ 25 | /******/ while(resolves.length) { 26 | /******/ resolves.shift()(); 27 | /******/ } 28 | /******/ 29 | /******/ // add entry modules from loaded chunk to deferred list 30 | /******/ deferredModules.push.apply(deferredModules, executeModules || []); 31 | /******/ 32 | /******/ // run deferred modules when all chunks ready 33 | /******/ return checkDeferredModules(); 34 | /******/ }; 35 | /******/ function checkDeferredModules() { 36 | /******/ var result; 37 | /******/ for(var i = 0; i < deferredModules.length; i++) { 38 | /******/ var deferredModule = deferredModules[i]; 39 | /******/ var fulfilled = true; 40 | /******/ for(var j = 1; j < deferredModule.length; j++) { 41 | /******/ var depId = deferredModule[j]; 42 | /******/ if(installedChunks[depId] !== 0) fulfilled = false; 43 | /******/ } 44 | /******/ if(fulfilled) { 45 | /******/ deferredModules.splice(i--, 1); 46 | /******/ result = __webpack_require__(__webpack_require__.s = deferredModule[0]); 47 | /******/ } 48 | /******/ } 49 | /******/ return result; 50 | /******/ } 51 | /******/ 52 | /******/ // The module cache 53 | /******/ var installedModules = {}; 54 | /******/ 55 | /******/ // object to store loaded and loading chunks 56 | /******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched 57 | /******/ // Promise = chunk loading, 0 = chunk loaded 58 | /******/ var installedChunks = { 59 | /******/ "todos": 0 60 | /******/ }; 61 | /******/ 62 | /******/ var deferredModules = []; 63 | /******/ 64 | /******/ // The require function 65 | /******/ function __webpack_require__(moduleId) { 66 | /******/ 67 | /******/ // Check if module is in cache 68 | /******/ if(installedModules[moduleId]) { 69 | /******/ return installedModules[moduleId].exports; 70 | /******/ } 71 | /******/ // Create a new module (and put it into the cache) 72 | /******/ var module = installedModules[moduleId] = { 73 | /******/ i: moduleId, 74 | /******/ l: false, 75 | /******/ exports: {} 76 | /******/ }; 77 | /******/ 78 | /******/ // Execute the module function 79 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 80 | /******/ 81 | /******/ // Flag the module as loaded 82 | /******/ module.l = true; 83 | /******/ 84 | /******/ // Return the exports of the module 85 | /******/ return module.exports; 86 | /******/ } 87 | /******/ 88 | /******/ 89 | /******/ // expose the modules object (__webpack_modules__) 90 | /******/ __webpack_require__.m = modules; 91 | /******/ 92 | /******/ // expose the module cache 93 | /******/ __webpack_require__.c = installedModules; 94 | /******/ 95 | /******/ // define getter function for harmony exports 96 | /******/ __webpack_require__.d = function(exports, name, getter) { 97 | /******/ if(!__webpack_require__.o(exports, name)) { 98 | /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); 99 | /******/ } 100 | /******/ }; 101 | /******/ 102 | /******/ // define __esModule on exports 103 | /******/ __webpack_require__.r = function(exports) { 104 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { 105 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 106 | /******/ } 107 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 108 | /******/ }; 109 | /******/ 110 | /******/ // create a fake namespace object 111 | /******/ // mode & 1: value is a module id, require it 112 | /******/ // mode & 2: merge all properties of value into the ns 113 | /******/ // mode & 4: return value when already ns object 114 | /******/ // mode & 8|1: behave like require 115 | /******/ __webpack_require__.t = function(value, mode) { 116 | /******/ if(mode & 1) value = __webpack_require__(value); 117 | /******/ if(mode & 8) return value; 118 | /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; 119 | /******/ var ns = Object.create(null); 120 | /******/ __webpack_require__.r(ns); 121 | /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); 122 | /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); 123 | /******/ return ns; 124 | /******/ }; 125 | /******/ 126 | /******/ // getDefaultExport function for compatibility with non-harmony modules 127 | /******/ __webpack_require__.n = function(module) { 128 | /******/ var getter = module && module.__esModule ? 129 | /******/ function getDefault() { return module['default']; } : 130 | /******/ function getModuleExports() { return module; }; 131 | /******/ __webpack_require__.d(getter, 'a', getter); 132 | /******/ return getter; 133 | /******/ }; 134 | /******/ 135 | /******/ // Object.prototype.hasOwnProperty.call 136 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 137 | /******/ 138 | /******/ // __webpack_public_path__ 139 | /******/ __webpack_require__.p = ""; 140 | /******/ 141 | /******/ var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || []; 142 | /******/ var oldJsonpFunction = jsonpArray.push.bind(jsonpArray); 143 | /******/ jsonpArray.push = webpackJsonpCallback; 144 | /******/ jsonpArray = jsonpArray.slice(); 145 | /******/ for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]); 146 | /******/ var parentJsonpFunction = oldJsonpFunction; 147 | /******/ 148 | /******/ 149 | /******/ // add entry module to deferred list 150 | /******/ deferredModules.push(["./examples/todos/app.js","shared"]); 151 | /******/ // run deferred modules when ready 152 | /******/ return checkDeferredModules(); 153 | /******/ }) 154 | /************************************************************************/ 155 | /******/ ([]); -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Examples 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const webpack = require('webpack') 3 | const webpackDevMiddleware = require('webpack-dev-middleware') 4 | const webpackHotMiddleware = require('webpack-hot-middleware') 5 | const WebpackConfig = require('./webpack.config') 6 | 7 | const app = express() 8 | const compiler = webpack(WebpackConfig) 9 | 10 | app.use(webpackDevMiddleware(compiler, { 11 | publicPath: '/__build__/', 12 | stats: { 13 | colors: true, 14 | chunks: false 15 | } 16 | })) 17 | 18 | app.use(webpackHotMiddleware(compiler)) 19 | 20 | app.use(express.static(__dirname)) 21 | 22 | const port = process.env.PORT || 8080 23 | module.exports = app.listen(port, () => { 24 | console.log(`Server listening on http://localhost:${port}, Ctrl+C to stop`) 25 | }) -------------------------------------------------------------------------------- /examples/todos/Context.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { TodoDoneCommand, TodoAddCommand, TodoDeleteCommand } from './TodoCommands'; 3 | import VueCommander from 'vuecommander'; 4 | 5 | Vue.use(VueCommander); 6 | 7 | export default new VueCommander.Context({ 8 | 'todo.done': TodoDoneCommand, 9 | 'todo.submitted': TodoAddCommand, 10 | 'todo.delete': TodoDeleteCommand, 11 | }); -------------------------------------------------------------------------------- /examples/todos/Todo.vue: -------------------------------------------------------------------------------- 1 | 30 | -------------------------------------------------------------------------------- /examples/todos/TodoCommands.js: -------------------------------------------------------------------------------- 1 | import TodosModel from './TodosModel'; 2 | 3 | function remapIndices(todos) { 4 | return todos.map((todo, index) => { 5 | todo.id = index; 6 | return todo; 7 | }); 8 | } 9 | 10 | export class TodoDoneCommand { 11 | constructor(e) { 12 | this.backup; 13 | this.event = e; 14 | this.todo = this.event.data; 15 | } 16 | 17 | saveState() { 18 | this.backup = TodosModel.todos[this.todo.id].done; 19 | } 20 | 21 | undo() { 22 | TodosModel.todos[this.todo.id].done = this.backup; 23 | } 24 | 25 | execute() { 26 | TodosModel.todos[this.todo.id].done = !TodosModel.todos[this.todo.id].done; 27 | } 28 | } 29 | 30 | export class TodoAddCommand { 31 | constructor(e) { 32 | this.backup; 33 | this.event = e; 34 | } 35 | 36 | saveState() { 37 | this.backup = TodosModel.todos.length; 38 | } 39 | 40 | undo() { 41 | TodosModel.todos.splice(this.backup, 1); 42 | TodosModel.todos = remapIndices(TodosModel.todos); 43 | } 44 | 45 | execute() { 46 | const todo = { 47 | id: TodosModel.todos.length, 48 | content: this.event.data, 49 | done: false, 50 | } 51 | TodosModel.todos.push(todo); 52 | TodosModel.currentInput = ''; 53 | } 54 | } 55 | 56 | export class TodoDeleteCommand { 57 | constructor(e) { 58 | this.backup; 59 | this.event = e; 60 | } 61 | 62 | saveState() { 63 | this.backup = TodosModel.todos[this.event.data.id]; 64 | } 65 | 66 | undo() { 67 | TodosModel.todos.splice(this.backup.id, 0, this.backup); 68 | TodosModel.todos = remapIndices(TodosModel.todos); 69 | } 70 | 71 | execute() { 72 | TodosModel.todos.splice(this.event.data.id, 1); 73 | TodosModel.todos = remapIndices(TodosModel.todos); 74 | } 75 | } -------------------------------------------------------------------------------- /examples/todos/Todos.vue: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /examples/todos/TodosModel.js: -------------------------------------------------------------------------------- 1 | export default { 2 | currentInput: "", 3 | todos: [{ 4 | id: 0, 5 | content: "Make sure to do the thing", 6 | done: false 7 | }] 8 | } -------------------------------------------------------------------------------- /examples/todos/app.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import context from './Context'; 3 | import Todos from './Todos.vue'; 4 | 5 | new Vue({ 6 | el: '#app', 7 | context: context, 8 | render: h => h(Todos), 9 | }); -------------------------------------------------------------------------------- /examples/todos/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Examples 5 | 6 | 7 | 8 | 13 | 14 | 15 |
16 |
17 |

Examples

18 |
19 |
20 | 36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /examples/webpack.config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const webpack = require('webpack') 4 | const VueLoaderPlugin = require('vue-loader/lib/plugin') 5 | 6 | module.exports = { 7 | mode: 'development', 8 | 9 | entry: fs.readdirSync(__dirname).reduce((entries, dir) => { 10 | const fullDir = path.join(__dirname, dir) 11 | const entry = path.join(fullDir, 'app.js') 12 | if (fs.statSync(fullDir).isDirectory() && fs.existsSync(entry)) { 13 | entries[dir] = ['webpack-hot-middleware/client', entry] 14 | } 15 | 16 | return entries 17 | }, {}), 18 | 19 | output: { 20 | path: path.join(__dirname, '__build__'), 21 | filename: '[name].js', 22 | chunkFilename: '[id].chunk.js', 23 | publicPath: '/__build__/' 24 | }, 25 | 26 | module: { 27 | rules: [ 28 | { test: /\.js$/, exclude: /node_modules/, use: ['babel-loader'] }, 29 | { test: /\.vue$/, use: ['vue-loader'] }, 30 | { test: /\.css$/, use: ['vue-style-loader', 'css-loader'] } 31 | ] 32 | }, 33 | 34 | resolve: { 35 | alias: { 36 | vuecommander: path.resolve(__dirname, '../dist/vuecommander.esm.js') 37 | } 38 | }, 39 | 40 | optimization: { 41 | splitChunks: { 42 | cacheGroups: { 43 | vendors: { 44 | name: 'shared', 45 | filename: 'shared.js', 46 | chunks: 'initial' 47 | } 48 | } 49 | } 50 | }, 51 | 52 | plugins: [ 53 | new VueLoaderPlugin(), 54 | new webpack.HotModuleReplacementPlugin(), 55 | new webpack.NoEmitOnErrorsPlugin() 56 | ] 57 | 58 | } -------------------------------------------------------------------------------- /examples/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const webpack = require('webpack') 4 | const VueLoaderPlugin = require('vue-loader/lib/plugin') 5 | 6 | module.exports = { 7 | mode: 'development', 8 | 9 | entry: fs.readdirSync(__dirname).reduce((entries, dir) => { 10 | const fullDir = path.join(__dirname, dir) 11 | const entry = path.join(fullDir, 'app.js') 12 | if (fs.statSync(fullDir).isDirectory() && fs.existsSync(entry)) { 13 | entries[dir] = entry 14 | } 15 | 16 | return entries 17 | }, {}), 18 | 19 | output: { 20 | path: path.join(__dirname, 'dist'), 21 | filename: '[name].js', 22 | chunkFilename: '[id].chunk.js', 23 | }, 24 | 25 | module: { 26 | rules: [ 27 | { test: /\.js$/, exclude: /node_modules/, use: ['babel-loader'] }, 28 | { test: /\.vue$/, use: ['vue-loader'] }, 29 | { test: /\.css$/, use: ['vue-style-loader', 'css-loader'] } 30 | ] 31 | }, 32 | 33 | resolve: { 34 | alias: { 35 | vuecommander: path.resolve(__dirname, '../dist/vuecommander.esm.js') 36 | } 37 | }, 38 | 39 | optimization: { 40 | splitChunks: { 41 | cacheGroups: { 42 | vendors: { 43 | name: 'shared', 44 | filename: 'shared.js', 45 | chunks: 'initial' 46 | } 47 | } 48 | } 49 | }, 50 | 51 | plugins: [ 52 | new VueLoaderPlugin(), 53 | new webpack.NoEmitOnErrorsPlugin() 54 | ] 55 | 56 | } -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jschatz1/vuecommander/b79afaf915d99cc7d678ffac779eff567056e6f4/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuecommander", 3 | "version": "0.0.12", 4 | "description": "state management and application framework for vuejs", 5 | "main": "dist/vuecommander.common.js", 6 | "module": "dist/vuecommander.esm.js", 7 | "unpkg": "dist/vuecommander.js", 8 | "files": [ 9 | "dist" 10 | ], 11 | "repository": "https://github.com/jschatz1/vuec", 12 | "scripts": { 13 | "build": "rollup --config config/rollup.config.js", 14 | "build:dev": "rollup --config config/rollup.config.common.js", 15 | "build:examples": "yarn webpack --config examples/webpack.config.prod.js", 16 | "dev": "npm run build:dev && node examples/server.js", 17 | "test": "npm run build:dev && mocha test/unit" 18 | }, 19 | "author": "Jacob Schatz", 20 | "license": "MIT", 21 | "private": false, 22 | "devDependencies": { 23 | "@babel/core": "^7.2.2", 24 | "@babel/preset-env": "^7.3.1", 25 | "babel-loader": "^8.0.5", 26 | "express": "^4.16.4", 27 | "mocha": "^5.2.0", 28 | "rollup": "^1.1.2", 29 | "rollup-plugin-babel": "^4.3.2", 30 | "rollup-plugin-commonjs": "^9.2.0", 31 | "rollup-plugin-node-resolve": "^4.0.0", 32 | "rollup-plugin-uglify": "^6.0.2", 33 | "rollup-plugin-vue": "^4.7.1", 34 | "vue": "^2.6.4", 35 | "vue-loader": "^15.6.2", 36 | "vue-template-compiler": "^2.6.4", 37 | "webpack": "^4.29.3", 38 | "webpack-cli": "^3.2.3", 39 | "webpack-dev-middleware": "^3.5.2", 40 | "webpack-hot-middleware": "^2.24.3" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/env", {"modules": false}] 4 | ] 5 | } -------------------------------------------------------------------------------- /src/Context.js: -------------------------------------------------------------------------------- 1 | import { EventManager, HistoryManager } from './EventManager'; 2 | import Mapper from './Mapper'; 3 | 4 | export default class Context { 5 | constructor(map) { 6 | this.events = new EventManager(); 7 | this.history = new HistoryManager(); 8 | this.mapper = new Mapper(this.events, this.history); 9 | this.mapCommands(map); 10 | } 11 | 12 | mapCommands(map) { 13 | Object.keys(map).forEach((key,index) => { 14 | this.mapper.mapCommand(map[key], key); 15 | }); 16 | } 17 | } -------------------------------------------------------------------------------- /src/EventManager.js: -------------------------------------------------------------------------------- 1 | import Interface from './Interface'; 2 | 3 | export class HistoryManager { 4 | constructor() { 5 | this.history = []; 6 | } 7 | 8 | get length() { 9 | return this.history.length; 10 | } 11 | 12 | clear() { 13 | this.history = []; 14 | } 15 | 16 | push(command) { 17 | return this.history.push(command); 18 | } 19 | 20 | pop() { 21 | return this.history.pop(); 22 | } 23 | } 24 | 25 | export class EventManager { 26 | 27 | constructor() { 28 | this.listeners = {}; 29 | } 30 | 31 | subscribe(eventType, listener) { 32 | if (!this.listeners[eventType]) { 33 | this.listeners[eventType] = []; 34 | } 35 | this.listeners[eventType].push(listener); 36 | } 37 | 38 | unsubscribe(eventType, listener) { 39 | const listeners = this.listeners[eventType]; 40 | if (!listeners) return -1; 41 | const index = listeners.indexOf(listener); 42 | if (index > -1) { 43 | listeners.splice(index, 1); 44 | } 45 | this.listeners[eventType] = listeners; 46 | return index; 47 | } 48 | 49 | notify(eventType, data) { 50 | const listeners = this.listeners[eventType]; 51 | if (!listeners) return -1; 52 | const notified = []; 53 | listeners.forEach((listener) => { 54 | const payload = { 55 | eventType, 56 | data, 57 | }; 58 | listener.update(payload); 59 | notified.push({ 60 | listener, 61 | eventType, 62 | data, 63 | }); 64 | }); 65 | return notified; 66 | } 67 | } 68 | 69 | export const IEventListener = new Interface('update'); 70 | -------------------------------------------------------------------------------- /src/Interface.js: -------------------------------------------------------------------------------- 1 | export default class Interface { 2 | constructor(...args) { 3 | this.fields = args; 4 | } 5 | 6 | implementedBy(cls) { 7 | this.fields.forEach((arg) => { 8 | if (!cls[arg]) { 9 | throw new Error( 10 | `Class ${cls.constructor.name} does not implement method "${arg}"`, 11 | ); 12 | } 13 | }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Mapper.js: -------------------------------------------------------------------------------- 1 | import { IEventListener } from './EventManager'; 2 | 3 | export default class Mapper { 4 | constructor(events, history) { 5 | this.events = events; 6 | this.history = history; 7 | this.maps = {}; 8 | IEventListener.implementedBy(this); 9 | } 10 | 11 | update(e) { 12 | const command = new this.maps[e.eventType](e); 13 | if(this.history && command.saveState) { 14 | command.saveState(); 15 | this.history.push(command); 16 | } 17 | command.execute(); 18 | } 19 | 20 | undo() { 21 | let command; 22 | if(this.history) { 23 | command = this.history.pop(); 24 | } 25 | if(command.undo) { 26 | this.history.pop().undo(); 27 | } 28 | } 29 | 30 | mapCommand(command, eventType) { 31 | this.events.subscribe(eventType, this); 32 | this.maps[eventType] = command; 33 | } 34 | 35 | unmapCommand(command, eventType) { 36 | this.events.unsubscribe(eventType, this); 37 | delete this.maps[eventType]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Mapper from './Mapper'; 2 | import Interface from './Interface'; 3 | import Context from './Context'; 4 | import install from './install'; 5 | import { IEventListener, EventManager, HistoryManager } from './EventManager'; 6 | 7 | export default { 8 | Context, 9 | IEventListener, 10 | EventManager, 11 | HistoryManager, 12 | Mapper, 13 | Interface, 14 | install, 15 | } -------------------------------------------------------------------------------- /src/install.js: -------------------------------------------------------------------------------- 1 | export default function (Vue) { 2 | Vue.mixin({ beforeCreate: function() { 3 | const options = this.$options 4 | if (options.context) { 5 | this.$context = typeof options.context === 'function' 6 | ? options.context() 7 | : options.context 8 | } else if (options.parent && options.parent.$context) { 9 | this.$context = options.parent.$context; 10 | } 11 | }}); 12 | } -------------------------------------------------------------------------------- /test/unit/context.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const vuecommander = require('../../dist/vuecommander.common.js'); 4 | 5 | var assert = require('assert'); 6 | describe('Context', function() { 7 | describe('New Context', function() { 8 | 9 | function Command(e) { 10 | this.event = e; 11 | this.backup; 12 | } 13 | 14 | Command.prototype.saveState = function(state) { 15 | this.backup = 0; 16 | } 17 | 18 | Command.prototype.undo = function() { 19 | 20 | } 21 | 22 | Command.prototype.execute = function() { 23 | result += 1; 24 | if(!this.event.data) return 25 | resultFromEvent += this.event.data.num; 26 | } 27 | 28 | var context = new vuecommander.Context({ 29 | 'context.works': Command, 30 | }); 31 | 32 | it('Context should map event', function() { 33 | assert.strictEqual(context.mapper.maps['context.works'], Command); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/unit/eventManager.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const vuecommander = require('../../dist/vuecommander.common.js'); 4 | 5 | var assert = require('assert'); 6 | describe('EventManager', function() { 7 | describe('implemented', function() { 8 | var eventManager = new vuecommander.EventManager(); 9 | var listener; 10 | var result = 0; 11 | function Listener() { 12 | vuecommander.IEventListener.implementedBy(this) 13 | } 14 | Listener.prototype.update = function(e){ 15 | result += e.data; 16 | } 17 | 18 | it('Should not throw an error when IEventListener implemented properly', function() { 19 | listener = new Listener(); 20 | assert.equal("Listener", listener.constructor.name); 21 | }); 22 | 23 | it('Should update when subscribed event is notified', function() { 24 | assert.equal(result, 0); 25 | listener = new Listener(); 26 | eventManager.subscribe('eventmanager.works', listener); 27 | eventManager.notify('eventmanager.works', 1); 28 | assert.equal(result, 1); 29 | result = 0; 30 | }); 31 | 32 | it('Should not update when unsubscribed event is notified', function() { 33 | assert.equal(result, 0); 34 | listener = new Listener(); 35 | eventManager.subscribe('eventmanager.works', listener); 36 | eventManager.unsubscribe('eventmanager.works', listener); 37 | eventManager.notify('eventmanager.works', 7); 38 | assert.equal(result, 7); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/unit/history.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const vuecommander = require('../../dist/vuecommander.common.js'); 4 | 5 | var assert = require('assert'); 6 | describe('History', function() { 7 | describe('mapCommand', function() { 8 | var result = 0; 9 | var resultFromEvent = 0; 10 | var eventManager = new vuecommander.EventManager(); 11 | var historyManager = new vuecommander.HistoryManager(); 12 | var mapper = new vuecommander.Mapper(eventManager, historyManager); 13 | function Command(e) { 14 | this.event = e; 15 | this.backup; 16 | } 17 | 18 | Command.prototype.saveState = function(state) { 19 | this.backup = 0; 20 | } 21 | 22 | Command.prototype.undo = function() { 23 | 24 | } 25 | 26 | Command.prototype.execute = function() { 27 | result += 1; 28 | if(!this.event.data) return 29 | resultFromEvent += this.event.data.num; 30 | } 31 | 32 | it('History should contain a history item on command', function() { 33 | mapper.mapCommand(Command, 'command.notify'); 34 | eventManager.notify('command.notify', 1); 35 | assert.equal(historyManager.history.length, 1) 36 | assert.equal(historyManager.history[0].constructor.name, 'Command') 37 | mapper.unmapCommand(Command, 'command.notify'); 38 | historyManager.clear() 39 | }); 40 | 41 | it('History should pop off a command', function() { 42 | mapper.mapCommand(Command, 'command.notify'); 43 | eventManager.notify('command.notify', 1); 44 | assert.equal(historyManager.history.length, 1) 45 | historyManager.pop(); 46 | assert.equal(historyManager.history.length, 0) 47 | mapper.unmapCommand(Command, 'command.notify'); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/unit/interface.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const vuecommander = require('../../dist/vuecommander.common.js'); 4 | 5 | var assert = require('assert'); 6 | describe('Interface', function() { 7 | describe('implemented', function() { 8 | var IHuman = new vuecommander.Interface("run"); 9 | var human; 10 | function Human() { 11 | IHuman.implementedBy(this); 12 | this.name = "Jeffery" 13 | } 14 | 15 | it('Should throw an error when not implemented', function() { 16 | try{ 17 | human = new Human(); 18 | } catch(err) { 19 | assert.equal(err.message, 'Class Human does not implement method "run"'); 20 | } 21 | }); 22 | 23 | it('Should not throw an error when implemented properly', function() { 24 | Human.prototype.run = function() {} 25 | human = new Human(); 26 | assert.equal(human.name, "Jeffery"); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/unit/mapper.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const vuecommander = require('../../dist/vuecommander.common.js'); 4 | 5 | var assert = require('assert'); 6 | describe('Mapper', function() { 7 | describe('mapCommand', function() { 8 | var result = 0; 9 | var resultFromEvent = 0; 10 | var eventManager = new vuecommander.EventManager(); 11 | var mapper = new vuecommander.Mapper(eventManager, null); 12 | function Command(e) { 13 | this.event = e; 14 | } 15 | 16 | Command.prototype.execute = function() { 17 | result += 1; 18 | if(!this.event.data) return 19 | resultFromEvent += this.event.data.num; 20 | } 21 | 22 | it('Mapper should map command', function() { 23 | mapper.mapCommand(Command, 'command.events'); 24 | assert.strictEqual(mapper.maps['command.events'], Command); 25 | mapper.unmapCommand(Command, 'command.events'); 26 | }); 27 | 28 | it('Mapper should execute command on event notify', function() { 29 | mapper.mapCommand(Command, 'command.notify'); 30 | eventManager.notify('command.notify'); 31 | assert.equal(result, 1); 32 | mapper.unmapCommand(Command, 'command.notify'); 33 | }); 34 | 35 | it('Mapper should receive event from command', function() { 36 | mapper.mapCommand(Command, 'command.events'); 37 | eventManager.notify('command.events', { num: 5 }); 38 | assert.equal(resultFromEvent, 5); 39 | }); 40 | }); 41 | }); 42 | --------------------------------------------------------------------------------