From ef2b1021aa23d7f45c902d59c02ea6030ed7da21 Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Sun, 28 Feb 2016 18:48:28 -0800 Subject: [PATCH 1/8] copy es6-babel-react-flux-karma to react-with-type-safe-flux --- react-with-type-safe-flux/.gitignore | 201 ++++++++++++++++++ react-with-type-safe-flux/LICENSE | 22 ++ react-with-type-safe-flux/README.md | 47 ++++ react-with-type-safe-flux/gulp/.eslintrc | 73 +++++++ react-with-type-safe-flux/gulp/clean.js | 29 +++ react-with-type-safe-flux/gulp/inject.js | 55 +++++ react-with-type-safe-flux/gulp/staticFiles.js | 31 +++ react-with-type-safe-flux/gulp/tests.js | 26 +++ react-with-type-safe-flux/gulp/webpack.js | 91 ++++++++ react-with-type-safe-flux/gulpFile.js | 66 ++++++ react-with-type-safe-flux/karma.conf.js | 68 ++++++ react-with-type-safe-flux/package.json | 72 +++++++ .../src/actions/GreetingActions.ts | 23 ++ .../src/components/App.tsx | 44 ++++ .../src/components/Greeting.tsx | 36 ++++ .../src/components/WhoToGreet.tsx | 52 +++++ .../action-types/GreetingActionTypes.ts | 7 + .../src/dispatcher/AppDispatcher.ts | 5 + react-with-type-safe-flux/src/index.html | 19 ++ react-with-type-safe-flux/src/main.tsx | 6 + .../src/stores/FluxStore.ts | 62 ++++++ .../src/stores/GreetingStore.ts | 38 ++++ react-with-type-safe-flux/src/tsconfig.json | 38 ++++ .../src/types/GreetingState.ts | 6 + .../test/components/App.tests.tsx | 31 +++ .../test/components/Greeting.tests.tsx | 44 ++++ .../test/components/WhoToGreet.tests.tsx | 67 ++++++ .../test/import-babel-polyfill.js | 1 + .../test/stores/GreetingStore.tests.ts | 56 +++++ react-with-type-safe-flux/test/tsconfig.json | 34 +++ react-with-type-safe-flux/tsd.json | 27 +++ react-with-type-safe-flux/webpack.config.js | 42 ++++ 32 files changed, 1419 insertions(+) create mode 100644 react-with-type-safe-flux/.gitignore create mode 100644 react-with-type-safe-flux/LICENSE create mode 100644 react-with-type-safe-flux/README.md create mode 100644 react-with-type-safe-flux/gulp/.eslintrc create mode 100644 react-with-type-safe-flux/gulp/clean.js create mode 100644 react-with-type-safe-flux/gulp/inject.js create mode 100644 react-with-type-safe-flux/gulp/staticFiles.js create mode 100644 react-with-type-safe-flux/gulp/tests.js create mode 100644 react-with-type-safe-flux/gulp/webpack.js create mode 100644 react-with-type-safe-flux/gulpFile.js create mode 100644 react-with-type-safe-flux/karma.conf.js create mode 100644 react-with-type-safe-flux/package.json create mode 100644 react-with-type-safe-flux/src/actions/GreetingActions.ts create mode 100644 react-with-type-safe-flux/src/components/App.tsx create mode 100644 react-with-type-safe-flux/src/components/Greeting.tsx create mode 100644 react-with-type-safe-flux/src/components/WhoToGreet.tsx create mode 100644 react-with-type-safe-flux/src/constants/action-types/GreetingActionTypes.ts create mode 100644 react-with-type-safe-flux/src/dispatcher/AppDispatcher.ts create mode 100644 react-with-type-safe-flux/src/index.html create mode 100644 react-with-type-safe-flux/src/main.tsx create mode 100644 react-with-type-safe-flux/src/stores/FluxStore.ts create mode 100644 react-with-type-safe-flux/src/stores/GreetingStore.ts create mode 100644 react-with-type-safe-flux/src/tsconfig.json create mode 100644 react-with-type-safe-flux/src/types/GreetingState.ts create mode 100644 react-with-type-safe-flux/test/components/App.tests.tsx create mode 100644 react-with-type-safe-flux/test/components/Greeting.tests.tsx create mode 100644 react-with-type-safe-flux/test/components/WhoToGreet.tests.tsx create mode 100644 react-with-type-safe-flux/test/import-babel-polyfill.js create mode 100644 react-with-type-safe-flux/test/stores/GreetingStore.tests.ts create mode 100644 react-with-type-safe-flux/test/tsconfig.json create mode 100644 react-with-type-safe-flux/tsd.json create mode 100644 react-with-type-safe-flux/webpack.config.js diff --git a/react-with-type-safe-flux/.gitignore b/react-with-type-safe-flux/.gitignore new file mode 100644 index 0000000..a983022 --- /dev/null +++ b/react-with-type-safe-flux/.gitignore @@ -0,0 +1,201 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ +dist/ + +# Visual Studo 2015 cache/options directory +.vs/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# JUnit test results +test-results + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding addin-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +*.[Cc]ache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +bower_components/ +typings/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt diff --git a/react-with-type-safe-flux/LICENSE b/react-with-type-safe-flux/LICENSE new file mode 100644 index 0000000..12a65bc --- /dev/null +++ b/react-with-type-safe-flux/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 John Reilly + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/react-with-type-safe-flux/README.md b/react-with-type-safe-flux/README.md new file mode 100644 index 0000000..8c18bca --- /dev/null +++ b/react-with-type-safe-flux/README.md @@ -0,0 +1,47 @@ +# ES6 + TypeScript + Babel + React + Karma: The Secret Recipe + +## Getting started + +You'll need [node / npm](https://nodejs.org/) and [tsd](http://definitelytyped.org/tsd/) installed globally. To get up and running just enter: + +``` +npm install +tsd install +npm run serve +``` + +This will: + +1. Download the npm packages you need +2. Download the typings from DefinitelyTyped that you need. +3. Compile the code and serve it up at [http://localhost:8080](http://localhost:8080) + +Now you need dev tools. There's a world of choice out there; there's [Atom](https://atom.io/), there's [VS Code](https://www.visualstudio.com/en-us/products/code-vs.aspx), there's [Sublime](http://www.sublimetext.com/). There's even something called [Visual Studio](http://www.visualstudio.com). It's all your choice really. + +For myself I've been using Atom combined with the mighty [atom-typescript package](https://atom.io/packages/atom-typescript). I advise you to give it a go. You won't look back. + +## I want to have an ASP.Net project and use Visual Studio + IIS Express to serve this instead + +If you drop this code into an empty Visual Studio ASP.Net project should should be good to go. You'll need this section in your `web.config` to ensure Visual Studio serves from the `dist` directory: + +``` + + + + + + + + + + + + +``` + +And rather than running `npm run serve` you'll want to use `npm run watch`. (This builds / watches your code / runs tests etc but does **not** spin up a web server.) + +Finally you'll want to set the following TypeScript options for your project + +- ECMAScript Version: ECMAScript 6 +- JSX compilation in TSX files: Preserve diff --git a/react-with-type-safe-flux/gulp/.eslintrc b/react-with-type-safe-flux/gulp/.eslintrc new file mode 100644 index 0000000..e91bbce --- /dev/null +++ b/react-with-type-safe-flux/gulp/.eslintrc @@ -0,0 +1,73 @@ +{ + "root": true, + "env": { + "commonjs": true, + }, + "rules": { + "camelcase": 2, + "comma-spacing": 1, + "consistent-return": 2, + "curly": [ 2, "all" ], + "dot-notation": [ + 2, + { "allowKeywords": true } + ], + "eol-last": 2, + "eqeqeq": 2, + "keyword-spacing": 2, + "new-cap": 2, + "new-parens": 2, + "no-alert": 2, + "no-array-constructor": 2, + "no-caller": 2, + "no-catch-shadow": 2, + "no-eval": 2, + "no-extend-native": 2, + "no-extra-bind": 2, + "no-extra-parens": [ 2, "functions" ], + "no-implied-eval": 2, + "no-iterator": 2, + "no-labels": 2, + "no-label-var": 2, + "no-lone-blocks": 2, + "no-loop-func": 2, + "no-multi-str": 2, + "no-native-reassign": 2, + "no-new": 2, + "no-new-func": 2, + "no-new-object": 2, + "no-new-wrappers": 2, + "no-octal-escape": 2, + "no-proto": 2, + "no-return-assign": 2, + "no-script-url": 2, + "no-sequences": 2, + "no-shadow": 2, + "no-shadow-restricted-names": 2, + "no-spaced-func": 2, + "no-trailing-spaces": 1, + "no-undef-init": 2, + "no-unused-expressions": 2, + "no-use-before-define": [ 2, "nofunc" ], + "no-with": 2, + "quotes": [ 1, "single" ], + "semi": 2, + "semi-spacing": [ + 2, + { + "before": false, + "after": true + } + ], + "space-infix-ops": 2, + "space-unary-ops": [ + 2, + { + "words": true, + "nonwords": false + } + ], + "strict": [ 2, "global" ], + "yoda": [ 2, "never" ] + } +} diff --git a/react-with-type-safe-flux/gulp/clean.js b/react-with-type-safe-flux/gulp/clean.js new file mode 100644 index 0000000..c51b4e4 --- /dev/null +++ b/react-with-type-safe-flux/gulp/clean.js @@ -0,0 +1,29 @@ +'use strict'; + +var del = require('del'); +var gutil = require('gulp-util'); +var fs = require('fs'); + +function run(done) { + fs.stat('./dist', function(err){ + if (err) { + // Never existed + done(); + } + else { + del(['./dist'], { force: true }) + .then(function(paths) { + gutil.log('Deleted files/folders:\n', paths.join('\n')); + done(); + }) + .catch(function(error) { + gutil.log('Problem deleting:\n', error); + done(); + }); + } + }); +} + +module.exports = { + run: function(done) { return run(done); } +}; diff --git a/react-with-type-safe-flux/gulp/inject.js b/react-with-type-safe-flux/gulp/inject.js new file mode 100644 index 0000000..e4133e5 --- /dev/null +++ b/react-with-type-safe-flux/gulp/inject.js @@ -0,0 +1,55 @@ +'use strict'; + +var gulp = require('gulp'); +var inject = require('gulp-inject'); +var glob = require('glob'); + +function injectIndex(options) { + function run() { + var target = gulp.src('./src/index.html'); + var sources = gulp.src([ + //'./dist/styles/main*.css', + './dist/scripts/vendor*.js', + './dist/scripts/main*.js' + ], { read: false }); + + return target + .pipe(inject(sources, { ignorePath: '/dist/', addRootSlash: false, removeTags: true })) + .pipe(gulp.dest('./dist')); + } + + var jsCssGlob = 'dist/**/*.{js,css}'; + + function checkForInitialFilesThenRun() { + glob(jsCssGlob, function (er, files) { + var filesWeNeed = ['dist/scripts/main', 'dist/scripts/vendor'/*, 'dist/styles/main'*/]; + + function fileIsPresent(fileWeNeed) { + return files.some(function(file) { + return file.indexOf(fileWeNeed) !== -1; + }); + } + + if (filesWeNeed.every(fileIsPresent)) { + run('initial build'); + } else { + checkForInitialFilesThenRun(); + } + }); + } + + checkForInitialFilesThenRun(); + + if (options.shouldWatch) { + gulp.watch(jsCssGlob, function(evt) { + if (evt.path && evt.type === 'changed') { + run(evt.path); + } + }); + } +} + +module.exports = { + build: function() { return injectIndex({ shouldWatch: false }); }, + watch: function() { return injectIndex({ shouldWatch: true }); } +}; diff --git a/react-with-type-safe-flux/gulp/staticFiles.js b/react-with-type-safe-flux/gulp/staticFiles.js new file mode 100644 index 0000000..20327f2 --- /dev/null +++ b/react-with-type-safe-flux/gulp/staticFiles.js @@ -0,0 +1,31 @@ +'use strict'; + +var gulp = require('gulp'); +var cache = require('gulp-cached'); + +var targets = [ + { description: 'INDEX', src: './src/index.html', dest: './dist' } +]; + +function copy(options) { + function run(target) { + gulp.src(target.src) + .pipe(cache(target.description)) + .pipe(gulp.dest(target.dest)); + } + + function watch(target) { + gulp.watch(target.src, function() { run(target); }); + } + + targets.forEach(run); + + if (options.shouldWatch) { + targets.forEach(watch); + } +} + +module.exports = { + build: function() { return copy({ shouldWatch: false }); }, + watch: function() { return copy({ shouldWatch: true }); } +}; diff --git a/react-with-type-safe-flux/gulp/tests.js b/react-with-type-safe-flux/gulp/tests.js new file mode 100644 index 0000000..4d6ba04 --- /dev/null +++ b/react-with-type-safe-flux/gulp/tests.js @@ -0,0 +1,26 @@ +'use strict'; + +var Server = require('karma').Server; +var path = require('path'); +var gutil = require('gulp-util'); + +module.exports = { + watch: function() { + // Documentation: https://karma-runner.github.io/0.13/dev/public-api.html + var karmaConfig = { + configFile: path.join(__dirname, '../karma.conf.js'), + singleRun: false, + + // Fancy runner + plugins: ['karma-webpack', 'karma-jasmine', 'karma-mocha-reporter', /*'karma-junit-reporter', 'karma-coverage', */'karma-sourcemap-loader', 'karma-phantomjs-launcher'], + reporters: ['mocha'] + }; + + new Server(karmaConfig, karmaCompleted).start(); + + function karmaCompleted(exitCode) { + gutil.log('Karma has exited with:', exitCode); + process.exit(exitCode); + } + } +}; diff --git a/react-with-type-safe-flux/gulp/webpack.js b/react-with-type-safe-flux/gulp/webpack.js new file mode 100644 index 0000000..37c4ded --- /dev/null +++ b/react-with-type-safe-flux/gulp/webpack.js @@ -0,0 +1,91 @@ +'use strict'; + +var gulp = require('gulp'); +var gutil = require('gulp-util'); +var webpack = require('webpack'); +var WebpackNotifierPlugin = require('webpack-notifier'); +var webpackConfig = require('../webpack.config.js'); + +function buildProduction(done) { + // modify some webpack config options + var myProdConfig = Object.create(webpackConfig); + myProdConfig.output.filename = '[name].[hash].js'; + + myProdConfig.plugins = myProdConfig.plugins.concat( + new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', filename: 'vendor.[hash].js' }), + new webpack.optimize.DedupePlugin(), + new webpack.optimize.UglifyJsPlugin() + ); + + // run webpack + webpack(myProdConfig, function(err, stats) { + if(err) { throw new gutil.PluginError('webpack:build', err); } + gutil.log('[webpack:build]', stats.toString({ + colors: true + })); + + if (done) { done(); } + }); +} + +function createDevCompiler() { + // modify some webpack config options + var myDevConfig = Object.create(webpackConfig); + myDevConfig.devtool = 'inline-source-map'; + myDevConfig.debug = true; + + myDevConfig.plugins = myDevConfig.plugins.concat( + new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', filename: 'vendor.js' }), + new WebpackNotifierPlugin({ title: 'Webpack build', excludeWarnings: true }) + ); + + // create a single instance of the compiler to allow caching + return webpack(myDevConfig); +} + +function buildDevelopment(done, devCompiler) { + // run webpack + devCompiler.run(function(err, stats) { + if(err) { throw new gutil.PluginError('webpack:build-dev', err); } + gutil.log('[webpack:build-dev]', stats.toString({ + chunks: false, + colors: true + })); + + if (done) { done(); } + }); +} + + +function bundle(options) { + var devCompiler; + + function build(done) { + if (options.shouldWatch) { + buildDevelopment(done, devCompiler); + } else { + buildProduction(done); + } + } + + if (options.shouldWatch) { + devCompiler = createDevCompiler(); + + gulp.watch('src/**/*', function() { build(); }); + } + + return new Promise(function(resolve, reject) { + build(function (err) { + if (err) { + reject(err); + } else { + resolve('webpack built'); + } + }); + }); +} + +module.exports = { + build: function() { return bundle({ shouldWatch: false }); }, + watch: function() { return bundle({ shouldWatch: true }); } +}; diff --git a/react-with-type-safe-flux/gulpFile.js b/react-with-type-safe-flux/gulpFile.js new file mode 100644 index 0000000..b7b3abe --- /dev/null +++ b/react-with-type-safe-flux/gulpFile.js @@ -0,0 +1,66 @@ +/* eslint-disable no-var, strict, prefer-arrow-callback */ +'use strict'; + +var gulp = require('gulp'); +var gutil = require('gulp-util'); +var eslint = require('gulp-eslint'); +var webpack = require('./gulp/webpack'); +var staticFiles = require('./gulp/staticFiles'); +var tests = require('./gulp/tests'); +var clean = require('./gulp/clean'); +var inject = require('./gulp/inject'); + +var lintSrcs = ['./gulp/**/*.js']; + +gulp.task('delete-dist', function (done) { + clean.run(done); +}); + +gulp.task('build-process.env.NODE_ENV', function () { + process.env.NODE_ENV = 'production'; +}); + +gulp.task('build-js', ['delete-dist', 'build-process.env.NODE_ENV'], function(done) { + webpack.build().then(function() { done(); }); +}); + +gulp.task('build-other', ['delete-dist', 'build-process.env.NODE_ENV'], function() { + staticFiles.build(); +}); + +gulp.task('build', ['build-js', 'build-other', 'lint'], function () { + inject.build(); +}); + +gulp.task('lint', function () { + return gulp.src(lintSrcs) + .pipe(eslint()) + .pipe(eslint.format()); +}); + +gulp.task('watch', ['delete-dist'], function(done) { + process.env.NODE_ENV = 'development'; + Promise.all([ + webpack.watch()//, + //less.watch() + ]).then(function() { + gutil.log('Now that initial assets (js and css) are generated inject will start...'); + inject.watch(); + done(); + }).catch(function(error) { + gutil.log('Problem generating initial assets (js and css)', error); + }); + + gulp.watch(lintSrcs, ['lint']); + staticFiles.watch(); + tests.watch(); +}); + +gulp.task('watch-and-serve', ['watch'], function() { + // local as not required for build + var express = require('express') + var app = express() + + app.use(express.static('dist', {'index': 'index.html'})) + app.listen(8080); +}); diff --git a/react-with-type-safe-flux/karma.conf.js b/react-with-type-safe-flux/karma.conf.js new file mode 100644 index 0000000..10ffba7 --- /dev/null +++ b/react-with-type-safe-flux/karma.conf.js @@ -0,0 +1,68 @@ +/* eslint-disable no-var, strict */ +'use strict'; + +var webpackConfig = require('./webpack.config.js'); + +module.exports = function(config) { + // Documentation: https://karma-runner.github.io/0.13/config/configuration-file.html + config.set({ + browsers: [ 'PhantomJS' ], + + files: [ + 'test/import-babel-polyfill.js', // This ensures we have the es6 shims in place from babel + 'test/**/*.tests.ts', + 'test/**/*.tests.tsx' + ], + + port: 9876, + + frameworks: [ 'jasmine' ], + + logLevel: config.LOG_INFO, //config.LOG_DEBUG + + preprocessors: { + 'test/import-babel-polyfill.js': [ 'webpack', 'sourcemap' ], + 'src/**/*.{ts,tsx}': [ 'webpack', 'sourcemap' ], + 'test/**/*.tests.{ts,tsx}': [ 'webpack', 'sourcemap' ] + }, + + webpack: { + devtool: 'eval-source-map', //'inline-source-map', + debug: true, + module: webpackConfig.module, + resolve: webpackConfig.resolve + }, + + webpackMiddleware: { + quiet: true, + stats: { + colors: true + } + }, + + // reporter options + mochaReporter: { + colors: { + success: 'bgGreen', + info: 'cyan', + warning: 'bgBlue', + error: 'bgRed' + } + }, + + // the default configuration + junitReporter: { + outputDir: 'test-results', // results will be saved as $outputDir/$browserName.xml + outputFile: undefined, // if included, results will be saved as $outputDir/$browserName/$outputFile + suite: '' + }, + + coverageReporter: { + reporters:[ + //{type: 'html', dir:'coverage/'}, // https://github.com/karma-runner/karma-coverage/issues/123 + {type: 'text'}, + {type: 'text-summary'} + ], + } + }); +}; diff --git a/react-with-type-safe-flux/package.json b/react-with-type-safe-flux/package.json new file mode 100644 index 0000000..04d8f90 --- /dev/null +++ b/react-with-type-safe-flux/package.json @@ -0,0 +1,72 @@ +{ + "name": "es6-babel-react-flux-karma", + "version": "1.0.0", + "description": "ES6 + TypeScript + Babel + React + Karma: The Secret Recipe", + "main": "index.js", + "scripts": { + "test": "karma start --reporters mocha,junit --single-run --browsers PhantomJS", + "serve": "gulp watch-and-serve", + "watch": "gulp watch", + "build": "gulp build" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/microsoft/typescriptsamples.git" + }, + "keywords": [ + "React", + "Flux", + "ES6", + "typescript" + ], + "author": "John Reilly", + "license": "MIT", + "bugs": { + "url": "https://github.com/microsoft/typescriptsamples/issues" + }, + "homepage": "https://github.com/Microsoft/TypeScriptSamples/tree/master/es6-babel-react-flux-karma#readme", + "devDependencies": { + "babel": "^6.0.0", + "babel-core": "^6.0.0", + "babel-loader": "^6.0.0", + "babel-polyfill": "^6.0.0", + "babel-preset-es2015": "^6.0.0", + "babel-preset-react": "^6.0.0", + "del": "^2.0.2", + "eslint": "^2.0.0", + "express": "^4.13.3", + "flux": "^2.0.3", + "glob": "^7.0.0", + "gulp": "^3.9.0", + "gulp-autoprefixer": "^3.1.0", + "gulp-cached": "^1.1.0", + "gulp-cssmin": "^0.1.7", + "gulp-eslint": "^2.0.0", + "gulp-if": "^2.0.0", + "gulp-inject": "^3.0.0", + "gulp-notify": "^2.2.0", + "gulp-sourcemaps": "^1.5.2", + "gulp-streamify": "1.0.2", + "gulp-uglify": "^1.2.0", + "gulp-util": "^3.0.6", + "jasmine-core": "^2.3.4", + "karma": "^0.13.10", + "karma-coverage": "^0.5.2", + "karma-jasmine": "^0.3.6", + "karma-junit-reporter": "^0.3.7", + "karma-mocha-reporter": "^1.1.1", + "karma-phantomjs-launcher": "^1.0.0", + "karma-phantomjs-shim": "^1.1.1", + "karma-sourcemap-loader": "^0.3.6", + "karma-webpack": "^1.7.0", + "phantomjs": "^2.1.3", + "phantomjs-prebuilt": "^2.1.4", + "react": "^0.14.3", + "react-addons-test-utils": "^0.14.3", + "react-dom": "^0.14.3", + "ts-loader": "^0.8.1", + "typescript": "^1.8.0", + "webpack": "^1.12.2", + "webpack-notifier": "^1.2.1" + } +} diff --git a/react-with-type-safe-flux/src/actions/GreetingActions.ts b/react-with-type-safe-flux/src/actions/GreetingActions.ts new file mode 100644 index 0000000..c451280 --- /dev/null +++ b/react-with-type-safe-flux/src/actions/GreetingActions.ts @@ -0,0 +1,23 @@ +import AppDispatcher from '../dispatcher/AppDispatcher'; +import GreetingActionTypes from '../constants/action-types/GreetingActionTypes'; + +export function addGreeting(newGreeting: string) { + AppDispatcher.dispatch({ + newGreeting, + type: GreetingActionTypes.ADD_GREETING + }); +} + +export function newGreetingChanged(newGreeting: string) { + AppDispatcher.dispatch({ + newGreeting, + type: GreetingActionTypes.NEW_GREETING_CHANGED + }); +} + +export function removeGreeting(greetingToRemove: string) { + AppDispatcher.dispatch({ + greetingToRemove, + type: GreetingActionTypes.REMOVE_GREETING + }); +} diff --git a/react-with-type-safe-flux/src/components/App.tsx b/react-with-type-safe-flux/src/components/App.tsx new file mode 100644 index 0000000..f6bceb6 --- /dev/null +++ b/react-with-type-safe-flux/src/components/App.tsx @@ -0,0 +1,44 @@ +import * as React from 'react'; +import GreetingStore from '../stores/GreetingStore'; +import * as GreetingActions from '../actions/GreetingActions'; +import GreetingState from '../types/GreetingState'; +import WhoToGreet from './WhoToGreet'; +import Greeting from './Greeting'; + +class App extends React.Component { + constructor(props) { + super(props); + this.state = this._getStateFromStores(); + } + + componentWillMount() { + GreetingStore.addChangeListener(this._onChange); + } + + componentWillUnmount() { + GreetingStore.removeChangeListener(this._onChange); + } + + render() { + const { greetings, newGreeting } = this.state; + return ( +
+

