├── .gitignore ├── LICENSE ├── README.md ├── build ├── favicon.ico ├── index.html └── static │ └── js │ ├── main.4c684789.js │ └── main.4c684789.js.map ├── config ├── babel.dev.js ├── babel.prod.js ├── env.js ├── eslint.js ├── flow │ ├── css.js.flow │ └── file.js.flow ├── paths.js ├── polyfills.js ├── webpack.config.dev.js └── webpack.config.prod.js ├── favicon.ico ├── index.html ├── package.json ├── scripts ├── build.js ├── start.js └── utils │ ├── WatchMissingNodeModulesPlugin.js │ ├── chrome.applescript │ └── prompt.js └── src ├── Todo.js ├── TodoView.js ├── TodoView.material.js ├── TodoViewModel.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-mobx-todo-editor 2 | An example of React & MobX MVVM approach. 3 | 4 | ![Example Image](https://cdn-images-1.medium.com/max/800/1*gDZgHB7KYr44WG442p5BBw.png "Example Image") 5 | 6 | * [Read the Medium Article](https://medium.com/@MattiaManzati/building-a-react-mobx-application-with-mvvm-ec0b3e3c8786#.ta4m76g20) 7 | * [Live Example](https://mattiamanzati.github.io/react-mobx-todo-editor/build) 8 | -------------------------------------------------------------------------------- /build/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattiamanzati/react-mobx-todo-editor/819eff780750a472537d03263116075c0e55cc74/build/favicon.ico -------------------------------------------------------------------------------- /build/index.html: -------------------------------------------------------------------------------- 1 | React App
-------------------------------------------------------------------------------- /config/babel.dev.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Don't try to find .babelrc because we want to force this configuration. 3 | babelrc: false, 4 | // This is a feature of `babel-loader` for webpack (not Babel itself). 5 | // It enables caching results in OS temporary directory for faster rebuilds. 6 | cacheDirectory: true, 7 | presets: [ 8 | // Latest stable ECMAScript features 9 | require.resolve('babel-preset-latest'), 10 | // JSX, Flow 11 | require.resolve('babel-preset-react') 12 | ], 13 | plugins: [ 14 | // handles @decorator 15 | require.resolve('babel-plugin-transform-decorators-legacy'), 16 | // class { handleClick = () => { } } 17 | require.resolve('babel-plugin-transform-class-properties'), 18 | // { ...todo, completed: true } 19 | require.resolve('babel-plugin-transform-object-rest-spread'), 20 | // function* () { yield 42; yield 43; } 21 | [require.resolve('babel-plugin-transform-regenerator'), { 22 | // Async functions are converted to generators by babel-preset-latest 23 | async: false 24 | }], 25 | // Polyfills the runtime needed for async/await and generators 26 | [require.resolve('babel-plugin-transform-runtime'), { 27 | helpers: false, 28 | polyfill: false, 29 | regenerator: true 30 | }] 31 | ] 32 | }; 33 | -------------------------------------------------------------------------------- /config/babel.prod.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Don't try to find .babelrc because we want to force this configuration. 3 | babelrc: false, 4 | presets: [ 5 | // Latest stable ECMAScript features 6 | require.resolve('babel-preset-latest'), 7 | // JSX, Flow 8 | require.resolve('babel-preset-react') 9 | ], 10 | plugins: [ 11 | // handles @decorator 12 | require.resolve('babel-plugin-transform-decorators-legacy'), 13 | // class { handleClick = () => { } } 14 | require.resolve('babel-plugin-transform-class-properties'), 15 | // { ...todo, completed: true } 16 | require.resolve('babel-plugin-transform-object-rest-spread'), 17 | // function* () { yield 42; yield 43; } 18 | [require.resolve('babel-plugin-transform-regenerator'), { 19 | // Async functions are converted to generators by babel-preset-latest 20 | async: false 21 | }], 22 | // Polyfills the runtime needed for async/await and generators 23 | [require.resolve('babel-plugin-transform-runtime'), { 24 | helpers: false, 25 | polyfill: false, 26 | regenerator: true 27 | }], 28 | // Optimization: hoist JSX that never changes out of render() 29 | require.resolve('babel-plugin-transform-react-constant-elements') 30 | ], 31 | }; 32 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 2 | // injected into the application via DefinePlugin in Webpack configuration. 3 | 4 | var REACT_APP = /^REACT_APP_/i; 5 | var NODE_ENV = JSON.stringify(process.env.NODE_ENV || 'development'); 6 | 7 | module.exports = Object 8 | .keys(process.env) 9 | .filter(key => REACT_APP.test(key)) 10 | .reduce((env, key) => { 11 | env['process.env.' + key] = JSON.stringify(process.env[key]); 12 | return env; 13 | }, { 14 | 'process.env.NODE_ENV': NODE_ENV 15 | }); 16 | -------------------------------------------------------------------------------- /config/eslint.js: -------------------------------------------------------------------------------- 1 | // Inspired by https://github.com/airbnb/javascript but less opinionated. 2 | 3 | // We use eslint-loader so even warnings are very visibile. 4 | // This is why we only use "WARNING" level for potential errors, 5 | // and we don't use "ERROR" level at all. 6 | 7 | // In the future, we might create a separate list of rules for production. 8 | // It would probably be more strict. 9 | 10 | module.exports = { 11 | root: true, 12 | 13 | parser: 'babel-eslint', 14 | 15 | // import plugin is termporarily disabled, scroll below to see why 16 | plugins: [/*'import', */'flowtype', 'jsx-a11y', 'react'], 17 | 18 | env: { 19 | browser: true, 20 | commonjs: true, 21 | es6: true, 22 | node: true 23 | }, 24 | 25 | parserOptions: { 26 | ecmaVersion: 6, 27 | sourceType: 'module', 28 | ecmaFeatures: { 29 | jsx: true, 30 | generators: true, 31 | experimentalObjectRestSpread: true 32 | } 33 | }, 34 | 35 | settings: { 36 | 'import/ignore': [ 37 | 'node_modules', 38 | '\\.(json|css|jpg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm)$', 39 | ], 40 | 'import/extensions': ['.js'], 41 | 'import/resolver': { 42 | node: { 43 | extensions: ['.js', '.json'] 44 | } 45 | } 46 | }, 47 | 48 | rules: { 49 | // http://eslint.org/docs/rules/ 50 | 'array-callback-return': 'warn', 51 | 'default-case': ['warn', { commentPattern: '^no default$' }], 52 | 'dot-location': ['warn', 'property'], 53 | eqeqeq: ['warn', 'allow-null'], 54 | 'guard-for-in': 'warn', 55 | 'new-parens': 'warn', 56 | 'no-array-constructor': 'warn', 57 | 'no-caller': 'warn', 58 | 'no-cond-assign': ['warn', 'always'], 59 | 'no-const-assign': 'warn', 60 | 'no-control-regex': 'warn', 61 | 'no-delete-var': 'warn', 62 | 'no-dupe-args': 'warn', 63 | 'no-dupe-class-members': 'warn', 64 | 'no-dupe-keys': 'warn', 65 | 'no-duplicate-case': 'warn', 66 | 'no-empty-character-class': 'warn', 67 | 'no-empty-pattern': 'warn', 68 | 'no-eval': 'warn', 69 | 'no-ex-assign': 'warn', 70 | 'no-extend-native': 'warn', 71 | 'no-extra-bind': 'warn', 72 | 'no-extra-label': 'warn', 73 | 'no-fallthrough': 'warn', 74 | 'no-func-assign': 'warn', 75 | 'no-implied-eval': 'warn', 76 | 'no-invalid-regexp': 'warn', 77 | 'no-iterator': 'warn', 78 | 'no-label-var': 'warn', 79 | 'no-labels': ['warn', { allowLoop: false, allowSwitch: false }], 80 | 'no-lone-blocks': 'warn', 81 | 'no-loop-func': 'warn', 82 | 'no-mixed-operators': ['warn', { 83 | groups: [ 84 | ['&', '|', '^', '~', '<<', '>>', '>>>'], 85 | ['==', '!=', '===', '!==', '>', '>=', '<', '<='], 86 | ['&&', '||'], 87 | ['in', 'instanceof'] 88 | ], 89 | allowSamePrecedence: false 90 | }], 91 | 'no-multi-str': 'warn', 92 | 'no-native-reassign': 'warn', 93 | 'no-negated-in-lhs': 'warn', 94 | 'no-new-func': 'warn', 95 | 'no-new-object': 'warn', 96 | 'no-new-symbol': 'warn', 97 | 'no-new-wrappers': 'warn', 98 | 'no-obj-calls': 'warn', 99 | 'no-octal': 'warn', 100 | 'no-octal-escape': 'warn', 101 | 'no-redeclare': 'warn', 102 | 'no-regex-spaces': 'warn', 103 | 'no-restricted-syntax': [ 104 | 'warn', 105 | 'LabeledStatement', 106 | 'WithStatement', 107 | ], 108 | 'no-return-assign': 'warn', 109 | 'no-script-url': 'warn', 110 | 'no-self-assign': 'warn', 111 | 'no-self-compare': 'warn', 112 | 'no-sequences': 'warn', 113 | 'no-shadow-restricted-names': 'warn', 114 | 'no-sparse-arrays': 'warn', 115 | 'no-this-before-super': 'warn', 116 | 'no-throw-literal': 'warn', 117 | 'no-undef': 'warn', 118 | 'no-unexpected-multiline': 'warn', 119 | 'no-unreachable': 'warn', 120 | 'no-unused-expressions': 'warn', 121 | 'no-unused-labels': 'warn', 122 | 'no-unused-vars': ['warn', { vars: 'local', args: 'none' }], 123 | 'no-use-before-define': ['warn', 'nofunc'], 124 | 'no-useless-computed-key': 'warn', 125 | 'no-useless-concat': 'warn', 126 | 'no-useless-constructor': 'warn', 127 | 'no-useless-escape': 'warn', 128 | 'no-useless-rename': ['warn', { 129 | ignoreDestructuring: false, 130 | ignoreImport: false, 131 | ignoreExport: false, 132 | }], 133 | 'no-with': 'warn', 134 | 'no-whitespace-before-property': 'warn', 135 | 'operator-assignment': ['warn', 'always'], 136 | radix: 'warn', 137 | 'require-yield': 'warn', 138 | 'rest-spread-spacing': ['warn', 'never'], 139 | strict: ['warn', 'never'], 140 | 'unicode-bom': ['warn', 'never'], 141 | 'use-isnan': 'warn', 142 | 'valid-typeof': 'warn', 143 | 144 | // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/ 145 | 146 | // TODO: import rules are temporarily disabled because they don't play well 147 | // with how eslint-loader only checks the file you change. So if module A 148 | // imports module B, and B is missing a default export, the linter will 149 | // record this as an issue in module A. Now if you fix module B, the linter 150 | // will not be aware that it needs to re-lint A as well, so the error 151 | // will stay until the next restart, which is really confusing. 152 | 153 | // This is probably fixable with a patch to eslint-loader. 154 | // When file A is saved, we want to invalidate all files that import it 155 | // *and* that currently have lint errors. This should fix the problem. 156 | 157 | // 'import/default': 'warn', 158 | // 'import/export': 'warn', 159 | // 'import/named': 'warn', 160 | // 'import/namespace': 'warn', 161 | // 'import/no-amd': 'warn', 162 | // 'import/no-duplicates': 'warn', 163 | // 'import/no-extraneous-dependencies': 'warn', 164 | // 'import/no-named-as-default': 'warn', 165 | // 'import/no-named-as-default-member': 'warn', 166 | // 'import/no-unresolved': ['warn', { commonjs: true }], 167 | 168 | // https://github.com/yannickcr/eslint-plugin-react/tree/master/docs/rules 169 | 'react/jsx-equals-spacing': ['warn', 'never'], 170 | 'react/jsx-no-duplicate-props': ['warn', { ignoreCase: true }], 171 | 'react/jsx-no-undef': 'warn', 172 | 'react/jsx-pascal-case': ['warn', { 173 | allowAllCaps: true, 174 | ignore: [], 175 | }], 176 | 'react/jsx-uses-react': 'warn', 177 | 'react/jsx-uses-vars': 'warn', 178 | 'react/no-deprecated': 'warn', 179 | 'react/no-direct-mutation-state': 'warn', 180 | 'react/no-is-mounted': 'warn', 181 | 'react/react-in-jsx-scope': 'warn', 182 | 'react/require-render-return': 'warn', 183 | 184 | // https://github.com/evcohen/eslint-plugin-jsx-a11y/tree/master/docs/rules 185 | 'jsx-a11y/aria-role': 'warn', 186 | 'jsx-a11y/img-has-alt': 'warn', 187 | 'jsx-a11y/img-redundant-alt': 'warn', 188 | 'jsx-a11y/no-access-key': 'warn', 189 | 190 | // https://github.com/gajus/eslint-plugin-flowtype 191 | 'flowtype/define-flow-type': 'warn', 192 | 'flowtype/require-valid-file-annotation': 'warn', 193 | 'flowtype/use-flow-type': 'warn' 194 | } 195 | }; 196 | -------------------------------------------------------------------------------- /config/flow/css.js.flow: -------------------------------------------------------------------------------- 1 | // @flow 2 | -------------------------------------------------------------------------------- /config/flow/file.js.flow: -------------------------------------------------------------------------------- 1 | // @flow 2 | declare export default string; 3 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | // TODO: we can split this file into several files (pre-eject, post-eject, test) 2 | // and use those instead. This way we don't need to branch here. 3 | 4 | var path = require('path'); 5 | 6 | // True after ejecting, false when used as a dependency 7 | var isEjected = ( 8 | path.resolve(path.join(__dirname, '..')) === 9 | path.resolve(process.cwd()) 10 | ); 11 | 12 | // Are we developing create-react-app locally? 13 | var isInCreateReactAppSource = ( 14 | process.argv.some(arg => arg.indexOf('--debug-template') > -1) 15 | ); 16 | 17 | function resolveOwn(relativePath) { 18 | return path.resolve(__dirname, relativePath); 19 | } 20 | 21 | function resolveApp(relativePath) { 22 | return path.resolve(relativePath); 23 | } 24 | 25 | if (isInCreateReactAppSource) { 26 | // create-react-app development: we're in ./config/ 27 | module.exports = { 28 | appBuild: resolveOwn('../build'), 29 | appHtml: resolveOwn('../template/index.html'), 30 | appFavicon: resolveOwn('../template/favicon.ico'), 31 | appPackageJson: resolveOwn('../package.json'), 32 | appSrc: resolveOwn('../template/src'), 33 | appNodeModules: resolveOwn('../node_modules'), 34 | ownNodeModules: resolveOwn('../node_modules') 35 | }; 36 | } else if (!isEjected) { 37 | // before eject: we're in ./node_modules/react-scripts/config/ 38 | module.exports = { 39 | appBuild: resolveApp('build'), 40 | appHtml: resolveApp('index.html'), 41 | appFavicon: resolveApp('favicon.ico'), 42 | appPackageJson: resolveApp('package.json'), 43 | appSrc: resolveApp('src'), 44 | appNodeModules: resolveApp('node_modules'), 45 | // this is empty with npm3 but node resolution searches higher anyway: 46 | ownNodeModules: resolveOwn('../node_modules') 47 | }; 48 | } else { 49 | // after eject: we're in ./config/ 50 | module.exports = { 51 | appBuild: resolveApp('build'), 52 | appHtml: resolveApp('index.html'), 53 | appFavicon: resolveApp('favicon.ico'), 54 | appPackageJson: resolveApp('package.json'), 55 | appSrc: resolveApp('src'), 56 | appNodeModules: resolveApp('node_modules'), 57 | ownNodeModules: resolveApp('node_modules') 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /config/polyfills.js: -------------------------------------------------------------------------------- 1 | if (typeof Promise === 'undefined') { 2 | // Rejection tracking prevents a common issue where React gets into an 3 | // inconsistent state due to an error, but it gets swallowed by a Promise, 4 | // and the user has no idea what causes React's erratic future behavior. 5 | require('promise/lib/rejection-tracking').enable(); 6 | window.Promise = require('promise/lib/es6-extensions.js'); 7 | } 8 | 9 | // fetch() polyfill for making API calls. 10 | require('whatwg-fetch'); 11 | 12 | // Object.assign() is commonly used with React. 13 | // It will use the native implementation if it's present and isn't buggy. 14 | Object.assign = require('object-assign'); 15 | -------------------------------------------------------------------------------- /config/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var autoprefixer = require('autoprefixer'); 3 | var webpack = require('webpack'); 4 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | var CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin'); 6 | var WatchMissingNodeModulesPlugin = require('../scripts/utils/WatchMissingNodeModulesPlugin'); 7 | var paths = require('./paths'); 8 | var env = require('./env'); 9 | 10 | // This is the development configuration. 11 | // It is focused on developer experience and fast rebuilds. 12 | // The production configuration is different and lives in a separate file. 13 | module.exports = { 14 | // This makes the bundle appear split into separate modules in the devtools. 15 | // We don't use source maps here because they can be confusing: 16 | // https://github.com/facebookincubator/create-react-app/issues/343#issuecomment-237241875 17 | // You may want 'cheap-module-source-map' instead if you prefer source maps. 18 | devtool: 'eval', 19 | // These are the "entry points" to our application. 20 | // This means they will be the "root" imports that are included in JS bundle. 21 | // The first two entry points enable "hot" CSS and auto-refreshes for JS. 22 | entry: [ 23 | // Include WebpackDevServer client. It connects to WebpackDevServer via 24 | // sockets and waits for recompile notifications. When WebpackDevServer 25 | // recompiles, it sends a message to the client by socket. If only CSS 26 | // was changed, the app reload just the CSS. Otherwise, it will refresh. 27 | // The "?/" bit at the end tells the client to look for the socket at 28 | // the root path, i.e. /sockjs-node/. Otherwise visiting a client-side 29 | // route like /todos/42 would make it wrongly request /todos/42/sockjs-node. 30 | // The socket server is a part of WebpackDevServer which we are using. 31 | // The /sockjs-node/ path I'm referring to is hardcoded in WebpackDevServer. 32 | require.resolve('webpack-dev-server/client') + '?/', 33 | // Include Webpack hot module replacement runtime. Webpack is pretty 34 | // low-level so we need to put all the pieces together. The runtime listens 35 | // to the events received by the client above, and applies updates (such as 36 | // new CSS) to the running application. 37 | require.resolve('webpack/hot/dev-server'), 38 | // We ship a few polyfills by default. 39 | require.resolve('./polyfills'), 40 | // Finally, this is your app's code: 41 | path.join(paths.appSrc, 'index') 42 | // We include the app code last so that if there is a runtime error during 43 | // initialization, it doesn't blow up the WebpackDevServer client, and 44 | // changing JS code would still trigger a refresh. 45 | ], 46 | output: { 47 | // Next line is not used in dev but WebpackDevServer crashes without it: 48 | path: paths.appBuild, 49 | // Add /* filename */ comments to generated require()s in the output. 50 | pathinfo: true, 51 | // This does not produce a real file. It's just the virtual path that is 52 | // served by WebpackDevServer in development. This is the JS bundle 53 | // containing code from all our entry points, and the Webpack runtime. 54 | filename: 'static/js/bundle.js', 55 | // In development, we always serve from the root. This makes config easier. 56 | publicPath: '/' 57 | }, 58 | resolve: { 59 | // These are the reasonable defaults supported by the Node ecosystem. 60 | extensions: ['.js', '.json', ''], 61 | alias: { 62 | // This `alias` section can be safely removed after ejection. 63 | // We do this because `babel-runtime` may be inside `react-scripts`, 64 | // so when `babel-plugin-transform-runtime` imports it, it will not be 65 | // available to the app directly. This is a temporary solution that lets 66 | // us ship support for generators. However it is far from ideal, and 67 | // if we don't have a good solution, we should just make `babel-runtime` 68 | // a dependency in generated projects. 69 | // See https://github.com/facebookincubator/create-react-app/issues/255 70 | 'babel-runtime/regenerator': require.resolve('babel-runtime/regenerator'), 71 | 'react-native': 'react-native-web' 72 | } 73 | }, 74 | // Resolve loaders (webpack plugins for CSS, images, transpilation) from the 75 | // directory of `react-scripts` itself rather than the project directory. 76 | // You can remove this after ejecting. 77 | resolveLoader: { 78 | root: paths.ownNodeModules, 79 | moduleTemplates: ['*-loader'] 80 | }, 81 | module: { 82 | // First, run the linter. 83 | // It's important to do this before Babel processes the JS. 84 | preLoaders: [ 85 | { 86 | test: /\.js$/, 87 | loader: 'eslint', 88 | include: paths.appSrc, 89 | } 90 | ], 91 | loaders: [ 92 | // Process JS with Babel. 93 | { 94 | test: /\.js$/, 95 | include: paths.appSrc, 96 | loader: 'babel', 97 | query: require('./babel.dev') 98 | }, 99 | // "postcss" loader applies autoprefixer to our CSS. 100 | // "css" loader resolves paths in CSS and adds assets as dependencies. 101 | // "style" loader turns CSS into JS modules that inject