├── .gitignore ├── LICENSE ├── README.md ├── externs ├── browserify.js ├── react.js └── todomvc.js ├── gulpfile.js ├── package.json ├── src ├── app.jsx.ts ├── components │ ├── Footer.jsx.ts │ ├── Header.jsx.ts │ ├── MainSection.jsx.ts │ ├── TodoApp.jsx.ts │ ├── TodoItem.jsx.ts │ └── TodoTextInput.jsx.ts ├── flux │ ├── actions │ │ ├── TodoActionID.ts │ │ └── TodoActions.ts │ ├── dispatcher │ │ └── AppDispatcher.ts │ └── stores │ │ └── TodoStore.ts └── react │ ├── ReactComponent.ts │ └── ReactJSX.ts ├── tsconfig.json ├── typings ├── object-assign │ └── object-assign.d.ts ├── react-jsx │ └── react-jsx.d.ts ├── react-tools │ └── react-tools.d.ts ├── todomvc │ └── todomvc.d.ts ├── tsd.d.ts └── tsd.json └── web ├── css └── app.css ├── debug.html ├── index.html ├── js ├── bundle.js └── bundle.min.js ├── todomvc-app-css └── index.css └── todomvc-common ├── base.css └── base.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated folders 2 | build/ 3 | node_modules/ 4 | 5 | # typings 6 | /typings/* 7 | !/typings/tsd.d.ts 8 | !/typings/todomvc/todomvc.d.ts 9 | !/typings/react-jsx/react-jsx.d.ts 10 | !/typings/react-tools/react-tools.d.ts 11 | !/typings/store/store.d.ts 12 | 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Bernd Paradies 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 | # TypeScript & Flux/React TodoMVC Example 2 | 3 | > TypeScript is a language for application-scale JavaScript development. TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. Any browser. Any host. Any OS. Open Source. 4 | 5 | _[TypeScript - typescriptlang.org](https://typescriptlang.org/)_ 6 | 7 |
8 | 9 | > Flux is the application architecture that Facebook uses for building client-side web applications. It complements React's composable view components by utilizing a unidirectional data flow. 10 | 11 | _[Flux - facebook.github.io/flux](https://facebook.github.io/flux/)_ 12 | 13 |
14 | 15 | > React is a Javascript library for building user interfaces. React uses a virtual DOM diff implementation for ultra-high performance. 16 | 17 | _[React - facebook.github.io/react](https://facebook.github.io/react/)_ 18 | 19 | 20 | ## Learning TypeScript 21 | 22 | The [TypeScript website](https://typescriptlang.org/) is a great resource for getting started. 23 | 24 | Here are some links you may find helpful: 25 | 26 | * [Tutorial](https://www.typescriptlang.org/Tutorial/) 27 | * [Code Playground](https://www.typescriptlang.org/Playground/) 28 | * [Documentation](https://typescript.codeplex.com/documentation/) 29 | * [Applications built with TypeScript](https://www.typescriptlang.org/Samples/) 30 | * [Blog](https://blogs.msdn.com/b/typescript/) 31 | * [Source Code](https://typescript.codeplex.com/sourcecontrol/latest#README.txt/) 32 | 33 | Articles and guides from the community: 34 | 35 | * ["Evolving JavaScript with TypeScript" by Anders Hejlsberg](https://www.youtube.com/watch?v=Ut694dsIa8w/) 36 | * [Thoughts on TypeScript](https://www.nczonline.net/blog/2012/10/04/thoughts-on-typescript/) 37 | * [ScreenCast - Why I Like TypeScript](https://www.leebrimelow.com/why-i-like-typescripts/) 38 | 39 | Get help from other TypeScript users: 40 | 41 | * [TypeScript on StackOverflow](https://stackoverflow.com/questions/tagged/typescript/) 42 | * [Forums](https://typescript.codeplex.com/discussions/) 43 | * [TypeScript on Twitter](https://twitter.com/typescriptlang/) 44 | 45 | _If you have other helpful links to share, or find any of the links above no longer work, please [let us know](https://github.com/tastejs/todomvc/issues/)._ 46 | 47 | 48 | ## Implementation 49 | 50 | This application is a port of the [flux-todomvc](https://github.com/facebook/flux/tree/master/examples/flux-todomvc/) example, which is 51 | part of Facebook's [Flux](https://github.com/facebook/flux/) project. 52 | 53 | The original example uses [JSX](https://facebook.github.io/react/docs/jsx-in-depth.html/), which is a a JavaScript syntax extension that looks similar to XML. 54 | Transforming JSX to Javascript is pretty easy as James Brantly explained in his recent React.js Conf 2015 talk 55 | [Static typing with Flow and TypeScript](https://conf.reactjs.com/schedule.html#static-typing-with-flow-and-typescript/). 56 | 57 | James Brantly's [reactconf](https://github.com/jbrantly/reactconf/) uses [webpack](https://webpack.github.io/) and [ts-jsx-loader](https://github.com/jbrantly/ts-jsx-loader/). 58 | This project on the other hand uses [grunt](https://gruntjs.com/) and [grunt-text-replace](https://github.com/yoniholmes/grunt-text-replace/) with 59 | [react-tools](https://www.npmjs.com/package/react-tools/) and [grunt-browserify](https://github.com/jmreidy/grunt-browserify/) instead. 60 | 61 | The main reason for [choosing browserify over webpack](https://blog.namangoel.com/browserify-vs-webpack-js-drama/) is that 62 | browserify injects node.js polyfills like [EventEmitter](https://nodejs.org/api/events.html#events_class_events_eventemitter/), 63 | which are used by the original [flux-todomvc](https://github.com/facebook/flux/tree/master/examples/flux-todomvc/) example. 64 | 65 | During the build phase all `*.tsx` and `*.ts` are first being copied to a temporary `./build` folder and then compiled to Javascript using the Typescript compiler. 66 | A second pass then intelligently replaces `React.jsx(...)` with transformed JSX code. For more details see `./grunt/replace.js`. 67 | The build mechanics are all implemented in `./grunt/aliases.js`. The output folder is `./web`. 68 | 69 | This project also supports minification using Google's Closure Compiler. 70 | 71 | 72 | ## Running 73 | 74 | You must have [node.js](https://nodejs.org/) installed on your computer, which includes [npm](https://www.npmjs.org/). From the root project directory run these commands from the command line: 75 | 76 | ``` 77 | npm install 78 | ``` 79 | 80 | That will install all dependencies and build `web/js/bundle.js`, which is used by `web/index.html`. 81 | Once you've built the project you can open `web/index.html` in your browser, or simply run `npm start`. 82 | 83 | 84 | ## Demo 85 | 86 | Please try this live demo:
87 | https://bparadie.github.io/fluxts-todomvc 88 | -------------------------------------------------------------------------------- /externs/browserify.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by bparadie on 3/1/15. 3 | */ 4 | 5 | /** 6 | * @param {*=}o 7 | * @param {*=}u 8 | */ 9 | function require(o,u){} 10 | 11 | 12 | /** 13 | * @param {*=}o 14 | * @param {*=}u 15 | */ 16 | function define(o,u){} 17 | -------------------------------------------------------------------------------- /externs/react.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by bparadie on 3/1/15. 3 | */ 4 | 5 | /** 6 | * @type {*} 7 | */ 8 | var __REACT_DEVTOOLS_GLOBAL_HOOK__; 9 | 10 | /** 11 | * @type {*} 12 | */ 13 | var Symbol; 14 | 15 | /** 16 | * @type {*} 17 | */ 18 | var MSApp; 19 | -------------------------------------------------------------------------------- /externs/todomvc.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | /** 4 | * We have two choices: 5 | * - either not use enums 6 | * OR: 7 | * - declaring all enums as externs 8 | * 9 | * @type {*} 10 | */ 11 | var TodoMVCExterns = { 12 | TODO_CREATE:0, 13 | TODO_COMPLETE:0, 14 | TODO_DESTROY:0, 15 | TODO_DESTROY_COMPLETED:0, 16 | TODO_TOGGLE_COMPLETE_ALL:0, 17 | TODO_UNDO_COMPLETE:0, 18 | TODO_UPDATE_TEXT:0, 19 | module:0 20 | }; -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var require = require || null; 2 | 3 | var gulp = require('gulp'); 4 | var del = require('del'); 5 | var ts = require('gulp-typescript'); 6 | var filter = require('gulp-filter'); 7 | var log = require('gulp-print'); 8 | var replace = require('gulp-replace'); 9 | var reactTools = require('react-tools'); 10 | var browserify = require('gulp-browserify'); 11 | var concat = require('gulp-concat'); 12 | var tsd = require('gulp-tsd'); 13 | var closureCompiler = require('gulp-closure-compiler'); 14 | var runSequence = require('run-sequence'); 15 | var tslint = require('gulp-tslint'); 16 | var gutil = require('gulp-util'); 17 | 18 | var config = { 19 | build: 'build', 20 | web: 'web', 21 | js_generated: 'build/js', 22 | ts_typings: './typings/**', 23 | ts_sources: './src/**/*.ts', 24 | ts_jsx: 'build/src/**/*.ts' 25 | }; 26 | 27 | gulp.task('clean', function(callback) { 28 | del(config.build, callback); 29 | }); 30 | 31 | gulp.task('tsd', function (callback) { 32 | return tsd({ 33 | command: 'reinstall', 34 | config: './typings/tsd.json' 35 | }, callback); 36 | }); 37 | 38 | gulp.task('typings', function() { 39 | // You can use multiple globbing patterns as you would with `gulp.src` 40 | return gulp.src(config.ts_typings, {base: '.'}) 41 | .pipe(gulp.dest( config.build )); 42 | }); 43 | 44 | gulp.task('src', function() { 45 | // You can use multiple globbing patterns as you would with `gulp.src` 46 | return gulp.src(config.ts_sources, {base: '.'}) 47 | .pipe(gulp.dest( config.build )); 48 | }); 49 | 50 | gulp.task('jsx', function() { 51 | 52 | function jsx(all, match) 53 | { 54 | var reactCode; 55 | 56 | if( match ) 57 | { 58 | try { 59 | reactCode = reactTools.transform(match, { harmony: false }); 60 | } 61 | catch (ex) { 62 | gutil.error('Problem transforming the following:\n' + match + '\n\n' + ex); 63 | } 64 | } 65 | 66 | // In case you need to debug this: 67 | // gutil.log('jsx =>' + match); 68 | 69 | return '(' + reactCode + ')'; 70 | } 71 | 72 | return gulp.src([config.ts_sources,'!src/react/ReactJSX.ts']) 73 | .pipe(replace(/ReactJSX.*`([^`\\]*(\\.[^`\\]*)*)`([^`\\\)]*(\\.[^`\\\)]*)*)\)/gm, jsx)) 74 | .pipe(replace(/import ReactJSX.*$/gm, '')) 75 | // .pipe(log()) 76 | .pipe(gulp.dest(config.build + '/src/')); 77 | }); 78 | 79 | gulp.task('ts', function() { 80 | // Minify and copy all JavaScript (except vendor scripts) 81 | // with sourcemaps all the way down 82 | 83 | var ts_options = { // Use to override the default options, http://gruntjs.com/configuring-tasks#options 84 | target: 'es5', // 'es3' (default) | 'es5' 85 | sourceMap: false, // true (default) | false 86 | declaration: false, // true | false (default) 87 | removeComments: false, // true (default) | false 88 | noImplicitAny: true, // Warn on expressions and declarations with an implied 'any' type. 89 | module: 'commonjs', 90 | outDir: config.js_generated, 91 | fast: 'never', 92 | typescript: require('typescript') 93 | }; 94 | 95 | return gulp.src( config.ts_jsx) 96 | .pipe(ts(ts_options)) 97 | .pipe(log()) 98 | .pipe(gulp.dest( config.build + '/js/')); 99 | }); 100 | 101 | gulp.task('bundle', function() { 102 | // Minify and copy all JavaScript (except vendor scripts) 103 | // with sourcemaps all the way down 104 | 105 | // 'function ieKeyFix(key)' => 'var ieKeyFix = function(key)' 106 | // node_modules/store/store.js declares a function 'ieKeyFix' in the middle of another function and triggers: 107 | // ERROR - functions can only be declared at top level or immediately within another function in ES5 strict mode 108 | 109 | // 'this.module !== module' => 'window.module !== module' 110 | // node_modules/store/store.js has some questionable code that triggered: 111 | // Uncaught TypeError: Cannot read property 'module' of undefined 112 | 113 | return gulp.src( config.build + '/js/app.jsx.js') 114 | .pipe(browserify({ 115 | insertGlobals : true, 116 | debug: true 117 | })) 118 | .pipe(replace(/function ieKeyFix\(key\)/gm, 'var ieKeyFix = function(key)')) 119 | .pipe(replace(/this.module !== module/gm, 'window.module !== module')) 120 | .pipe(concat('bundle.js')) 121 | .pipe(log()) 122 | .pipe(gulp.dest( config.web + '/js/')); 123 | }); 124 | 125 | gulp.task('minify', function() { 126 | 127 | var options = { 128 | // [REQUIRED] Path to closure compiler 129 | compilerPath: './node_modules/closure-compiler/lib/vendor/compiler.jar', 130 | fileName: 'bundle.min.js', 131 | 132 | // [OPTIONAL] set to true if you want to check if files were modified 133 | // before starting compilation (can save some time in large sourcebases) 134 | checkModified: true, 135 | 136 | // [OPTIONAL] Set Closure Compiler Directives here 137 | compilerFlags: { 138 | // closure_entry_point: 'App.main', 139 | compilation_level: 'ADVANCED_OPTIMIZATIONS', 140 | define: [ 141 | // 'goog.DEBUG=false' 142 | ], 143 | externs: [ 144 | 'externs/browserify.js', 145 | 'externs/react.js', 146 | 'externs/todomvc.js' 147 | ], 148 | jscomp_off: [ 149 | 'checkTypes', 150 | 'fileoverviewTags' 151 | ], 152 | extra_annotation_name: 'jsx', 153 | // summary_detail_level: 3, 154 | language_in: 'ECMASCRIPT5_STRICT', 155 | // only_closure_dependencies: true, 156 | // .call is super important, otherwise Closure Library will not work in strict mode. 157 | output_wrapper: '(function(){%output%}).call(window);', 158 | warning_level: 'QUIET' 159 | }, 160 | // [OPTIONAL] Set exec method options 161 | execOpts: { 162 | /** 163 | * Set maxBuffer if you got message 'Error: maxBuffer exceeded.' 164 | * Node default: 200*1024 165 | */ 166 | maxBuffer: 999999 * 1024 167 | }, 168 | // [OPTIONAL] Java VM optimization options 169 | // see https://code.google.com/p/closure-compiler/wiki/FAQ#What_are_the_recommended_Java_VM_command-line_options? 170 | // Setting one of these to 'true' is strongly recommended, 171 | // and can reduce compile times by 50-80% depending on compilation size 172 | // and hardware. 173 | // On server-class hardware, such as with Github's Travis hook, 174 | // TieredCompilation should be used; on standard developer hardware, 175 | // d32 may be better. Set as appropriate for your environment. 176 | // Default for both is 'false'; do not set both to 'true'. 177 | d32: false, // will use 'java -client -d32 -jar compiler.jar' 178 | TieredCompilation: true // will use 'java -server -XX:+TieredCompilation -jar compiler.jar' 179 | }; 180 | 181 | return gulp.src(config.web + '/js/bundle.js') 182 | .pipe(closureCompiler(options)) 183 | .pipe(log()) 184 | .pipe(gulp.dest(config.web + '/js/')); 185 | }); 186 | 187 | gulp.task('tslint', function(){ 188 | 189 | var options = { 190 | configuration: { 191 | 'rules': { 192 | 'ban': [true, 193 | ['_', 'extend'], 194 | ['_', 'isNull'], 195 | ['_', 'isDefined'] 196 | ], 197 | 'class-name': true, 198 | 'comment-format': [false, 199 | 'check-space', 200 | 'check-lowercase' 201 | ], 202 | 'curly': true, 203 | 'eofline': true, 204 | 'forin': true, 205 | 'indent': [true, 4], 206 | 'interface-name': false, 207 | 'jsdoc-format': false, 208 | 'label-position': true, 209 | 'label-undefined': true, 210 | 'max-line-length': [true, 140], 211 | 'member-ordering': [false, 212 | 'public-before-private', 213 | 'static-before-instance', 214 | 'variables-before-functions' 215 | ], 216 | 'no-arg': true, 217 | 'no-bitwise': false, 218 | 'no-console': [true, 219 | 'debug', 220 | 'info-false', 221 | 'time', 222 | 'timeEnd', 223 | 'trace' 224 | ], 225 | 'no-construct': true, 226 | 'no-constructor-vars': true, 227 | 'no-debugger': true, 228 | 'no-duplicate-key': true, 229 | 'no-duplicate-variable': true, 230 | 'no-empty': false, 231 | 'no-eval': true, 232 | 'no-string-literal': true, 233 | 'no-switch-case-fall-through': true, 234 | 'no-trailing-comma': true, 235 | 'no-trailing-whitespace': false, 236 | 'no-unused-expression': false, 237 | 'no-unused-variable': true, 238 | 'no-unreachable': true, 239 | 'no-use-before-declare': true, 240 | 'no-var-requires': false, 241 | 'one-line': [false, 242 | 'check-open-brace', 243 | 'check-catch', 244 | 'check-else', 245 | 'check-whitespace' 246 | ], 247 | 'quotemark': [false, 'double'], 248 | 'radix': true, 249 | 'semicolon': true, 250 | 'triple-equals': [false, 'allow-null-check'], 251 | 'typedef': [true, 252 | 'callSignature', 253 | 'indexSignature', 254 | 'parameter', 255 | 'propertySignature', 256 | 'variableDeclarator' 257 | ], 258 | 'typedef-whitespace': [true, 259 | ['callSignature', 'noSpace'], 260 | ['catchClause', 'noSpace'], 261 | ['indexSignature', 'space'] 262 | ], 263 | 'use-strict': [false, 264 | 'check-module', 265 | 'check-function' 266 | ], 267 | 'variable-name': false, 268 | 'whitespace': [false, 269 | 'check-branch', 270 | 'check-decl', 271 | 'check-operator', 272 | 'check-separator', 273 | 'check-type' 274 | ] 275 | } 276 | } 277 | }; 278 | 279 | return gulp.src(config.build + '/src/**/*.ts') 280 | .pipe(tslint(options)) 281 | .pipe(tslint.report('prose')); 282 | }); 283 | 284 | /** 285 | The 'debug' task does not include: 286 | jsx, tslint 287 | */ 288 | gulp.task('debug', function(callback) { 289 | return runSequence( 290 | 'clean', 291 | ['typings','src'], 292 | 'ts', 293 | 'bundle', 294 | callback); 295 | }); 296 | 297 | 298 | gulp.task('release', function(callback) { 299 | return runSequence( 300 | 'clean', 301 | 'tsd', 302 | ['typings','jsx'], 303 | 'tslint', 304 | 'ts', 305 | 'bundle', 306 | 'minify', 307 | callback); 308 | }); 309 | 310 | gulp.task('lint', function(callback) { 311 | return runSequence( 312 | 'clean', 313 | 'tsd', 314 | ['typings','jsx'], 315 | 'tslint', 316 | callback); 317 | }); 318 | 319 | gulp.task('help', function(callback) { 320 | gutil.log(''); 321 | gutil.log('USAGE:'); 322 | gutil.log('Open web/index.html in browser.'); 323 | gutil.log(''); 324 | gutil.log('TASKS:'); 325 | gutil.log('debug - builds web/js/bundle.js with runtime JSX renderer.'); 326 | gutil.log('release - builds web/js/bundle.min.js with expanded JSX.'); 327 | gutil.log(''); 328 | }); 329 | 330 | gulp.task('build', ['release']); 331 | 332 | gulp.task('default', ['release']); 333 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fluxts-todomvc", 3 | "version": "1.0.0", 4 | "description": "Flux TodoMVC Example using Typescript", 5 | "author": "Bernd Paradies", 6 | "license": "MIT", 7 | "repository": "https://github.com/bparadie/fluxts-todomvc", 8 | "scripts": { 9 | "postinstall": "./node_modules/.bin/gulp release", 10 | "start": "open ./web/index.html", 11 | "lint": "./node_modules/.bin/gulp lint", 12 | "help": "./node_modules/.bin/gulp help" 13 | }, 14 | "devDependencies": { 15 | "closure-compiler": "0.2.6", 16 | "del": "1.2.0", 17 | "eventemitter3": "1.1.0", 18 | "flux": "2.0.3", 19 | "gulp": "3.9.0", 20 | "gulp-browserify": "0.5.1", 21 | "gulp-closure-compiler": "0.2.19", 22 | "gulp-concat": "2.5.2", 23 | "gulp-filter": "2.0.2", 24 | "gulp-print": "1.1.0", 25 | "gulp-replace": "0.5.3", 26 | "gulp-tsd": "0.0.4", 27 | "gulp-tslint": "3.0.1-beta", 28 | "gulp-typescript": "2.7.6", 29 | "gulp-util": "3.0.5", 30 | "object-assign": "3.0.0", 31 | "react-tools": "0.13.3", 32 | "run-sequence": "1.1.0", 33 | "store": "1.3.17", 34 | "typescript": "1.5.0-beta" 35 | }, 36 | "dependencies": { 37 | "react": "0.13.3" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app.jsx.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * Typescript port by Bernd Paradies, May 2015 10 | * @see https://github.com/facebook/flux/blob/master/examples/flux-todomvc/js/app.js 11 | * 12 | */ 13 | 14 | /// 15 | 16 | import React = require('react'); 17 | import ReactJSX = require('./react/ReactJSX'); 18 | import TodoApp = require('./components/TodoApp.jsx'); 19 | 20 | React.render( 21 | ReactJSX(``, this, { 22 | 'TodoApp': TodoApp 23 | }), 24 | document.getElementById('todoapp') 25 | ); 26 | -------------------------------------------------------------------------------- /src/components/Footer.jsx.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * Typescript port by Bernd Paradies, May 2015 10 | * @see https://github.com/facebook/flux/blob/master/examples/flux-todomvc/js/components/Footer.react.js 11 | */ 12 | 13 | /// 14 | 15 | import React = require('react/addons'); 16 | import ReactComponent = require('../react/ReactComponent'); 17 | import ReactJSX = require('../react/ReactJSX'); 18 | import TodoActions = require('../flux/actions/TodoActions'); 19 | 20 | var ReactPropTypes: React.ReactPropTypes = React.PropTypes; 21 | 22 | interface FooterProps { 23 | allTodos: MapStringTo; 24 | } 25 | 26 | interface FooterElement { 27 | id: string; 28 | } 29 | 30 | interface ClearCompletedButton { 31 | id: string; 32 | onClick: () => void; 33 | } 34 | 35 | class Footer extends ReactComponent { 36 | 37 | static propTypes: React.ValidationMap = { 38 | allTodos: ReactPropTypes.object.isRequired 39 | }; 40 | 41 | /** 42 | * Event handler to delete all completed TODOs 43 | */ 44 | private _onClearCompletedClick: () => void = 45 | (): void => { 46 | TodoActions.destroyCompleted(); 47 | }; 48 | 49 | /** 50 | * @return {object} 51 | */ 52 | public render(): React.ReactElement { 53 | 54 | var allTodos: MapStringTo = this.props.allTodos; 55 | var total: number = Object.keys(allTodos).length; 56 | var completed: number = 0; 57 | var key: string; 58 | var itemsLeft: number; 59 | var itemsLeftPhrase: string; 60 | var clearCompletedButton: React.ReactElement; 61 | 62 | if (total === 0) { 63 | return null; 64 | } 65 | 66 | for (key in allTodos) { 67 | if (allTodos[key].complete) { 68 | completed++; 69 | } 70 | } 71 | 72 | itemsLeft = total - completed; 73 | itemsLeftPhrase = itemsLeft === 1 ? ' item ' : ' items '; 74 | itemsLeftPhrase += 'left'; 75 | 76 | // Undefined and thus not rendered if no completed items are left. 77 | if (completed) { 78 | clearCompletedButton = 79 | ReactJSX(` 80 | `, 85 | this, 86 | { 87 | completed: completed 88 | }); 89 | } 90 | 91 | return ReactJSX(` 92 |
93 | 94 | 95 | {itemsLeft} 96 | 97 | {itemsLeftPhrase} 98 | 99 | {clearCompletedButton} 100 |
`, 101 | this, 102 | { 103 | 'itemsLeft': itemsLeft, 104 | 'itemsLeftPhrase': itemsLeftPhrase, 105 | 'clearCompletedButton': clearCompletedButton 106 | } 107 | ); 108 | } 109 | } 110 | 111 | export = Footer; 112 | -------------------------------------------------------------------------------- /src/components/Header.jsx.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * Typescript port by Bernd Paradies, May 2015 10 | * @see https://github.com/facebook/flux/blob/master/examples/flux-todomvc/js/components/Header.react.js 11 | * 12 | */ 13 | 14 | import React = require('react/addons'); 15 | import TodoActions = require('../flux/actions/TodoActions'); 16 | import TodoTextInput = require('./TodoTextInput.jsx'); 17 | import ReactComponent = require('../react/ReactComponent'); 18 | import ReactJSX = require('../react/ReactJSX'); 19 | 20 | interface HeaderProps { 21 | } 22 | 23 | interface HeaderElement { 24 | id: string; 25 | } 26 | 27 | class Header extends ReactComponent { 28 | 29 | /** 30 | * Event handler called within TodoTextInput. 31 | * Defining this here allows TodoTextInput to be used in multiple places 32 | * in different ways. 33 | * @param {string} text 34 | */ 35 | private _onSave: (text: string) => void = 36 | (text: string): void => { 37 | if (text.trim()){ 38 | TodoActions.create(text); 39 | } 40 | }; 41 | 42 | /** 43 | * @return {object} 44 | */ 45 | public render(): React.ReactElement { 46 | return ReactJSX(` 47 | `, 55 | this, 56 | { 57 | 'TodoTextInput': TodoTextInput 58 | }); 59 | } 60 | }; 61 | 62 | export = Header; 63 | -------------------------------------------------------------------------------- /src/components/MainSection.jsx.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * Typescript port by Bernd Paradies, May 2015 10 | * @see https://github.com/facebook/flux/blob/master/examples/flux-todomvc/js/components/MainSection.react.js 11 | * 12 | */ 13 | 14 | import React = require('react/addons'); 15 | import TodoActions = require('../flux/actions/TodoActions'); 16 | import TodoItem = require('./TodoItem.jsx'); 17 | import ReactComponent = require('../react/ReactComponent'); 18 | import ReactJSX = require('../react/ReactJSX'); 19 | 20 | var ReactPropTypes: React.ReactPropTypes = React.PropTypes; 21 | 22 | interface MainSectionElement { 23 | id: string; 24 | } 25 | 26 | class MainSection extends ReactComponent { 27 | 28 | static propTypes: React.ValidationMap = { 29 | allTodos: ReactPropTypes.object.isRequired, 30 | areAllComplete: ReactPropTypes.bool.isRequired 31 | }; 32 | 33 | /** 34 | * Event handler to mark all TODOs as complete 35 | */ 36 | private _onToggleCompleteAll: () => void = 37 | (): void => { 38 | TodoActions.toggleCompleteAll(); 39 | }; 40 | 41 | /** 42 | * @return {object} 43 | */ 44 | public render(): React.ReactElement { 45 | var key: string; 46 | var todos: React.ReactElement[]; 47 | var allTodos: MapStringTo; 48 | 49 | // This section should be hidden by default 50 | // and shown when there are todos. 51 | if (Object.keys(this.props.allTodos).length < 1) { 52 | return null; 53 | } 54 | 55 | allTodos = this.props.allTodos; 56 | todos = []; 57 | 58 | for (key in allTodos) { 59 | if( allTodos.hasOwnProperty(key) ) 60 | { 61 | todos.push( 62 | ReactJSX(``, 63 | this, 64 | { 65 | TodoItem: TodoItem, 66 | allTodos: allTodos, 67 | key: key 68 | }) 69 | ); 70 | } 71 | } 72 | 73 | return ReactJSX(` 74 |
75 | 81 | 82 |
    {todos}
83 |
`, 84 | this, 85 | { 86 | 'todos': todos 87 | }); 88 | } 89 | }; 90 | 91 | export = MainSection; 92 | -------------------------------------------------------------------------------- /src/components/TodoApp.jsx.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * Typescript port by Bernd Paradies, May 2015 10 | * @see https://github.com/facebook/flux/blob/master/examples/flux-todomvc/js/components/TodoApp.react.js 11 | * 12 | */ 13 | 14 | /** 15 | * This component operates as a 'Controller-View'. It listens for changes in 16 | * the TodoStore and passes the new data to its children. 17 | */ 18 | 19 | import Footer = require('./Footer.jsx'); 20 | import Header = require('./Header.jsx'); 21 | import MainSection = require('./MainSection.jsx'); 22 | import React = require('react/addons'); 23 | import TodoStore = require('../flux/stores/TodoStore'); 24 | import ReactComponent = require('../react/ReactComponent'); 25 | import ReactJSX = require('../react/ReactJSX'); 26 | 27 | interface TodoAppProps { 28 | } 29 | 30 | interface TodoAppElement { 31 | className: string; 32 | } 33 | 34 | /** 35 | * Retrieve the current TODO data from the TodoStore 36 | */ 37 | function getTodoState(): TodoState { 38 | return { 39 | allTodos: TodoStore.getAll(), 40 | areAllComplete: TodoStore.areAllComplete() 41 | }; 42 | } 43 | 44 | class TodoApp extends ReactComponent { 45 | 46 | /** 47 | * Event handler for 'change' events coming from the TodoStore 48 | */ 49 | private _onChange = () => { 50 | this.setState(getTodoState()); 51 | }; 52 | 53 | public componentDidMount: () => void = 54 | (): void => { 55 | TodoStore.addChangeListener(this._onChange); 56 | }; 57 | 58 | public componentWillUnmount: () => void = 59 | (): void => { 60 | TodoStore.removeChangeListener(this._onChange); 61 | }; 62 | 63 | public getDerivedInitialState(): TodoState { 64 | return getTodoState(); 65 | } 66 | 67 | /** 68 | * @return {object} 69 | */ 70 | public render(): React.ReactElement { 71 | // this.state = this.state || this.getInitialState(); 72 | 73 | return ReactJSX(` 74 |
75 |
76 | 80 |
81 |
82 | `, 83 | this, 84 | { 85 | 'Header': Header, 86 | 'MainSection': MainSection, 87 | 'Footer': Footer 88 | }); 89 | } 90 | }; 91 | 92 | export = TodoApp; 93 | -------------------------------------------------------------------------------- /src/components/TodoItem.jsx.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * Typescript port by Bernd Paradies, May 2015 10 | * @see https://github.com/facebook/flux/blob/master/examples/flux-todomvc/js/components/TodoItem.react.js 11 | * 12 | */ 13 | 14 | /// 15 | 16 | import React = require('react/addons'); 17 | import TodoActions = require('../flux/actions/TodoActions'); 18 | import TodoTextInput = require('./TodoTextInput.jsx'); 19 | import ReactComponent = require('../react/ReactComponent'); 20 | import ReactJSX = require('../react/ReactJSX'); 21 | import cx = require('react/lib/cx'); 22 | 23 | var ReactPropTypes = React.PropTypes; 24 | 25 | interface TodoItemState { 26 | isEditing: boolean; 27 | } 28 | 29 | interface TodoItemElement { 30 | className: any; 31 | key: string; 32 | } 33 | 34 | class TodoItem extends ReactComponent { 35 | 36 | static propTypes: React.ValidationMap = { 37 | todo: ReactPropTypes.object.isRequired 38 | }; 39 | 40 | private _onToggleComplete = () => { 41 | TodoActions.toggleComplete(this.props.todo); 42 | }; 43 | 44 | private _onDoubleClick = () => { 45 | this.setState({isEditing: true}); 46 | }; 47 | 48 | /** 49 | * Event handler called within TodoTextInput. 50 | * Defining this here allows TodoTextInput to be used in multiple places 51 | * in different ways. 52 | * @param {string} text 53 | */ 54 | private _onSave = (text: string) => { 55 | if( text.length > 0 ) 56 | { 57 | TodoActions.updateText(this.props.todo.id, text); 58 | this.setState({isEditing: false}); 59 | } 60 | else 61 | { 62 | this._onDestroyClick(); 63 | } 64 | }; 65 | 66 | private _onDestroyClick = () => { 67 | TodoActions.destroy(this.props.todo.id); 68 | }; 69 | 70 | public getDerivedInitialState(): TodoItemState { 71 | return { 72 | isEditing: false 73 | }; 74 | } 75 | 76 | /** 77 | * @return {object} 78 | */ 79 | public render(): React.ReactElement { 80 | var todo = this.props.todo; 81 | // this.state = this.state || this.getInitialState(); 82 | 83 | var input: React.ReactElement; 84 | if (this.state.isEditing) { 85 | input = ReactJSX(` 86 | `, 91 | this, 92 | { 93 | TodoTextInput: TodoTextInput, 94 | todo: todo 95 | }); 96 | } 97 | 98 | // List items should get the class 'editing' when editing 99 | // and 'completed' when marked as completed. 100 | // Note that 'completed' is a classification while 'complete' is a state. 101 | // This differentiation between classification and state becomes important 102 | // in the naming of view actions toggleComplete() vs. destroyCompleted(). 103 | return ReactJSX(` 104 |
  • 110 |
    111 | 117 | 120 |
    122 | {input} 123 |
  • 124 | `, 125 | this, 126 | { 127 | 'todo': todo, 128 | 'cx': cx, 129 | 'input': input 130 | }); 131 | } 132 | 133 | 134 | }; 135 | 136 | export = TodoItem; 137 | -------------------------------------------------------------------------------- /src/components/TodoTextInput.jsx.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * Typescript port by Bernd Paradies, May 2015 10 | * @see https://github.com/facebook/flux/blob/master/examples/flux-todomvc/js/components/TodoTextInput.react.js 11 | * 12 | */ 13 | 14 | import React = require('react/addons'); 15 | import ReactComponent = require('../react/ReactComponent'); 16 | import ReactJSX = require('../react/ReactJSX'); 17 | 18 | var ReactPropTypes = React.PropTypes; 19 | var ENTER_KEY_CODE = 13; 20 | var ENTER_ESC_CODE = 27; 21 | 22 | interface TodoTextInputState { 23 | value: string; 24 | } 25 | 26 | interface TodoTextInputProps { 27 | className: string; 28 | id: string; 29 | placeholder: string; 30 | onSave: (value:string) => void; 31 | value: string; 32 | } 33 | 34 | interface InputEvent { 35 | target: HTMLInputElement; 36 | } 37 | 38 | // Probably a bug in the typings. If you use 'TodoTextInputProps' instead of 'any': 39 | // class TodoTextInput extends React.Component { 40 | // you'll get this error: 41 | // .../fluxts-todomvc/build/src/components/Header.react.ts(32,29): 42 | // error TS2345: Argument of type 'typeof TodoTextInput' is not assignable to parameter of type 'string'. 43 | 44 | class TodoTextInput extends ReactComponent { 45 | 46 | static propTypes: React.ValidationMap = { 47 | className: ReactPropTypes.string, 48 | id: ReactPropTypes.string, 49 | placeholder: ReactPropTypes.string, 50 | onSave: ReactPropTypes.func.isRequired, 51 | value: ReactPropTypes.string 52 | }; 53 | 54 | /** 55 | * Invokes the callback passed in as onSave, allowing this component to be 56 | * used in different ways. 57 | */ 58 | private _save = () => { 59 | this.props.onSave(this.state.value); 60 | this.setState({ 61 | value: '' 62 | }); 63 | }; 64 | 65 | /** 66 | * @param {object} event 67 | */ 68 | private _onChange = (event: InputEvent) => { 69 | this.setState({ 70 | value: event.target.value 71 | }); 72 | }; 73 | 74 | /** 75 | * @param {object} event 76 | */ 77 | private _onKeyDown = (event: KeyboardEvent) => { 78 | if (event.keyCode === ENTER_KEY_CODE) { 79 | this._save(); 80 | } 81 | else if (event.keyCode === ENTER_ESC_CODE) 82 | { 83 | this.props.onSave(this.props.value); 84 | this.setState({ 85 | value: this.props.value 86 | }); 87 | } 88 | }; 89 | 90 | public getDerivedInitialState(): TodoTextInputState { 91 | return { 92 | value: this.props.value || '' 93 | }; 94 | } 95 | 96 | /** 97 | * @return {object} 98 | */ 99 | public render(): React.ReactElement { 100 | // this.state = this.state || this.getInitialState(); 101 | return ReactJSX(` 102 | 112 | `, 113 | this); 114 | } 115 | }; 116 | 117 | export = TodoTextInput; 118 | -------------------------------------------------------------------------------- /src/flux/actions/TodoActionID.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * TodoActionID 10 | * 11 | * Typescript port by Bernd Paradies, May 2015 12 | * @see https://github.com/facebook/flux/blob/master/examples/flux-todomvc/js/constants/TodoConstants.js 13 | * 14 | */ 15 | 16 | enum TodoActionID { 17 | TODO_CREATE, 18 | TODO_COMPLETE, 19 | TODO_DESTROY, 20 | TODO_DESTROY_COMPLETED, 21 | TODO_TOGGLE_COMPLETE_ALL, 22 | TODO_UNDO_COMPLETE, 23 | TODO_UPDATE_TEXT 24 | } 25 | 26 | export = TodoActionID; 27 | -------------------------------------------------------------------------------- /src/flux/actions/TodoActions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * TodoActions 10 | * 11 | * Typescript port by Bernd Paradies, May 2015 12 | * @see https://github.com/facebook/flux/blob/master/examples/flux-todomvc/js/actions/TodoActions.js 13 | */ 14 | 15 | /// 16 | 17 | import AppDispatcher = require('../dispatcher/AppDispatcher'); 18 | import TodoActionID = require('./TodoActionID'); 19 | 20 | class TodoActionsStatic { 21 | 22 | // so jshint won't bark 23 | public TodoActionID = TodoActionID; 24 | 25 | /** 26 | * @param {string} text 27 | */ 28 | public create(text:string): void { 29 | AppDispatcher.dispatch({ 30 | actionType: TodoActionID.TODO_CREATE, 31 | text: text 32 | }); 33 | } 34 | 35 | /** 36 | * @param {string} id The ID of the ToDo item 37 | * @param {string} text 38 | */ 39 | public updateText(id: string, text:string): void { 40 | AppDispatcher.dispatch({ 41 | actionType: TodoActionID.TODO_UPDATE_TEXT, 42 | id: id, 43 | text: text 44 | }); 45 | } 46 | 47 | /** 48 | * Toggle whether a single ToDo is complete 49 | * @param {object} todo 50 | */ 51 | public toggleComplete(todo: TodoData): void { 52 | var id: string = todo.id; 53 | if (todo.complete) { 54 | AppDispatcher.dispatch({ 55 | actionType: TodoActionID.TODO_UNDO_COMPLETE, 56 | id: id 57 | }); 58 | } else { 59 | AppDispatcher.dispatch({ 60 | actionType: TodoActionID.TODO_COMPLETE, 61 | id: id 62 | }); 63 | } 64 | } 65 | 66 | /** 67 | * Mark all ToDos as complete 68 | */ 69 | public toggleCompleteAll(): void { 70 | AppDispatcher.dispatch({ 71 | actionType: TodoActionID.TODO_TOGGLE_COMPLETE_ALL 72 | }); 73 | } 74 | 75 | /** 76 | * @param {string} id 77 | */ 78 | public destroy(id: string): void { 79 | AppDispatcher.dispatch({ 80 | actionType: TodoActionID.TODO_DESTROY, 81 | id: id 82 | }); 83 | } 84 | 85 | /** 86 | * Delete all the completed ToDos 87 | */ 88 | public destroyCompleted(): void { 89 | AppDispatcher.dispatch({ 90 | actionType: TodoActionID.TODO_DESTROY_COMPLETED 91 | }); 92 | } 93 | } 94 | 95 | var TodoActions: TodoActionsStatic = new TodoActionsStatic(); 96 | 97 | export = TodoActions; 98 | -------------------------------------------------------------------------------- /src/flux/dispatcher/AppDispatcher.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * AppDispatcher 10 | * 11 | * A singleton that operates as the central hub for application updates. 12 | * 13 | * Typescript port by Bernd Paradies, May 2015 14 | * @see https://github.com/facebook/flux/blob/master/examples/flux-todomvc/js/dispatcher/AppDispatcher.js 15 | * 16 | */ 17 | 18 | /// 19 | 20 | import flux = require('flux'); 21 | var Dispatcher: flux.Dispatcher = new flux.Dispatcher(); 22 | 23 | export = Dispatcher; 24 | -------------------------------------------------------------------------------- /src/flux/stores/TodoStore.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * TodoStore 10 | * 11 | * Typescript port by Bernd Paradies, May 2015 12 | * @see https://github.com/facebook/flux/blob/master/examples/flux-todomvc/js/stores/TodoStore.js 13 | * 14 | */ 15 | 16 | /// 17 | /// 18 | /// 19 | /// 20 | 21 | import AppDispatcher = require('../dispatcher/AppDispatcher'); 22 | import TodoActionID = require('../actions/TodoActionID'); 23 | import assign = require('object-assign'); 24 | import EventEmitter = require('eventemitter3'); 25 | import store = require('store'); 26 | 27 | var CHANGE_EVENT = 'change'; 28 | 29 | type TodoMap = MapStringTo; 30 | 31 | var keyPrefix = "TodoItem-"; 32 | 33 | function loadFromStore(): TodoMap 34 | { 35 | var map: TodoMap = {}; 36 | store.forEach(function(key: string, val: TodoData) { 37 | if( typeof(key) == 'string' && key.indexOf(keyPrefix) == 0 ) { 38 | map[key.substring(keyPrefix.length)] = val; 39 | } 40 | }); 41 | return map; 42 | } 43 | 44 | var _todos: TodoMap = loadFromStore(); 45 | 46 | interface TodoUpdateData { 47 | id?: string; 48 | complete?: boolean; 49 | text?: string; 50 | } 51 | 52 | /** 53 | * Create a TODO item. 54 | * @param {string} text The content of the TODO 55 | */ 56 | function create(text:string): void { 57 | // Hand waving here -- not showing how this interacts with XHR or persistent 58 | // server-side storage. 59 | // Using the current timestamp + random number in place of a real id. 60 | var id: string = (+new Date() + Math.floor(Math.random() * 999999)).toString(36); 61 | _todos[id] = { 62 | id: id, 63 | complete: false, 64 | text: text 65 | }; 66 | store.set(keyPrefix + id, _todos[id]); 67 | } 68 | 69 | /** 70 | * Update a TODO item. 71 | * @param {string} id 72 | * @param {object} updates An object literal containing only the data to be 73 | * updated. 74 | */ 75 | function update(id: string, updates: TodoUpdateData): void { 76 | _todos[id] = assign({}, _todos[id], updates); 77 | store.set(keyPrefix + id, _todos[id]); 78 | } 79 | 80 | /** 81 | * Update all of the TODO items with the same object. 82 | * the data to be updated. Used to mark all TODOs as completed. 83 | * @param {object} updates An object literal containing only the data to be 84 | * updated. 85 | 86 | */ 87 | function updateAll(updates:TodoUpdateData): void { 88 | var id: string; 89 | for (id in _todos) { 90 | if(_todos.hasOwnProperty(id)) { 91 | update(id, updates); 92 | } 93 | } 94 | } 95 | 96 | /** 97 | * Delete a TODO item. 98 | * @param {string} id 99 | */ 100 | function destroy(id: string): void { 101 | delete _todos[id]; 102 | store.remove(keyPrefix + id); 103 | } 104 | 105 | /** 106 | * Delete all the completed TODO items. 107 | */ 108 | function destroyCompleted(): void { 109 | var id: string; 110 | for (id in _todos) { 111 | if (_todos[id].complete) { 112 | destroy(id); 113 | } 114 | } 115 | } 116 | 117 | class TodoStoreStatic extends EventEmitter { 118 | 119 | /** 120 | * Tests whether all the remaining TODO items are marked as completed. 121 | * @return {boolean} 122 | */ 123 | public areAllComplete(): boolean { 124 | var id: string; 125 | 126 | for(id in _todos) { 127 | if(!_todos[id].complete) { 128 | return false; 129 | } 130 | } 131 | return true; 132 | } 133 | 134 | /** 135 | * Get the entire collection of TODOs. 136 | * @return {object} 137 | */ 138 | public getAll(): TodoMap { 139 | return _todos; 140 | } 141 | 142 | public emitChange(): void { 143 | this.emit(CHANGE_EVENT); 144 | } 145 | 146 | /** 147 | * @param {function} callback 148 | */ 149 | public addChangeListener(callback: () => void): void { 150 | this.on(CHANGE_EVENT, callback); 151 | } 152 | 153 | /** 154 | * @param {function} callback 155 | */ 156 | public removeChangeListener(callback: () => void) { 157 | this.removeListener(CHANGE_EVENT, callback); 158 | } 159 | } 160 | 161 | var TodoStore: TodoStoreStatic = new TodoStoreStatic(); 162 | 163 | // Register callback to handle all updates 164 | AppDispatcher.register( function(action:TodoAction): void { 165 | var text: string; 166 | 167 | switch(action.actionType) { 168 | case TodoActionID.TODO_CREATE: 169 | text = action.text.trim(); 170 | if (text !== '') { 171 | create(text); 172 | } 173 | TodoStore.emitChange(); 174 | break; 175 | 176 | case TodoActionID.TODO_TOGGLE_COMPLETE_ALL: 177 | if (TodoStore.areAllComplete()) { 178 | updateAll({complete: false}); 179 | } else { 180 | updateAll({complete: true}); 181 | } 182 | TodoStore.emitChange(); 183 | break; 184 | 185 | case TodoActionID.TODO_UNDO_COMPLETE: 186 | update(action.id, {complete: false}); 187 | TodoStore.emitChange(); 188 | break; 189 | 190 | case TodoActionID.TODO_COMPLETE: 191 | update(action.id, {complete: true}); 192 | TodoStore.emitChange(); 193 | break; 194 | 195 | case TodoActionID.TODO_UPDATE_TEXT: 196 | text = action.text.trim(); 197 | if (text !== '') { 198 | update(action.id, {text: text}); 199 | } 200 | TodoStore.emitChange(); 201 | break; 202 | 203 | case TodoActionID.TODO_DESTROY: 204 | destroy(action.id); 205 | TodoStore.emitChange(); 206 | break; 207 | 208 | case TodoActionID.TODO_DESTROY_COMPLETED: 209 | destroyCompleted(); 210 | TodoStore.emitChange(); 211 | break; 212 | 213 | // default: 214 | // no op 215 | } 216 | }); 217 | 218 | export = TodoStore; 219 | -------------------------------------------------------------------------------- /src/react/ReactComponent.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | import React = require('react/addons'); 5 | 6 | class ReactComponent extends React.Component { 7 | 8 | public getDerivedInitialState(): S { 9 | return null; 10 | } 11 | 12 | public getInitialState = (): S => { 13 | return this.getDerivedInitialState(); 14 | }; 15 | 16 | /** 17 | * @see React.createClass 18 | */ 19 | constructor(props:P, context:any) { 20 | 21 | super(props, context); 22 | 23 | this.props = props; 24 | this.context = context; 25 | this.state = this.getInitialState(); 26 | 27 | // Nasty trick to avoid warnings. 28 | this.getInitialState = null; 29 | } 30 | } 31 | 32 | export = ReactComponent; 33 | -------------------------------------------------------------------------------- /src/react/ReactJSX.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | import React = require('react/addons'); 6 | import ReactTools = require('react-tools'); 7 | 8 | var _react = React; 9 | 10 | interface MapStringTo { 11 | [key:string]: T 12 | } 13 | 14 | /* 15 | Horrible, horrible trick using evil eval in order to create a function from a 16 | template string that has been transformed by ReactTools.transform(). 17 | 18 | That said, this technique is probably not known to many fellow devs. 19 | At least I didn't find it mentioned in this post that everybody refers to: 20 | http://perfectionkills.com/global-eval-what-are-the-options/ 21 | */ 22 | 23 | function ReactJSX( jsx: string, owner: any = null, imports: MapStringTo = {} ): React.ReactElement { 24 | var reactCode: string = 'function jsx(imports) { \n'; 25 | 26 | // No need to let every call site add React to the imports. 27 | // That one comes for free, we'll add it here: 28 | reactCode += 'var React = imports.React;\n'; 29 | imports['React'] = _react; 30 | 31 | for( var im in imports ) { 32 | reactCode += 'var ' + im + ' = imports.' + im + ';\n'; 33 | } 34 | 35 | reactCode += 'return (' + ReactTools.transform(jsx, { harmony: false }) + '); }; jsx;'; 36 | 37 | // I believe the discussion in http://perfectionkills.com/global-eval-what-are-the-options/ 38 | // missed that you can eval code that creates a function. 39 | // If done right, that function can be used like any other function in the caller's domain 40 | // as long as all scope variables are captured in 'imports'. 41 | var fn: () => React.ReactElement = eval.call(null,reactCode); 42 | var element: React.ReactElement = fn.call(owner, imports); 43 | return element; 44 | } 45 | 46 | export = ReactJSX; 47 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.5.0-beta", 3 | "compilerOptions": { 4 | "target": "es5", 5 | "module": "commonjs", 6 | "declaration": false, 7 | "noImplicitAny": true, 8 | "removeComments": false, 9 | "noLib": false, 10 | "outDir": "./build/js/" 11 | }, 12 | "filesGlob": [ 13 | "./src/**/*.ts", 14 | "./build/src/**/*.ts", 15 | "!./node_modules/**/*.ts" 16 | ], 17 | "files": [ 18 | "./build/src/actions/TodoActions.ts", 19 | "./build/src/app.ts", 20 | "./build/src/components/Footer.react.ts", 21 | "./build/src/components/Header.react.ts", 22 | "./build/src/components/MainSection.react.ts", 23 | "./build/src/components/TodoApp.react.ts", 24 | "./build/src/components/TodoItem.react.ts", 25 | "./build/src/components/TodoTextInput.react.ts", 26 | "./build/src/constants/TodoConstants.ts", 27 | "./build/src/dispatcher/AppDispatcher.ts", 28 | "./build/src/react/ReactComponent.ts", 29 | "./build/src/react/ReactJSX.ts", 30 | "./build/src/stores/TodoStore.ts", 31 | "./build/typings/browserify/browserify.d.ts", 32 | "./build/typings/flux/flux.d.ts", 33 | "./build/typings/node/node.d.ts", 34 | "./build/typings/object-assign/object-assign.d.ts", 35 | "./build/typings/react-jsx/react-jsx.d.ts", 36 | "./build/typings/react-tools/react-tools.d.ts", 37 | "./build/typings/react/react-addons.d.ts", 38 | "./build/typings/react/react-global.d.ts", 39 | "./build/typings/react/react.d.ts", 40 | "./build/typings/todomvc/todomvc.d.ts", 41 | "./build/typings/tsd.d.ts", 42 | "./build/typings/typings/tsd.d.ts" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /typings/object-assign/object-assign.d.ts: -------------------------------------------------------------------------------- 1 | 2 | declare module "object-assign" { 3 | function assign(target:U, source:T, ...args: U[]): T; 4 | 5 | export = assign; 6 | } 7 | -------------------------------------------------------------------------------- /typings/react-jsx/react-jsx.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Source: https://github.com/jbrantly/reactconf/blob/master/src_ts/lib/react-jsx.d.ts 3 | */ 4 | 5 | /// 6 | 7 | declare module 'react/addons' { 8 | function jsx(jsx?: string): React.ReactElement; 9 | } 10 | 11 | declare module 'react/lib/cx' { 12 | function cs(json:any): string; 13 | export = cs; 14 | } 15 | -------------------------------------------------------------------------------- /typings/react-tools/react-tools.d.ts: -------------------------------------------------------------------------------- 1 | 2 | declare module 'react-tools' { 3 | 4 | interface TransformOptions 5 | { 6 | harmony?: boolean; 7 | } 8 | 9 | function transform(jsx: string, options: TransformOptions): string; 10 | } 11 | -------------------------------------------------------------------------------- /typings/todomvc/todomvc.d.ts: -------------------------------------------------------------------------------- 1 | interface MapStringTo { 2 | [key:string]: T; 3 | } 4 | 5 | interface TodoData { 6 | id: string; 7 | complete: boolean; 8 | text: string; 9 | } 10 | 11 | interface TodoState { 12 | allTodos: MapStringTo; 13 | areAllComplete: boolean; 14 | } 15 | 16 | interface TodoItemProps { 17 | todo: TodoData; 18 | } 19 | 20 | interface TodoAction { 21 | actionType: number; 22 | id?: string; 23 | text?: string; 24 | } 25 | 26 | interface TodoTextInputElement { 27 | id: string; 28 | } 29 | 30 | declare module todomvc { 31 | type TodoMap = MapStringTo; 32 | } 33 | -------------------------------------------------------------------------------- /typings/tsd.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | /// 6 | /// 7 | /// 8 | -------------------------------------------------------------------------------- /typings/tsd.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "v4", 3 | "repo": "borisyankov/DefinitelyTyped", 4 | "ref": "master", 5 | "path": ".", 6 | "bundle": "typings/tsd.d.ts", 7 | "installed": { 8 | "browserify/browserify.d.ts": { 9 | "commit": "b3834d886a95789e6ab56e8244775ec10c5293d0" 10 | }, 11 | "node/node.d.ts": { 12 | "commit": "b3834d886a95789e6ab56e8244775ec10c5293d0" 13 | }, 14 | "react/react.d.ts": { 15 | "commit": "b3834d886a95789e6ab56e8244775ec10c5293d0" 16 | }, 17 | "react/react-addons.d.ts": { 18 | "commit": "b3834d886a95789e6ab56e8244775ec10c5293d0" 19 | }, 20 | "react/react-global.d.ts": { 21 | "commit": "b3834d886a95789e6ab56e8244775ec10c5293d0" 22 | }, 23 | "flux/flux.d.ts": { 24 | "commit": "b3834d886a95789e6ab56e8244775ec10c5293d0" 25 | }, 26 | "eventemitter3/eventemitter3.d.ts": { 27 | "commit": "b3834d886a95789e6ab56e8244775ec10c5293d0" 28 | }, 29 | "storejs/storejs.d.ts": { 30 | "commit": "b3834d886a95789e6ab56e8244775ec10c5293d0" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /web/css/app.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * base.css overrides 10 | */ 11 | 12 | /** 13 | * We are not changing from display:none, but rather re-rendering instead. 14 | * Therefore this needs to be displayed normally by default. 15 | */ 16 | #todo-list li .edit { 17 | display: inline; 18 | } 19 | -------------------------------------------------------------------------------- /web/debug.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flux • TodoMVC 6 | 7 | 8 | 9 | 10 |
    11 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flux • TodoMVC 6 | 7 | 8 | 9 | 10 |
    11 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /web/todomvc-app-css/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | button { 8 | margin: 0; 9 | padding: 0; 10 | border: 0; 11 | background: none; 12 | font-size: 100%; 13 | vertical-align: baseline; 14 | font-family: inherit; 15 | font-weight: inherit; 16 | color: inherit; 17 | -webkit-appearance: none; 18 | appearance: none; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-font-smoothing: antialiased; 21 | font-smoothing: antialiased; 22 | } 23 | 24 | body { 25 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 26 | line-height: 1.4em; 27 | background: #f5f5f5; 28 | color: #4d4d4d; 29 | min-width: 230px; 30 | max-width: 550px; 31 | margin: 0 auto; 32 | -webkit-font-smoothing: antialiased; 33 | -moz-font-smoothing: antialiased; 34 | font-smoothing: antialiased; 35 | font-weight: 300; 36 | } 37 | 38 | button, 39 | input[type="checkbox"] { 40 | outline: none; 41 | } 42 | 43 | .hidden { 44 | display: none; 45 | } 46 | 47 | #todoapp { 48 | background: #fff; 49 | margin: 130px 0 40px 0; 50 | position: relative; 51 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 52 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 53 | } 54 | 55 | #todoapp input::-webkit-input-placeholder { 56 | font-style: italic; 57 | font-weight: 300; 58 | color: #e6e6e6; 59 | } 60 | 61 | #todoapp input::-moz-placeholder { 62 | font-style: italic; 63 | font-weight: 300; 64 | color: #e6e6e6; 65 | } 66 | 67 | #todoapp input::input-placeholder { 68 | font-style: italic; 69 | font-weight: 300; 70 | color: #e6e6e6; 71 | } 72 | 73 | #todoapp h1 { 74 | position: absolute; 75 | top: -155px; 76 | width: 100%; 77 | font-size: 100px; 78 | font-weight: 100; 79 | text-align: center; 80 | color: rgba(175, 47, 47, 0.15); 81 | -webkit-text-rendering: optimizeLegibility; 82 | -moz-text-rendering: optimizeLegibility; 83 | text-rendering: optimizeLegibility; 84 | } 85 | 86 | #new-todo, 87 | .edit { 88 | position: relative; 89 | margin: 0; 90 | width: 100%; 91 | font-size: 24px; 92 | font-family: inherit; 93 | font-weight: inherit; 94 | line-height: 1.4em; 95 | border: 0; 96 | outline: none; 97 | color: inherit; 98 | padding: 6px; 99 | border: 1px solid #999; 100 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 101 | box-sizing: border-box; 102 | -webkit-font-smoothing: antialiased; 103 | -moz-font-smoothing: antialiased; 104 | font-smoothing: antialiased; 105 | } 106 | 107 | #new-todo { 108 | padding: 16px 16px 16px 60px; 109 | border: none; 110 | background: rgba(0, 0, 0, 0.003); 111 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 112 | } 113 | 114 | #main { 115 | position: relative; 116 | z-index: 2; 117 | border-top: 1px solid #e6e6e6; 118 | } 119 | 120 | label[for='toggle-all'] { 121 | display: none; 122 | } 123 | 124 | #toggle-all { 125 | position: absolute; 126 | top: -55px; 127 | left: -12px; 128 | width: 60px; 129 | height: 34px; 130 | text-align: center; 131 | border: none; /* Mobile Safari */ 132 | } 133 | 134 | #toggle-all:before { 135 | content: '❯'; 136 | font-size: 22px; 137 | color: #e6e6e6; 138 | padding: 10px 27px 10px 27px; 139 | } 140 | 141 | #toggle-all:checked:before { 142 | color: #737373; 143 | } 144 | 145 | #todo-list { 146 | margin: 0; 147 | padding: 0; 148 | list-style: none; 149 | } 150 | 151 | #todo-list li { 152 | position: relative; 153 | font-size: 24px; 154 | border-bottom: 1px solid #ededed; 155 | } 156 | 157 | #todo-list li:last-child { 158 | border-bottom: none; 159 | } 160 | 161 | #todo-list li.editing { 162 | border-bottom: none; 163 | padding: 0; 164 | } 165 | 166 | #todo-list li.editing .edit { 167 | display: block; 168 | width: 506px; 169 | padding: 13px 17px 12px 17px; 170 | margin: 0 0 0 43px; 171 | } 172 | 173 | #todo-list li.editing .view { 174 | display: none; 175 | } 176 | 177 | #todo-list li .toggle { 178 | text-align: center; 179 | width: 40px; 180 | /* auto, since non-WebKit browsers doesn't support input styling */ 181 | height: auto; 182 | position: absolute; 183 | top: 0; 184 | bottom: 0; 185 | margin: auto 0; 186 | border: none; /* Mobile Safari */ 187 | -webkit-appearance: none; 188 | appearance: none; 189 | } 190 | 191 | #todo-list li .toggle:after { 192 | content: url('data:image/svg+xml;utf8,'); 193 | } 194 | 195 | #todo-list li .toggle:checked:after { 196 | content: url('data:image/svg+xml;utf8,'); 197 | } 198 | 199 | #todo-list li label { 200 | white-space: pre; 201 | word-break: break-word; 202 | padding: 15px 60px 15px 15px; 203 | margin-left: 45px; 204 | display: block; 205 | line-height: 1.2; 206 | transition: color 0.4s; 207 | } 208 | 209 | #todo-list li.completed label { 210 | color: #d9d9d9; 211 | text-decoration: line-through; 212 | } 213 | 214 | #todo-list li .destroy { 215 | display: none; 216 | position: absolute; 217 | top: 0; 218 | right: 10px; 219 | bottom: 0; 220 | width: 40px; 221 | height: 40px; 222 | margin: auto 0; 223 | font-size: 30px; 224 | color: #cc9a9a; 225 | margin-bottom: 11px; 226 | transition: color 0.2s ease-out; 227 | } 228 | 229 | #todo-list li .destroy:hover { 230 | color: #af5b5e; 231 | } 232 | 233 | #todo-list li .destroy:after { 234 | content: '×'; 235 | } 236 | 237 | #todo-list li:hover .destroy { 238 | display: block; 239 | } 240 | 241 | #todo-list li .edit { 242 | display: none; 243 | } 244 | 245 | #todo-list li.editing:last-child { 246 | margin-bottom: -1px; 247 | } 248 | 249 | #footer { 250 | color: #777; 251 | padding: 10px 15px; 252 | height: 20px; 253 | text-align: center; 254 | border-top: 1px solid #e6e6e6; 255 | } 256 | 257 | #footer:before { 258 | content: ''; 259 | position: absolute; 260 | right: 0; 261 | bottom: 0; 262 | left: 0; 263 | height: 50px; 264 | overflow: hidden; 265 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 266 | 0 8px 0 -3px #f6f6f6, 267 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 268 | 0 16px 0 -6px #f6f6f6, 269 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 270 | } 271 | 272 | #todo-count { 273 | float: left; 274 | text-align: left; 275 | } 276 | 277 | #todo-count strong { 278 | font-weight: 300; 279 | } 280 | 281 | #filters { 282 | margin: 0; 283 | padding: 0; 284 | list-style: none; 285 | position: absolute; 286 | right: 0; 287 | left: 0; 288 | } 289 | 290 | #filters li { 291 | display: inline; 292 | } 293 | 294 | #filters li a { 295 | color: inherit; 296 | margin: 3px; 297 | padding: 3px 7px; 298 | text-decoration: none; 299 | border: 1px solid transparent; 300 | border-radius: 3px; 301 | } 302 | 303 | #filters li a.selected, 304 | #filters li a:hover { 305 | border-color: rgba(175, 47, 47, 0.1); 306 | } 307 | 308 | #filters li a.selected { 309 | border-color: rgba(175, 47, 47, 0.2); 310 | } 311 | 312 | #clear-completed, 313 | html #clear-completed:active { 314 | float: right; 315 | position: relative; 316 | line-height: 20px; 317 | text-decoration: none; 318 | cursor: pointer; 319 | position: relative; 320 | } 321 | 322 | #clear-completed:hover { 323 | text-decoration: underline; 324 | } 325 | 326 | #info { 327 | margin: 65px auto 0; 328 | color: #bfbfbf; 329 | font-size: 10px; 330 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 331 | text-align: center; 332 | } 333 | 334 | #info p { 335 | line-height: 1; 336 | } 337 | 338 | #info a { 339 | color: inherit; 340 | text-decoration: none; 341 | font-weight: 400; 342 | } 343 | 344 | #info a:hover { 345 | text-decoration: underline; 346 | } 347 | 348 | /* 349 | Hack to remove background from Mobile Safari. 350 | Can't use it globally since it destroys checkboxes in Firefox 351 | */ 352 | @media screen and (-webkit-min-device-pixel-ratio:0) { 353 | #toggle-all, 354 | #todo-list li .toggle { 355 | background: none; 356 | } 357 | 358 | #todo-list li .toggle { 359 | height: 40px; 360 | } 361 | 362 | #toggle-all { 363 | -webkit-transform: rotate(90deg); 364 | transform: rotate(90deg); 365 | -webkit-appearance: none; 366 | appearance: none; 367 | } 368 | } 369 | 370 | @media (max-width: 430px) { 371 | #footer { 372 | height: 50px; 373 | } 374 | 375 | #filters { 376 | bottom: 10px; 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /web/todomvc-common/base.css: -------------------------------------------------------------------------------- 1 | hr { 2 | margin: 20px 0; 3 | border: 0; 4 | border-top: 1px dashed #c5c5c5; 5 | border-bottom: 1px dashed #f7f7f7; 6 | } 7 | 8 | .learn a { 9 | font-weight: normal; 10 | text-decoration: none; 11 | color: #b83f45; 12 | } 13 | 14 | .learn a:hover { 15 | text-decoration: underline; 16 | color: #787e7e; 17 | } 18 | 19 | .learn h3, 20 | .learn h4, 21 | .learn h5 { 22 | margin: 10px 0; 23 | font-weight: 500; 24 | line-height: 1.2; 25 | color: #000; 26 | } 27 | 28 | .learn h3 { 29 | font-size: 24px; 30 | } 31 | 32 | .learn h4 { 33 | font-size: 18px; 34 | } 35 | 36 | .learn h5 { 37 | margin-bottom: 0; 38 | font-size: 14px; 39 | } 40 | 41 | .learn ul { 42 | padding: 0; 43 | margin: 0 0 30px 25px; 44 | } 45 | 46 | .learn li { 47 | line-height: 20px; 48 | } 49 | 50 | .learn p { 51 | font-size: 15px; 52 | font-weight: 300; 53 | line-height: 1.3; 54 | margin-top: 0; 55 | margin-bottom: 0; 56 | } 57 | 58 | #issue-count { 59 | display: none; 60 | } 61 | 62 | .quote { 63 | border: none; 64 | margin: 20px 0 60px 0; 65 | } 66 | 67 | .quote p { 68 | font-style: italic; 69 | } 70 | 71 | .quote p:before { 72 | content: '“'; 73 | font-size: 50px; 74 | opacity: .15; 75 | position: absolute; 76 | top: -20px; 77 | left: 3px; 78 | } 79 | 80 | .quote p:after { 81 | content: '”'; 82 | font-size: 50px; 83 | opacity: .15; 84 | position: absolute; 85 | bottom: -42px; 86 | right: 3px; 87 | } 88 | 89 | .quote footer { 90 | position: absolute; 91 | bottom: -40px; 92 | right: 0; 93 | } 94 | 95 | .quote footer img { 96 | border-radius: 3px; 97 | } 98 | 99 | .quote footer a { 100 | margin-left: 5px; 101 | vertical-align: middle; 102 | } 103 | 104 | .speech-bubble { 105 | position: relative; 106 | padding: 10px; 107 | background: rgba(0, 0, 0, .04); 108 | border-radius: 5px; 109 | } 110 | 111 | .speech-bubble:after { 112 | content: ''; 113 | position: absolute; 114 | top: 100%; 115 | right: 30px; 116 | border: 13px solid transparent; 117 | border-top-color: rgba(0, 0, 0, .04); 118 | } 119 | 120 | .learn-bar > .learn { 121 | position: absolute; 122 | width: 272px; 123 | top: 8px; 124 | left: -300px; 125 | padding: 10px; 126 | border-radius: 5px; 127 | background-color: rgba(255, 255, 255, .6); 128 | transition-property: left; 129 | transition-duration: 500ms; 130 | } 131 | 132 | @media (min-width: 899px) { 133 | .learn-bar { 134 | width: auto; 135 | padding-left: 300px; 136 | } 137 | 138 | .learn-bar > .learn { 139 | left: 8px; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /web/todomvc-common/base.js: -------------------------------------------------------------------------------- 1 | /* global _ */ 2 | (function () { 3 | 'use strict'; 4 | 5 | /* jshint ignore:start */ 6 | // Underscore's Template Module 7 | // Courtesy of underscorejs.org 8 | var _ = (function (_) { 9 | _.defaults = function (object) { 10 | if (!object) { 11 | return object; 12 | } 13 | for (var argsIndex = 1, argsLength = arguments.length; argsIndex < argsLength; argsIndex++) { 14 | var iterable = arguments[argsIndex]; 15 | if (iterable) { 16 | for (var key in iterable) { 17 | if (object[key] == null) { 18 | object[key] = iterable[key]; 19 | } 20 | } 21 | } 22 | } 23 | return object; 24 | } 25 | 26 | // By default, Underscore uses ERB-style template delimiters, change the 27 | // following template settings to use alternative delimiters. 28 | _.templateSettings = { 29 | evaluate : /<%([\s\S]+?)%>/g, 30 | interpolate : /<%=([\s\S]+?)%>/g, 31 | escape : /<%-([\s\S]+?)%>/g 32 | }; 33 | 34 | // When customizing `templateSettings`, if you don't want to define an 35 | // interpolation, evaluation or escaping regex, we need one that is 36 | // guaranteed not to match. 37 | var noMatch = /(.)^/; 38 | 39 | // Certain characters need to be escaped so that they can be put into a 40 | // string literal. 41 | var escapes = { 42 | "'": "'", 43 | '\\': '\\', 44 | '\r': 'r', 45 | '\n': 'n', 46 | '\t': 't', 47 | '\u2028': 'u2028', 48 | '\u2029': 'u2029' 49 | }; 50 | 51 | var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; 52 | 53 | // JavaScript micro-templating, similar to John Resig's implementation. 54 | // Underscore templating handles arbitrary delimiters, preserves whitespace, 55 | // and correctly escapes quotes within interpolated code. 56 | _.template = function(text, data, settings) { 57 | var render; 58 | settings = _.defaults({}, settings, _.templateSettings); 59 | 60 | // Combine delimiters into one regular expression via alternation. 61 | var matcher = new RegExp([ 62 | (settings.escape || noMatch).source, 63 | (settings.interpolate || noMatch).source, 64 | (settings.evaluate || noMatch).source 65 | ].join('|') + '|$', 'g'); 66 | 67 | // Compile the template source, escaping string literals appropriately. 68 | var index = 0; 69 | var source = "__p+='"; 70 | text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { 71 | source += text.slice(index, offset) 72 | .replace(escaper, function(match) { return '\\' + escapes[match]; }); 73 | 74 | if (escape) { 75 | source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; 76 | } 77 | if (interpolate) { 78 | source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; 79 | } 80 | if (evaluate) { 81 | source += "';\n" + evaluate + "\n__p+='"; 82 | } 83 | index = offset + match.length; 84 | return match; 85 | }); 86 | source += "';\n"; 87 | 88 | // If a variable is not specified, place data values in local scope. 89 | if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; 90 | 91 | source = "var __t,__p='',__j=Array.prototype.join," + 92 | "print=function(){__p+=__j.call(arguments,'');};\n" + 93 | source + "return __p;\n"; 94 | 95 | try { 96 | render = new Function(settings.variable || 'obj', '_', source); 97 | } catch (e) { 98 | e.source = source; 99 | throw e; 100 | } 101 | 102 | if (data) return render(data, _); 103 | var template = function(data) { 104 | return render.call(this, data, _); 105 | }; 106 | 107 | // Provide the compiled function source as a convenience for precompilation. 108 | template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; 109 | 110 | return template; 111 | }; 112 | 113 | return _; 114 | })({}); 115 | 116 | if (location.hostname === 'todomvc.com') { 117 | window._gaq = [['_setAccount','UA-31081062-1'],['_trackPageview']];(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];g.src='//www.google-analytics.com/ga.js';s.parentNode.insertBefore(g,s)}(document,'script')); 118 | } 119 | /* jshint ignore:end */ 120 | 121 | function redirect() { 122 | if (location.hostname === 'tastejs.github.io') { 123 | location.href = location.href.replace('tastejs.github.io/todomvc', 'todomvc.com'); 124 | } 125 | } 126 | 127 | function findRoot() { 128 | var base = location.href.indexOf('examples/'); 129 | return location.href.substr(0, base); 130 | } 131 | 132 | function getFile(file, callback) { 133 | if (!location.host) { 134 | return console.info('Miss the info bar? Run TodoMVC from a server to avoid a cross-origin error.'); 135 | } 136 | 137 | var xhr = new XMLHttpRequest(); 138 | 139 | xhr.open('GET', findRoot() + file, true); 140 | xhr.send(); 141 | 142 | xhr.onload = function () { 143 | if (xhr.status === 200 && callback) { 144 | callback(xhr.responseText); 145 | } 146 | }; 147 | } 148 | 149 | function Learn(learnJSON, config) { 150 | if (!(this instanceof Learn)) { 151 | return new Learn(learnJSON, config); 152 | } 153 | 154 | var template, framework; 155 | 156 | if (typeof learnJSON !== 'object') { 157 | try { 158 | learnJSON = JSON.parse(learnJSON); 159 | } catch (e) { 160 | return; 161 | } 162 | } 163 | 164 | if (config) { 165 | template = config.template; 166 | framework = config.framework; 167 | } 168 | 169 | if (!template && learnJSON.templates) { 170 | template = learnJSON.templates.todomvc; 171 | } 172 | 173 | if (!framework && document.querySelector('[data-framework]')) { 174 | framework = document.querySelector('[data-framework]').dataset.framework; 175 | } 176 | 177 | this.template = template; 178 | 179 | if (learnJSON.backend) { 180 | this.frameworkJSON = learnJSON.backend; 181 | this.frameworkJSON.issueLabel = framework; 182 | this.append({ 183 | backend: true 184 | }); 185 | } else if (learnJSON[framework]) { 186 | this.frameworkJSON = learnJSON[framework]; 187 | this.frameworkJSON.issueLabel = framework; 188 | this.append(); 189 | } 190 | 191 | this.fetchIssueCount(); 192 | } 193 | 194 | Learn.prototype.append = function (opts) { 195 | var aside = document.createElement('aside'); 196 | aside.innerHTML = _.template(this.template, this.frameworkJSON); 197 | aside.className = 'learn'; 198 | 199 | if (opts && opts.backend) { 200 | // Remove demo link 201 | var sourceLinks = aside.querySelector('.source-links'); 202 | var heading = sourceLinks.firstElementChild; 203 | var sourceLink = sourceLinks.lastElementChild; 204 | // Correct link path 205 | var href = sourceLink.getAttribute('href'); 206 | sourceLink.setAttribute('href', href.substr(href.lastIndexOf('http'))); 207 | sourceLinks.innerHTML = heading.outerHTML + sourceLink.outerHTML; 208 | } else { 209 | // Localize demo links 210 | var demoLinks = aside.querySelectorAll('.demo-link'); 211 | Array.prototype.forEach.call(demoLinks, function (demoLink) { 212 | if (demoLink.getAttribute('href').substr(0, 4) !== 'http') { 213 | demoLink.setAttribute('href', findRoot() + demoLink.getAttribute('href')); 214 | } 215 | }); 216 | } 217 | 218 | document.body.className = (document.body.className + ' learn-bar').trim(); 219 | document.body.insertAdjacentHTML('afterBegin', aside.outerHTML); 220 | }; 221 | 222 | Learn.prototype.fetchIssueCount = function () { 223 | var issueLink = document.getElementById('issue-count-link'); 224 | if (issueLink) { 225 | var url = issueLink.href.replace('https://github.com', 'https://api.github.com/repos'); 226 | var xhr = new XMLHttpRequest(); 227 | xhr.open('GET', url, true); 228 | xhr.onload = function (e) { 229 | var parsedResponse = JSON.parse(e.target.responseText); 230 | if (parsedResponse instanceof Array) { 231 | var count = parsedResponse.length 232 | if (count !== 0) { 233 | issueLink.innerHTML = 'This app has ' + count + ' open issues'; 234 | document.getElementById('issue-count').style.display = 'inline'; 235 | } 236 | } 237 | }; 238 | xhr.send(); 239 | } 240 | }; 241 | 242 | redirect(); 243 | getFile('learn.json', Learn); 244 | })(); 245 | --------------------------------------------------------------------------------