Hello People!

+ + + + { greetings.map((g, index) => ) } +
+ ); + } + + _onChange = () => { + this.setState(this._getStateFromStores()); + } + + _getStateFromStores() { + return GreetingStore.getState(); + } +} + +export default App; diff --git a/react-with-type-safe-flux/src/components/Greeting.tsx b/react-with-type-safe-flux/src/components/Greeting.tsx new file mode 100644 index 0000000..213af66 --- /dev/null +++ b/react-with-type-safe-flux/src/components/Greeting.tsx @@ -0,0 +1,36 @@ +import * as React from 'react'; +import * as GreetingActions from '../actions/GreetingActions'; + +interface Props { + key: number; + targetOfGreeting: string; +} + +class Greeting extends React.Component { + constructor(props) { + super(props); + } + + static propTypes: React.ValidationMap = { + targetOfGreeting: React.PropTypes.string.isRequired + } + + render() { + return ( +

+ Hello { this.props.targetOfGreeting }! + + +

+ ); + } + + _onClick = (event) => { + GreetingActions.removeGreeting(this.props.targetOfGreeting); + } +} + +export default Greeting; diff --git a/react-with-type-safe-flux/src/components/WhoToGreet.tsx b/react-with-type-safe-flux/src/components/WhoToGreet.tsx new file mode 100644 index 0000000..5e3794f --- /dev/null +++ b/react-with-type-safe-flux/src/components/WhoToGreet.tsx @@ -0,0 +1,52 @@ +import * as React from 'react'; +import * as GreetingActions from '../actions/GreetingActions'; + +interface Props { + newGreeting: string; +} + +class WhoToGreet extends React.Component { + constructor(props) { + super(props); + } + + static propTypes: React.ValidationMap = { + newGreeting: React.PropTypes.string.isRequired + } + + render() { + return ( +
+
+ + +
+
+ ); + } + + get _preventSubmission() { + return !this.props.newGreeting; + } + + _handleNewGreetingChange = (event) => { + const { target: { value: newGreeting } } = event; + GreetingActions.newGreetingChanged(newGreeting); + } + + _onSubmit = (event) => { + event.preventDefault(); + + if (!this._preventSubmission) { + GreetingActions.addGreeting(this.props.newGreeting); + } + } +} + +export default WhoToGreet; diff --git a/react-with-type-safe-flux/src/constants/action-types/GreetingActionTypes.ts b/react-with-type-safe-flux/src/constants/action-types/GreetingActionTypes.ts new file mode 100644 index 0000000..ef9fda4 --- /dev/null +++ b/react-with-type-safe-flux/src/constants/action-types/GreetingActionTypes.ts @@ -0,0 +1,7 @@ +const GreetingActionTypes = { + ADD_GREETING: 'GreetingActionTypes.ADD_GREETING', + REMOVE_GREETING: 'GreetingActionTypes.REMOVE_GREETING', + NEW_GREETING_CHANGED: 'GreetingActionTypes.NEW_GREETING_CHANGED' +}; + +export default GreetingActionTypes; diff --git a/react-with-type-safe-flux/src/dispatcher/AppDispatcher.ts b/react-with-type-safe-flux/src/dispatcher/AppDispatcher.ts new file mode 100644 index 0000000..428b8a6 --- /dev/null +++ b/react-with-type-safe-flux/src/dispatcher/AppDispatcher.ts @@ -0,0 +1,5 @@ +import { Dispatcher } from 'flux'; + +const dispatcherInstance = new Dispatcher(); + +export default dispatcherInstance; diff --git a/react-with-type-safe-flux/src/index.html b/react-with-type-safe-flux/src/index.html new file mode 100644 index 0000000..c5c150d --- /dev/null +++ b/react-with-type-safe-flux/src/index.html @@ -0,0 +1,19 @@ + + + + + + + + ES6 + Babel + React + Flux + Karma: The Secret Recipe + + + + + + +
+ + + + diff --git a/react-with-type-safe-flux/src/main.tsx b/react-with-type-safe-flux/src/main.tsx new file mode 100644 index 0000000..1e14a73 --- /dev/null +++ b/react-with-type-safe-flux/src/main.tsx @@ -0,0 +1,6 @@ +import 'babel-polyfill'; +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import App from './components/App'; + +ReactDOM.render(, document.getElementById('content')); diff --git a/react-with-type-safe-flux/src/stores/FluxStore.ts b/react-with-type-safe-flux/src/stores/FluxStore.ts new file mode 100644 index 0000000..552aaf7 --- /dev/null +++ b/react-with-type-safe-flux/src/stores/FluxStore.ts @@ -0,0 +1,62 @@ +import { EventEmitter } from 'events'; + +const CHANGE_EVENT = 'change'; + +class FluxStore { + _changed: boolean; + _emitter: EventEmitter; + dispatchToken: string; + _dispatcher: Flux.Dispatcher; + _cleanStateFn: () => TState; + _state: TState; + + constructor(dispatcher, cleanStateFn) { + this._emitter = new EventEmitter(); + this._changed = false; + this._dispatcher = dispatcher; + this.dispatchToken = dispatcher.register(payload => { + this._invokeOnDispatch(payload); + }); + + this._cleanStateFn = cleanStateFn; + this._state = this._cleanStateFn(); + } + + /** + * Is idempotent per dispatched event + */ + emitChange() { + this._changed = true; + } + + hasChanged() { return this._changed; } + + addChangeListener(callback) { + this._emitter.on(CHANGE_EVENT, callback); + } + + removeChangeListener(callback) { + this._emitter.removeListener(CHANGE_EVENT, callback); + } + + _cleanState() { + this._changed = false; + this._state = this._cleanStateFn(); + } + + _invokeOnDispatch(payload) { + this._changed = false; + this._onDispatch(payload); + if (this._changed) { + this._emitter.emit(CHANGE_EVENT); + } + } + + _onDispatch(payload) { + if (process.env.NODE_ENV !== 'production') { + console.error(`${this.constructor.name} has not overridden FluxStore.__onDispatch(), which is required`); // eslint-disable-line no-console + } + } +} + +export default FluxStore; diff --git a/react-with-type-safe-flux/src/stores/GreetingStore.ts b/react-with-type-safe-flux/src/stores/GreetingStore.ts new file mode 100644 index 0000000..c80dbfc --- /dev/null +++ b/react-with-type-safe-flux/src/stores/GreetingStore.ts @@ -0,0 +1,38 @@ +import FluxStore from './FluxStore'; +import GreetingActionTypes from '../constants/action-types/GreetingActionTypes'; +import AppDispatcher from '../dispatcher/AppDispatcher'; +import GreetingState from '../types/GreetingState'; + +class GreeterStore extends FluxStore { + constructor(dispatcher) { + super(dispatcher, () => ({ + greetings: [], + newGreeting: '' + })); + } + + getState() { + return this._state + } + + _onDispatch(action) { + switch(action.type) { + case GreetingActionTypes.ADD_GREETING: + this._state.newGreeting = ''; + this._state.greetings = this._state.greetings.concat(action.newGreeting); + this.emitChange(); + break; + case GreetingActionTypes.REMOVE_GREETING: + this._state.greetings = this._state.greetings.filter(g => g !== action.greetingToRemove); + this.emitChange(); + break; + case GreetingActionTypes.NEW_GREETING_CHANGED: + this._state.newGreeting = action.newGreeting; + this.emitChange(); + break; + } + } +} + +const greeterStoreInstance = new GreeterStore(AppDispatcher); +export default greeterStoreInstance; diff --git a/react-with-type-safe-flux/src/tsconfig.json b/react-with-type-safe-flux/src/tsconfig.json new file mode 100644 index 0000000..dbffad0 --- /dev/null +++ b/react-with-type-safe-flux/src/tsconfig.json @@ -0,0 +1,38 @@ +{ + "compileOnSave": false, + "filesGlob": [ + "../typings/**/*.*.ts", + "!../typings/jasmine/jasmine.d.ts", + "!../typings/react/react-addons-test-utils.d.ts", + "**/*.{ts,tsx}" + ], + "compilerOptions": { + "jsx": "preserve", + "target": "es6", + "noImplicitAny": false, + "removeComments": false, + "preserveConstEnums": true, + "sourceMap": true + }, + "files": [ + "../typings/flux/flux.d.ts", + "../typings/node/node.d.ts", + "../typings/react/react-dom.d.ts", + "../typings/react/react.d.ts", + "../typings/tsd.d.ts", + "actions/GreetingActions.ts", + "components/App.tsx", + "components/Greeting.tsx", + "components/WhoToGreet.tsx", + "constants/action-types/GreetingActionTypes.ts", + "dispatcher/AppDispatcher.ts", + "main.tsx", + "stores/FluxStore.ts", + "stores/GreetingStore.ts", + "types/GreetingState.ts" + ], + "exclude": [], + "atom": { + "rewriteTsconfig": true + } +} diff --git a/react-with-type-safe-flux/src/types/GreetingState.ts b/react-with-type-safe-flux/src/types/GreetingState.ts new file mode 100644 index 0000000..656b201 --- /dev/null +++ b/react-with-type-safe-flux/src/types/GreetingState.ts @@ -0,0 +1,6 @@ +interface GreetingState { + greetings: string[]; + newGreeting: string; +} + +export default GreetingState; diff --git a/react-with-type-safe-flux/test/components/App.tests.tsx b/react-with-type-safe-flux/test/components/App.tests.tsx new file mode 100644 index 0000000..1530e64 --- /dev/null +++ b/react-with-type-safe-flux/test/components/App.tests.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import * as TestUtils from 'react-addons-test-utils'; +import App from '../../src/components/App'; +import WhoToGreet from '../../src/components/WhoToGreet'; +import Greeting from '../../src/components/Greeting'; +import GreetingStore from '../../src/stores/GreetingStore'; + +describe('App', () => { + it('renders expected HTML', () => { + const app = render({ greetings: ['James'], newGreeting: 'Benjamin' }); + expect(app).toEqual( +
+

Hello People!

+ + + + { [ + + ] } +
+ ); + }); + + function render(state) { + const shallowRenderer = TestUtils.createRenderer(); + spyOn(GreetingStore, 'getState').and.returnValue(state); + + shallowRenderer.render(); + return shallowRenderer.getRenderOutput(); + } +}); diff --git a/react-with-type-safe-flux/test/components/Greeting.tests.tsx b/react-with-type-safe-flux/test/components/Greeting.tests.tsx new file mode 100644 index 0000000..9ea44b4 --- /dev/null +++ b/react-with-type-safe-flux/test/components/Greeting.tests.tsx @@ -0,0 +1,44 @@ +import * as React from 'react'; +import * as TestUtils from 'react-addons-test-utils'; +import Greeting from '../../src/components/Greeting'; +import * as GreetingActions from '../../src/actions/GreetingActions'; + +describe('Greeting', () => { + let handleSelectionChangeSpy: jasmine.Spy; + beforeEach(() => { + handleSelectionChangeSpy = jasmine.createSpy('handleSelectionChange'); + }); + + it('given a targetOfGreeting of \'James\' it renders a p containing a greeting and a remove button', () => { + const targetOfGreeting = 'James'; + + const p = render({ targetOfGreeting }); + expect(p.type).toBe('p'); + expect(p.props.children[0]).toBe('Hello '); + expect(p.props.children[1]).toBe('James'); + expect(p.props.children[2]).toBe('!'); + + const [ , , , button ] = p.props.children; + + expect(button.type).toBe('button'); + expect(button.props.className).toBe('btn btn-default btn-danger'); + expect(button.props.children).toBe('Remove'); + }); + + it('button onClick triggers an removeGreeting action', () => { + const targetOfGreeting = 'Benjamin'; + const p = render({ targetOfGreeting }); + const [ , , , button ] = p.props.children; + spyOn(GreetingActions, 'removeGreeting'); + + button.props.onClick(); + + expect(GreetingActions.removeGreeting).toHaveBeenCalledWith(targetOfGreeting); + }); + + function render({ targetOfGreeting }) { + const shallowRenderer = TestUtils.createRenderer(); + shallowRenderer.render(); + return shallowRenderer.getRenderOutput(); + } +}); diff --git a/react-with-type-safe-flux/test/components/WhoToGreet.tests.tsx b/react-with-type-safe-flux/test/components/WhoToGreet.tests.tsx new file mode 100644 index 0000000..e514ec3 --- /dev/null +++ b/react-with-type-safe-flux/test/components/WhoToGreet.tests.tsx @@ -0,0 +1,67 @@ +import * as React from 'react'; +import * as TestUtils from 'react-addons-test-utils'; +import WhoToGreet from '../../src/components/WhoToGreet'; +import * as GreetingActions from '../../src/actions/GreetingActions'; + +describe('WhoToGreet', () => { + let handleSelectionChangeSpy: jasmine.Spy; + beforeEach(() => { + handleSelectionChangeSpy = jasmine.createSpy('handleSelectionChange'); + }); + + it('given a newGreeting then it renders a form containing an input containing that text and an add button', () => { + const newGreeting = 'James'; + + const form = render({ newGreeting }); + expect(form.type).toBe('form'); + expect(form.props.role).toBe('form'); + + const formGroup = form.props.children; + expect(formGroup.type).toBe('div'); + expect(formGroup.props.className).toBe('form-group'); + + const [ input, button ] = formGroup.props.children; + + expect(input.type).toBe('input'); + expect(input.props.type).toBe('text'); + expect(input.props.className).toBe('form-control'); + expect(input.props.placeholder).toBe('Who would you like to greet?'); + expect(input.props.value).toBe(newGreeting); + + expect(button.type).toBe('button'); + expect(button.props.type).toBe('submit'); + expect(button.props.className).toBe('btn btn-default btn-primary'); + expect(button.props.disabled).toBe(false); + expect(button.props.children).toBe('Add greeting'); + }); + + it('input onChange triggers a newGreetingChanged action', () => { + const newGreeting = 'Benjamin'; + const form = render({ newGreeting }); + const formGroup = form.props.children; + const [ input ] = formGroup.props.children; + spyOn(GreetingActions, 'newGreetingChanged'); + + input.props.onChange({ target: { value: newGreeting }}); + + expect(GreetingActions.newGreetingChanged).toHaveBeenCalledWith(newGreeting); + }); + + it('button onClick triggers an addGreeting action', () => { + const newGreeting = 'Benjamin'; + const form = render({ newGreeting }); + const formGroup = form.props.children; + const [ , button ] = formGroup.props.children; + spyOn(GreetingActions, 'addGreeting'); + + button.props.onClick({ preventDefault: () => {} }); + + expect(GreetingActions.addGreeting).toHaveBeenCalledWith(newGreeting); + }); + + function render({ newGreeting }) { + const shallowRenderer = TestUtils.createRenderer(); + shallowRenderer.render(); + return shallowRenderer.getRenderOutput(); + } +}); diff --git a/react-with-type-safe-flux/test/import-babel-polyfill.js b/react-with-type-safe-flux/test/import-babel-polyfill.js new file mode 100644 index 0000000..b012711 --- /dev/null +++ b/react-with-type-safe-flux/test/import-babel-polyfill.js @@ -0,0 +1 @@ +import 'babel-polyfill'; diff --git a/react-with-type-safe-flux/test/stores/GreetingStore.tests.ts b/react-with-type-safe-flux/test/stores/GreetingStore.tests.ts new file mode 100644 index 0000000..cb536b9 --- /dev/null +++ b/react-with-type-safe-flux/test/stores/GreetingStore.tests.ts @@ -0,0 +1,56 @@ +import GreetingStore from '../../src/stores/GreetingStore'; +import GreetingActionTypes from '../../src/constants/action-types/GreetingActionTypes'; + +const registeredCallback = GreetingStore._onDispatch.bind(GreetingStore); + +describe('GreetingStore', () => { + beforeEach(() => { + GreetingStore._cleanState(); + }); + + it('given no actions, newGreeting should be an empty string and greetings should be an empty array', () => { + const { greetings, newGreeting } = GreetingStore.getState(); + + expect(greetings).toEqual([]); + expect(newGreeting).toBe(''); + }); + + it('given an ADD_GREETING action with a newGreeting of \'Benjamin\', the newGreeting should be an empty string and greetings should contain \'Benjamin\'', () => { + [{ + newGreeting: 'Benjamin', + type: GreetingActionTypes.ADD_GREETING, + }].forEach(registeredCallback); + + const { greetings, newGreeting } = GreetingStore.getState(); + + expect(greetings.find(g => g === 'Benjamin')).toBeTruthy(); + expect(newGreeting).toBe(''); + }); + + it('given an REMOVE_GREETING action with a greetingToRemove of \'Benjamin\', the state greetings should be an empty array', () => { + [{ + newGreeting: 'Benjamin', + type: GreetingActionTypes.ADD_GREETING, + }, { + greetingToRemove: 'Benjamin', + type: GreetingActionTypes.REMOVE_GREETING, + }].forEach(registeredCallback); + + const { greetings } = GreetingStore.getState(); + + expect(greetings.length).toBe(0); + expect(greetings.find(g => g === 'Benjamin')).toBeFalsy(); + }); + + it('given a NEW_GREETING_CHANGED action with a newGreeting of \'Benjamin\', the state newGreeting should be \'Benjamin\'', () => { + [{ + newGreeting: 'Benjamin', + type: GreetingActionTypes.NEW_GREETING_CHANGED, + }].forEach(registeredCallback); + + const { newGreeting } = GreetingStore.getState(); + + expect(newGreeting).toEqual('Benjamin'); + }); + +}); diff --git a/react-with-type-safe-flux/test/tsconfig.json b/react-with-type-safe-flux/test/tsconfig.json new file mode 100644 index 0000000..368d9d5 --- /dev/null +++ b/react-with-type-safe-flux/test/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compileOnSave": false, + "filesGlob": [ + "**/*.{ts,tsx}", + "../typings/**/*.*.ts" + ], + "compilerOptions": { + "jsx": "preserve", + "target": "es6", + "module": "commonjs", + "noImplicitAny": false, + "suppressImplicitAnyIndexErrors": true, + "removeComments": false, + "preserveConstEnums": true, + "sourceMap": true + }, + "files": [ + "components/App.tests.tsx", + "components/Greeting.tests.tsx", + "components/WhoToGreet.tests.tsx", + "stores/GreetingStore.tests.ts", + "../typings/flux/flux.d.ts", + "../typings/jasmine/jasmine.d.ts", + "../typings/node/node.d.ts", + "../typings/react/react-addons-test-utils.d.ts", + "../typings/react/react-dom.d.ts", + "../typings/react/react.d.ts", + "../typings/tsd.d.ts" + ], + "exclude": [], + "atom": { + "rewriteTsconfig": true + } +} diff --git a/react-with-type-safe-flux/tsd.json b/react-with-type-safe-flux/tsd.json new file mode 100644 index 0000000..2716a22 --- /dev/null +++ b/react-with-type-safe-flux/tsd.json @@ -0,0 +1,27 @@ +{ + "version": "v4", + "repo": "borisyankov/DefinitelyTyped", + "ref": "master", + "path": "typings", + "bundle": "typings/tsd.d.ts", + "installed": { + "jasmine/jasmine.d.ts": { + "commit": "bcd5761826eb567876c197ccc6a87c4d05731054" + }, + "flux/flux.d.ts": { + "commit": "bcd5761826eb567876c197ccc6a87c4d05731054" + }, + "node/node.d.ts": { + "commit": "bcd5761826eb567876c197ccc6a87c4d05731054" + }, + "react/react.d.ts": { + "commit": "bcd5761826eb567876c197ccc6a87c4d05731054" + }, + "react/react-dom.d.ts": { + "commit": "bcd5761826eb567876c197ccc6a87c4d05731054" + }, + "react/react-addons-test-utils.d.ts": { + "commit": "bcd5761826eb567876c197ccc6a87c4d05731054" + } + } +} diff --git a/react-with-type-safe-flux/webpack.config.js b/react-with-type-safe-flux/webpack.config.js new file mode 100644 index 0000000..70d955b --- /dev/null +++ b/react-with-type-safe-flux/webpack.config.js @@ -0,0 +1,42 @@ +/* eslint-disable no-var, strict, prefer-arrow-callback */ +'use strict'; + +var path = require('path'); + +module.exports = { + cache: true, + entry: { + main: './src/main.tsx', + vendor: [ + 'babel-polyfill', + 'events', + 'flux', + 'react' + ] + }, + output: { + path: path.resolve(__dirname, './dist/scripts'), + filename: '[name].js', + chunkFilename: '[chunkhash].js' + }, + module: { + loaders: [{ + test: /\.ts(x?)$/, + exclude: /node_modules/, + loader: 'babel-loader?presets[]=es2015&presets[]=react!ts-loader' + }, { + test: /\.js$/, + exclude: /node_modules/, + loader: 'babel', + query: { + presets: ['es2015', 'react'] + } + }] + }, + plugins: [ + ], + resolve: { + // Add `.ts` and `.tsx` as a resolvable extension. + extensions: ['', '.webpack.js', '.web.js', '.ts', '.tsx', '.js'] + }, +}; From ceb7a127dc1fa0c0f5b69b492663f9a37537f23a Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Sun, 28 Feb 2016 18:52:52 -0800 Subject: [PATCH 2/8] do not require global tsd --- react-with-type-safe-flux/README.md | 4 ++-- react-with-type-safe-flux/package.json | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/react-with-type-safe-flux/README.md b/react-with-type-safe-flux/README.md index 8c18bca..351f466 100644 --- a/react-with-type-safe-flux/README.md +++ b/react-with-type-safe-flux/README.md @@ -2,11 +2,11 @@ ## Getting started -You'll need [node / npm](https://nodejs.org/) and [tsd](http://definitelytyped.org/tsd/) installed globally. To get up and running just enter: +You'll need [node / npm](https://nodejs.org/) installed. To get up and running just enter: ``` npm install -tsd install +npm run tsd install npm run serve ``` diff --git a/react-with-type-safe-flux/package.json b/react-with-type-safe-flux/package.json index 04d8f90..74ed7af 100644 --- a/react-with-type-safe-flux/package.json +++ b/react-with-type-safe-flux/package.json @@ -4,11 +4,15 @@ "description": "ES6 + TypeScript + Babel + React + Karma: The Secret Recipe", "main": "index.js", "scripts": { + "tsd": "tsd", "test": "karma start --reporters mocha,junit --single-run --browsers PhantomJS", "serve": "gulp watch-and-serve", "watch": "gulp watch", "build": "gulp build" }, + "bin": { + "tsd": "./node_modules/tsd/build/cli.js" + }, "repository": { "type": "git", "url": "git+https://github.com/microsoft/typescriptsamples.git" @@ -65,6 +69,7 @@ "react-addons-test-utils": "^0.14.3", "react-dom": "^0.14.3", "ts-loader": "^0.8.1", + "tsd": "^0.6.5", "typescript": "^1.8.0", "webpack": "^1.12.2", "webpack-notifier": "^1.2.1" From 7e4eac4546017d4760df2e4086db012ab3dbec54 Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Sun, 28 Feb 2016 21:52:22 -0800 Subject: [PATCH 3/8] add type definitions so that it will compile with noImplicitAny: true --- react-with-type-safe-flux/src/components/App.tsx | 4 ++-- .../src/components/Greeting.tsx | 4 ++-- .../src/components/WhoToGreet.tsx | 8 ++++---- .../src/dispatcher/AppDispatcher.ts | 2 +- .../src/stores/FluxStore.ts | 16 ++++++++-------- .../src/stores/GreetingStore.ts | 6 +++--- react-with-type-safe-flux/src/tsconfig.json | 2 +- 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/react-with-type-safe-flux/src/components/App.tsx b/react-with-type-safe-flux/src/components/App.tsx index f6bceb6..7146ac7 100644 --- a/react-with-type-safe-flux/src/components/App.tsx +++ b/react-with-type-safe-flux/src/components/App.tsx @@ -5,8 +5,8 @@ import GreetingState from '../types/GreetingState'; import WhoToGreet from './WhoToGreet'; import Greeting from './Greeting'; -class App extends React.Component { - constructor(props) { +class App extends React.Component<{}, GreetingState> { + constructor(props: {}) { super(props); this.state = this._getStateFromStores(); } diff --git a/react-with-type-safe-flux/src/components/Greeting.tsx b/react-with-type-safe-flux/src/components/Greeting.tsx index 213af66..7d1502b 100644 --- a/react-with-type-safe-flux/src/components/Greeting.tsx +++ b/react-with-type-safe-flux/src/components/Greeting.tsx @@ -7,7 +7,7 @@ interface Props { } class Greeting extends React.Component { - constructor(props) { + constructor(props: Props) { super(props); } @@ -28,7 +28,7 @@ class Greeting extends React.Component { ); } - _onClick = (event) => { + _onClick = (event: React.MouseEvent) => { GreetingActions.removeGreeting(this.props.targetOfGreeting); } } diff --git a/react-with-type-safe-flux/src/components/WhoToGreet.tsx b/react-with-type-safe-flux/src/components/WhoToGreet.tsx index 5e3794f..6712746 100644 --- a/react-with-type-safe-flux/src/components/WhoToGreet.tsx +++ b/react-with-type-safe-flux/src/components/WhoToGreet.tsx @@ -6,7 +6,7 @@ interface Props { } class WhoToGreet extends React.Component { - constructor(props) { + constructor(props: Props) { super(props); } @@ -35,12 +35,12 @@ class WhoToGreet extends React.Component { return !this.props.newGreeting; } - _handleNewGreetingChange = (event) => { - const { target: { value: newGreeting } } = event; + _handleNewGreetingChange = (event: React.FormEvent) => { + const newGreeting = (event.target as HTMLInputElement).value; GreetingActions.newGreetingChanged(newGreeting); } - _onSubmit = (event) => { + _onSubmit = (event: React.FormEvent) => { event.preventDefault(); if (!this._preventSubmission) { diff --git a/react-with-type-safe-flux/src/dispatcher/AppDispatcher.ts b/react-with-type-safe-flux/src/dispatcher/AppDispatcher.ts index 428b8a6..54daded 100644 --- a/react-with-type-safe-flux/src/dispatcher/AppDispatcher.ts +++ b/react-with-type-safe-flux/src/dispatcher/AppDispatcher.ts @@ -1,5 +1,5 @@ import { Dispatcher } from 'flux'; -const dispatcherInstance = new Dispatcher(); +const dispatcherInstance: Flux.Dispatcher = new Dispatcher(); export default dispatcherInstance; diff --git a/react-with-type-safe-flux/src/stores/FluxStore.ts b/react-with-type-safe-flux/src/stores/FluxStore.ts index 552aaf7..2326a45 100644 --- a/react-with-type-safe-flux/src/stores/FluxStore.ts +++ b/react-with-type-safe-flux/src/stores/FluxStore.ts @@ -2,19 +2,19 @@ import { EventEmitter } from 'events'; const CHANGE_EVENT = 'change'; -class FluxStore { +class FluxStore { _changed: boolean; _emitter: EventEmitter; dispatchToken: string; - _dispatcher: Flux.Dispatcher; + _dispatcher: Flux.Dispatcher; _cleanStateFn: () => TState; _state: TState; - constructor(dispatcher, cleanStateFn) { + constructor(dispatcher: Flux.Dispatcher, cleanStateFn: () => TState) { this._emitter = new EventEmitter(); this._changed = false; this._dispatcher = dispatcher; - this.dispatchToken = dispatcher.register(payload => { + this.dispatchToken = dispatcher.register((payload: PayloadType) => { this._invokeOnDispatch(payload); }); @@ -31,11 +31,11 @@ class FluxStore { hasChanged() { return this._changed; } - addChangeListener(callback) { + addChangeListener(callback: () => void) { this._emitter.on(CHANGE_EVENT, callback); } - removeChangeListener(callback) { + removeChangeListener(callback: () => void) { this._emitter.removeListener(CHANGE_EVENT, callback); } @@ -44,7 +44,7 @@ class FluxStore { this._state = this._cleanStateFn(); } - _invokeOnDispatch(payload) { + _invokeOnDispatch(payload: PayloadType) { this._changed = false; this._onDispatch(payload); if (this._changed) { @@ -52,7 +52,7 @@ class FluxStore { } } - _onDispatch(payload) { + _onDispatch(payload: PayloadType) { if (process.env.NODE_ENV !== 'production') { console.error(`${this.constructor.name} has not overridden FluxStore.__onDispatch(), which is required`); // eslint-disable-line no-console } diff --git a/react-with-type-safe-flux/src/stores/GreetingStore.ts b/react-with-type-safe-flux/src/stores/GreetingStore.ts index c80dbfc..ba72503 100644 --- a/react-with-type-safe-flux/src/stores/GreetingStore.ts +++ b/react-with-type-safe-flux/src/stores/GreetingStore.ts @@ -3,8 +3,8 @@ import GreetingActionTypes from '../constants/action-types/GreetingActionTypes'; import AppDispatcher from '../dispatcher/AppDispatcher'; import GreetingState from '../types/GreetingState'; -class GreeterStore extends FluxStore { - constructor(dispatcher) { +class GreeterStore extends FluxStore { + constructor(dispatcher: Flux.Dispatcher) { super(dispatcher, () => ({ greetings: [], newGreeting: '' @@ -15,7 +15,7 @@ class GreeterStore extends FluxStore { return this._state } - _onDispatch(action) { + _onDispatch(action: any) { switch(action.type) { case GreetingActionTypes.ADD_GREETING: this._state.newGreeting = ''; diff --git a/react-with-type-safe-flux/src/tsconfig.json b/react-with-type-safe-flux/src/tsconfig.json index dbffad0..f116b8b 100644 --- a/react-with-type-safe-flux/src/tsconfig.json +++ b/react-with-type-safe-flux/src/tsconfig.json @@ -9,7 +9,7 @@ "compilerOptions": { "jsx": "preserve", "target": "es6", - "noImplicitAny": false, + "noImplicitAny": true, "removeComments": false, "preserveConstEnums": true, "sourceMap": true From 580b89e228dd29ea00e189bca974d9bf557283cb Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Sun, 28 Feb 2016 23:28:41 -0800 Subject: [PATCH 4/8] introduce Event type in dispatcher --- react-with-type-safe-flux/src/actions/GreetingActions.ts | 8 ++++---- react-with-type-safe-flux/src/dispatcher/AppDispatcher.ts | 6 ++++-- react-with-type-safe-flux/src/stores/FluxStore.ts | 7 +------ react-with-type-safe-flux/src/stores/GreetingStore.ts | 6 +++--- 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/react-with-type-safe-flux/src/actions/GreetingActions.ts b/react-with-type-safe-flux/src/actions/GreetingActions.ts index c451280..a69a64b 100644 --- a/react-with-type-safe-flux/src/actions/GreetingActions.ts +++ b/react-with-type-safe-flux/src/actions/GreetingActions.ts @@ -1,23 +1,23 @@ -import AppDispatcher from '../dispatcher/AppDispatcher'; +import {Event, AppDispatcher} from '../dispatcher/AppDispatcher'; import GreetingActionTypes from '../constants/action-types/GreetingActionTypes'; export function addGreeting(newGreeting: string) { AppDispatcher.dispatch({ newGreeting, type: GreetingActionTypes.ADD_GREETING - }); + } as Event); } export function newGreetingChanged(newGreeting: string) { AppDispatcher.dispatch({ newGreeting, type: GreetingActionTypes.NEW_GREETING_CHANGED - }); + } as Event); } export function removeGreeting(greetingToRemove: string) { AppDispatcher.dispatch({ greetingToRemove, type: GreetingActionTypes.REMOVE_GREETING - }); + } as Event); } diff --git a/react-with-type-safe-flux/src/dispatcher/AppDispatcher.ts b/react-with-type-safe-flux/src/dispatcher/AppDispatcher.ts index 54daded..daf21ba 100644 --- a/react-with-type-safe-flux/src/dispatcher/AppDispatcher.ts +++ b/react-with-type-safe-flux/src/dispatcher/AppDispatcher.ts @@ -1,5 +1,7 @@ import { Dispatcher } from 'flux'; -const dispatcherInstance: Flux.Dispatcher = new Dispatcher(); +export type Event = {type: string}; -export default dispatcherInstance; +const dispatcherInstance: Flux.Dispatcher = new Dispatcher(); + +export const AppDispatcher = dispatcherInstance; diff --git a/react-with-type-safe-flux/src/stores/FluxStore.ts b/react-with-type-safe-flux/src/stores/FluxStore.ts index 2326a45..18beb9a 100644 --- a/react-with-type-safe-flux/src/stores/FluxStore.ts +++ b/react-with-type-safe-flux/src/stores/FluxStore.ts @@ -9,6 +9,7 @@ class FluxStore { _dispatcher: Flux.Dispatcher; _cleanStateFn: () => TState; _state: TState; + protected _onDispatch: (payload: PayloadType) => void; constructor(dispatcher: Flux.Dispatcher, cleanStateFn: () => TState) { this._emitter = new EventEmitter(); @@ -51,12 +52,6 @@ class FluxStore { this._emitter.emit(CHANGE_EVENT); } } - - _onDispatch(payload: PayloadType) { - if (process.env.NODE_ENV !== 'production') { - console.error(`${this.constructor.name} has not overridden FluxStore.__onDispatch(), which is required`); // eslint-disable-line no-console - } - } } export default FluxStore; diff --git a/react-with-type-safe-flux/src/stores/GreetingStore.ts b/react-with-type-safe-flux/src/stores/GreetingStore.ts index ba72503..1e6f726 100644 --- a/react-with-type-safe-flux/src/stores/GreetingStore.ts +++ b/react-with-type-safe-flux/src/stores/GreetingStore.ts @@ -1,6 +1,6 @@ import FluxStore from './FluxStore'; import GreetingActionTypes from '../constants/action-types/GreetingActionTypes'; -import AppDispatcher from '../dispatcher/AppDispatcher'; +import {Event, AppDispatcher} from '../dispatcher/AppDispatcher'; import GreetingState from '../types/GreetingState'; class GreeterStore extends FluxStore { @@ -15,8 +15,8 @@ class GreeterStore extends FluxStore { return this._state } - _onDispatch(action: any) { - switch(action.type) { + _onDispatch = (action: any) => { + switch((action).type) { case GreetingActionTypes.ADD_GREETING: this._state.newGreeting = ''; this._state.greetings = this._state.greetings.concat(action.newGreeting); From 4d7b2c1c44074964fc92c3230e70940931117d33 Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Mon, 29 Feb 2016 00:01:05 -0800 Subject: [PATCH 5/8] use {type: string; payload: any} as type for actions --- .../src/actions/GreetingActions.ts | 12 +++--- .../src/dispatcher/AppDispatcher.ts | 2 +- .../src/stores/FluxStore.ts | 12 +++--- .../src/stores/GreetingStore.ts | 41 +++++++++---------- .../test/stores/GreetingStore.tests.ts | 8 ++-- 5 files changed, 37 insertions(+), 38 deletions(-) diff --git a/react-with-type-safe-flux/src/actions/GreetingActions.ts b/react-with-type-safe-flux/src/actions/GreetingActions.ts index a69a64b..f484588 100644 --- a/react-with-type-safe-flux/src/actions/GreetingActions.ts +++ b/react-with-type-safe-flux/src/actions/GreetingActions.ts @@ -3,21 +3,21 @@ import GreetingActionTypes from '../constants/action-types/GreetingActionTypes'; export function addGreeting(newGreeting: string) { AppDispatcher.dispatch({ - newGreeting, + payload: newGreeting, type: GreetingActionTypes.ADD_GREETING - } as Event); + }); } export function newGreetingChanged(newGreeting: string) { AppDispatcher.dispatch({ - newGreeting, + payload: newGreeting, type: GreetingActionTypes.NEW_GREETING_CHANGED - } as Event); + }); } export function removeGreeting(greetingToRemove: string) { AppDispatcher.dispatch({ - greetingToRemove, + payload: greetingToRemove, type: GreetingActionTypes.REMOVE_GREETING - } as Event); + }); } diff --git a/react-with-type-safe-flux/src/dispatcher/AppDispatcher.ts b/react-with-type-safe-flux/src/dispatcher/AppDispatcher.ts index daf21ba..ab5248f 100644 --- a/react-with-type-safe-flux/src/dispatcher/AppDispatcher.ts +++ b/react-with-type-safe-flux/src/dispatcher/AppDispatcher.ts @@ -1,6 +1,6 @@ import { Dispatcher } from 'flux'; -export type Event = {type: string}; +export type Event = {type: string; payload: any}; const dispatcherInstance: Flux.Dispatcher = new Dispatcher(); diff --git a/react-with-type-safe-flux/src/stores/FluxStore.ts b/react-with-type-safe-flux/src/stores/FluxStore.ts index 18beb9a..0aca6c4 100644 --- a/react-with-type-safe-flux/src/stores/FluxStore.ts +++ b/react-with-type-safe-flux/src/stores/FluxStore.ts @@ -1,21 +1,21 @@ import { EventEmitter } from 'events'; +import { Event } from '../dispatcher/AppDispatcher'; const CHANGE_EVENT = 'change'; -class FluxStore { +class FluxStore { _changed: boolean; _emitter: EventEmitter; dispatchToken: string; - _dispatcher: Flux.Dispatcher; + _dispatcher: Flux.Dispatcher; _cleanStateFn: () => TState; _state: TState; - protected _onDispatch: (payload: PayloadType) => void; - constructor(dispatcher: Flux.Dispatcher, cleanStateFn: () => TState) { + constructor(dispatcher: Flux.Dispatcher, protected _onDispatch: (action: Event) => void, cleanStateFn: () => TState) { this._emitter = new EventEmitter(); this._changed = false; this._dispatcher = dispatcher; - this.dispatchToken = dispatcher.register((payload: PayloadType) => { + this.dispatchToken = dispatcher.register((payload: Event) => { this._invokeOnDispatch(payload); }); @@ -45,7 +45,7 @@ class FluxStore { this._state = this._cleanStateFn(); } - _invokeOnDispatch(payload: PayloadType) { + _invokeOnDispatch(payload: Event) { this._changed = false; this._onDispatch(payload); if (this._changed) { diff --git a/react-with-type-safe-flux/src/stores/GreetingStore.ts b/react-with-type-safe-flux/src/stores/GreetingStore.ts index 1e6f726..66d6e78 100644 --- a/react-with-type-safe-flux/src/stores/GreetingStore.ts +++ b/react-with-type-safe-flux/src/stores/GreetingStore.ts @@ -3,9 +3,26 @@ import GreetingActionTypes from '../constants/action-types/GreetingActionTypes'; import {Event, AppDispatcher} from '../dispatcher/AppDispatcher'; import GreetingState from '../types/GreetingState'; -class GreeterStore extends FluxStore { - constructor(dispatcher: Flux.Dispatcher) { - super(dispatcher, () => ({ +class GreeterStore extends FluxStore { + constructor(dispatcher: Flux.Dispatcher) { + const onDispatch = (action: Event) => { + switch(action.type) { + case GreetingActionTypes.ADD_GREETING: + this._state.newGreeting = ''; + this._state.greetings = this._state.greetings.concat(action.payload); + this.emitChange(); + break; + case GreetingActionTypes.REMOVE_GREETING: + this._state.greetings = this._state.greetings.filter(g => g !== action.payload); + this.emitChange(); + break; + case GreetingActionTypes.NEW_GREETING_CHANGED: + this._state.newGreeting = action.payload; + this.emitChange(); + break; + } + } + super(dispatcher, onDispatch, () => ({ greetings: [], newGreeting: '' })); @@ -14,24 +31,6 @@ class GreeterStore extends FluxStore { getState() { return this._state } - - _onDispatch = (action: any) => { - switch((action).type) { - case GreetingActionTypes.ADD_GREETING: - this._state.newGreeting = ''; - this._state.greetings = this._state.greetings.concat(action.newGreeting); - this.emitChange(); - break; - case GreetingActionTypes.REMOVE_GREETING: - this._state.greetings = this._state.greetings.filter(g => g !== action.greetingToRemove); - this.emitChange(); - break; - case GreetingActionTypes.NEW_GREETING_CHANGED: - this._state.newGreeting = action.newGreeting; - this.emitChange(); - break; - } - } } const greeterStoreInstance = new GreeterStore(AppDispatcher); diff --git a/react-with-type-safe-flux/test/stores/GreetingStore.tests.ts b/react-with-type-safe-flux/test/stores/GreetingStore.tests.ts index cb536b9..34ef2ca 100644 --- a/react-with-type-safe-flux/test/stores/GreetingStore.tests.ts +++ b/react-with-type-safe-flux/test/stores/GreetingStore.tests.ts @@ -17,7 +17,7 @@ describe('GreetingStore', () => { it('given an ADD_GREETING action with a newGreeting of \'Benjamin\', the newGreeting should be an empty string and greetings should contain \'Benjamin\'', () => { [{ - newGreeting: 'Benjamin', + payload: 'Benjamin', type: GreetingActionTypes.ADD_GREETING, }].forEach(registeredCallback); @@ -29,10 +29,10 @@ describe('GreetingStore', () => { it('given an REMOVE_GREETING action with a greetingToRemove of \'Benjamin\', the state greetings should be an empty array', () => { [{ - newGreeting: 'Benjamin', + payload: 'Benjamin', type: GreetingActionTypes.ADD_GREETING, }, { - greetingToRemove: 'Benjamin', + payload: 'Benjamin', type: GreetingActionTypes.REMOVE_GREETING, }].forEach(registeredCallback); @@ -44,7 +44,7 @@ describe('GreetingStore', () => { it('given a NEW_GREETING_CHANGED action with a newGreeting of \'Benjamin\', the state newGreeting should be \'Benjamin\'', () => { [{ - newGreeting: 'Benjamin', + payload: 'Benjamin', type: GreetingActionTypes.NEW_GREETING_CHANGED, }].forEach(registeredCallback); From 4935670d1e70274bc8534945e0e514eb97009414 Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Mon, 29 Feb 2016 00:20:30 -0800 Subject: [PATCH 6/8] add payload types --- .../src/actions/GreetingActions.ts | 10 +++++++--- react-with-type-safe-flux/src/stores/GreetingStore.ts | 10 +++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/react-with-type-safe-flux/src/actions/GreetingActions.ts b/react-with-type-safe-flux/src/actions/GreetingActions.ts index f484588..8c6ca51 100644 --- a/react-with-type-safe-flux/src/actions/GreetingActions.ts +++ b/react-with-type-safe-flux/src/actions/GreetingActions.ts @@ -1,23 +1,27 @@ import {Event, AppDispatcher} from '../dispatcher/AppDispatcher'; import GreetingActionTypes from '../constants/action-types/GreetingActionTypes'; +export interface AddGreetingEvent {type: string; payload: string;} +export interface NewGreetingChanged {type: string; payload: string;} +export interface RemoveGreeting {type: string; payload: string;} + export function addGreeting(newGreeting: string) { AppDispatcher.dispatch({ payload: newGreeting, type: GreetingActionTypes.ADD_GREETING - }); + } as AddGreetingEvent); } export function newGreetingChanged(newGreeting: string) { AppDispatcher.dispatch({ payload: newGreeting, type: GreetingActionTypes.NEW_GREETING_CHANGED - }); + } as NewGreetingChanged); } export function removeGreeting(greetingToRemove: string) { AppDispatcher.dispatch({ payload: greetingToRemove, type: GreetingActionTypes.REMOVE_GREETING - }); + } as RemoveGreeting); } diff --git a/react-with-type-safe-flux/src/stores/GreetingStore.ts b/react-with-type-safe-flux/src/stores/GreetingStore.ts index 66d6e78..e7a8b40 100644 --- a/react-with-type-safe-flux/src/stores/GreetingStore.ts +++ b/react-with-type-safe-flux/src/stores/GreetingStore.ts @@ -2,22 +2,26 @@ import FluxStore from './FluxStore'; import GreetingActionTypes from '../constants/action-types/GreetingActionTypes'; import {Event, AppDispatcher} from '../dispatcher/AppDispatcher'; import GreetingState from '../types/GreetingState'; +import { AddGreetingEvent, RemoveGreeting, NewGreetingChanged } from '../actions/GreetingActions'; class GreeterStore extends FluxStore { constructor(dispatcher: Flux.Dispatcher) { const onDispatch = (action: Event) => { switch(action.type) { case GreetingActionTypes.ADD_GREETING: + let payload1 = ( action).payload; this._state.newGreeting = ''; - this._state.greetings = this._state.greetings.concat(action.payload); + this._state.greetings = this._state.greetings.concat(payload1); this.emitChange(); break; case GreetingActionTypes.REMOVE_GREETING: - this._state.greetings = this._state.greetings.filter(g => g !== action.payload); + let payload2 = ( action).payload; + this._state.greetings = this._state.greetings.filter(g => g !== payload2); this.emitChange(); break; case GreetingActionTypes.NEW_GREETING_CHANGED: - this._state.newGreeting = action.payload; + let payload3 = ( action).payload; + this._state.newGreeting = payload3; this.emitChange(); break; } From 5e27fc0eddba50c0105d5ebe218990ca7d67233f Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Mon, 29 Feb 2016 11:17:47 -0800 Subject: [PATCH 7/8] move to TypedEvent --- .../src/actions/GreetingActions.ts | 23 +++++--------- .../src/dispatcher/AppDispatcher.ts | 6 +++- .../src/stores/GreetingStore.ts | 30 ++++++++----------- .../test/stores/GreetingStore.tests.ts | 19 +++--------- 4 files changed, 29 insertions(+), 49 deletions(-) diff --git a/react-with-type-safe-flux/src/actions/GreetingActions.ts b/react-with-type-safe-flux/src/actions/GreetingActions.ts index 8c6ca51..a4cfc7e 100644 --- a/react-with-type-safe-flux/src/actions/GreetingActions.ts +++ b/react-with-type-safe-flux/src/actions/GreetingActions.ts @@ -1,27 +1,18 @@ -import {Event, AppDispatcher} from '../dispatcher/AppDispatcher'; +import {TypedEvent, AppDispatcher} from '../dispatcher/AppDispatcher'; import GreetingActionTypes from '../constants/action-types/GreetingActionTypes'; -export interface AddGreetingEvent {type: string; payload: string;} -export interface NewGreetingChanged {type: string; payload: string;} -export interface RemoveGreeting {type: string; payload: string;} +export class AddGreetingEvent extends TypedEvent {} +export class NewGreetingChanged extends TypedEvent {} +export class RemoveGreeting extends TypedEvent {} export function addGreeting(newGreeting: string) { - AppDispatcher.dispatch({ - payload: newGreeting, - type: GreetingActionTypes.ADD_GREETING - } as AddGreetingEvent); + AppDispatcher.dispatch(new AddGreetingEvent(newGreeting)); } export function newGreetingChanged(newGreeting: string) { - AppDispatcher.dispatch({ - payload: newGreeting, - type: GreetingActionTypes.NEW_GREETING_CHANGED - } as NewGreetingChanged); + AppDispatcher.dispatch(new NewGreetingChanged(newGreeting)); } export function removeGreeting(greetingToRemove: string) { - AppDispatcher.dispatch({ - payload: greetingToRemove, - type: GreetingActionTypes.REMOVE_GREETING - } as RemoveGreeting); + AppDispatcher.dispatch(new RemoveGreeting(greetingToRemove)); } diff --git a/react-with-type-safe-flux/src/dispatcher/AppDispatcher.ts b/react-with-type-safe-flux/src/dispatcher/AppDispatcher.ts index ab5248f..9dfa556 100644 --- a/react-with-type-safe-flux/src/dispatcher/AppDispatcher.ts +++ b/react-with-type-safe-flux/src/dispatcher/AppDispatcher.ts @@ -1,6 +1,10 @@ import { Dispatcher } from 'flux'; -export type Event = {type: string; payload: any}; +export class TypedEvent

{ + constructor(public payload: P) {} +} + +export type Event = TypedEvent; const dispatcherInstance: Flux.Dispatcher = new Dispatcher(); diff --git a/react-with-type-safe-flux/src/stores/GreetingStore.ts b/react-with-type-safe-flux/src/stores/GreetingStore.ts index e7a8b40..08840b9 100644 --- a/react-with-type-safe-flux/src/stores/GreetingStore.ts +++ b/react-with-type-safe-flux/src/stores/GreetingStore.ts @@ -7,23 +7,19 @@ import { AddGreetingEvent, RemoveGreeting, NewGreetingChanged } from '../actions class GreeterStore extends FluxStore { constructor(dispatcher: Flux.Dispatcher) { const onDispatch = (action: Event) => { - switch(action.type) { - case GreetingActionTypes.ADD_GREETING: - let payload1 = ( action).payload; - this._state.newGreeting = ''; - this._state.greetings = this._state.greetings.concat(payload1); - this.emitChange(); - break; - case GreetingActionTypes.REMOVE_GREETING: - let payload2 = ( action).payload; - this._state.greetings = this._state.greetings.filter(g => g !== payload2); - this.emitChange(); - break; - case GreetingActionTypes.NEW_GREETING_CHANGED: - let payload3 = ( action).payload; - this._state.newGreeting = payload3; - this.emitChange(); - break; + if (action instanceof AddGreetingEvent) { + const {payload} = action; + this._state.newGreeting = ''; + this._state.greetings = this._state.greetings.concat(payload); + this.emitChange(); + } else if (action instanceof RemoveGreeting) { + const {payload} = action; + this._state.greetings = this._state.greetings.filter(g => g !== payload); + this.emitChange(); + } else if (action instanceof NewGreetingChanged) { + const {payload} = action; + this._state.newGreeting = payload; + this.emitChange(); } } super(dispatcher, onDispatch, () => ({ diff --git a/react-with-type-safe-flux/test/stores/GreetingStore.tests.ts b/react-with-type-safe-flux/test/stores/GreetingStore.tests.ts index 34ef2ca..f617a0d 100644 --- a/react-with-type-safe-flux/test/stores/GreetingStore.tests.ts +++ b/react-with-type-safe-flux/test/stores/GreetingStore.tests.ts @@ -1,5 +1,6 @@ import GreetingStore from '../../src/stores/GreetingStore'; import GreetingActionTypes from '../../src/constants/action-types/GreetingActionTypes'; +import { AddGreetingEvent, RemoveGreeting, NewGreetingChanged } from '../../src/actions/GreetingActions'; const registeredCallback = GreetingStore._onDispatch.bind(GreetingStore); @@ -16,10 +17,7 @@ describe('GreetingStore', () => { }); it('given an ADD_GREETING action with a newGreeting of \'Benjamin\', the newGreeting should be an empty string and greetings should contain \'Benjamin\'', () => { - [{ - payload: 'Benjamin', - type: GreetingActionTypes.ADD_GREETING, - }].forEach(registeredCallback); + [new AddGreetingEvent('Benjamin')].forEach(registeredCallback); const { greetings, newGreeting } = GreetingStore.getState(); @@ -28,13 +26,7 @@ describe('GreetingStore', () => { }); it('given an REMOVE_GREETING action with a greetingToRemove of \'Benjamin\', the state greetings should be an empty array', () => { - [{ - payload: 'Benjamin', - type: GreetingActionTypes.ADD_GREETING, - }, { - payload: 'Benjamin', - type: GreetingActionTypes.REMOVE_GREETING, - }].forEach(registeredCallback); + [new AddGreetingEvent('Benjamin'), new RemoveGreeting('Benjamin')].forEach(registeredCallback); const { greetings } = GreetingStore.getState(); @@ -43,10 +35,7 @@ describe('GreetingStore', () => { }); it('given a NEW_GREETING_CHANGED action with a newGreeting of \'Benjamin\', the state newGreeting should be \'Benjamin\'', () => { - [{ - payload: 'Benjamin', - type: GreetingActionTypes.NEW_GREETING_CHANGED, - }].forEach(registeredCallback); + [new NewGreetingChanged('Benjamin')].forEach(registeredCallback); const { newGreeting } = GreetingStore.getState(); From 5ca2db4d5967c4aa9b51f6704c830d12e646b8d0 Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Fri, 18 Mar 2016 08:48:49 -0700 Subject: [PATCH 8/8] update name in README, add link to blog --- react-with-type-safe-flux/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/react-with-type-safe-flux/README.md b/react-with-type-safe-flux/README.md index 351f466..985bf13 100644 --- a/react-with-type-safe-flux/README.md +++ b/react-with-type-safe-flux/README.md @@ -1,4 +1,6 @@ -# ES6 + TypeScript + Babel + React + Karma: The Secret Recipe +# Type-Safe Flux Example + +Based on [ES6 + TypeScript + Babel + React + Karma: The Secret Recipe](../es6-babel-react-flux-karma), see more in corresponding [blog post](https://sameroom.io/blog/type-safe-flux-architecture-using-typescript/). ## Getting started