20 |
21 | const DocumentForm = ({
22 | data,
23 | errors,
24 | onUpdate,
25 | onSubmit,
26 | onCancel,
27 | }) =>
28 |
66 |
67 | DocumentForm.propTypes = {
68 | data: PropTypes.object.isRequired,
69 | errors: PropTypes.object,
70 | onUpdate: PropTypes.func.isRequired,
71 | onSubmit: PropTypes.func,
72 | onCancel: PropTypes.func.isRequired,
73 | }
74 |
75 | export default pacomoTransformer(DocumentForm)
76 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import webpack from "webpack";
3 | import ExtractTextPlugin from "extract-text-webpack-plugin";
4 |
5 |
6 | export default (DEBUG, PATH, PORT=3000) => ({
7 | entry: (DEBUG ? [
8 | `webpack-dev-server/client?http://localhost:${PORT}`,
9 | ] : []).concat([
10 | './src/main.less',
11 | 'babel-polyfill',
12 | './src/main',
13 | ]),
14 |
15 | output: {
16 | path: path.resolve(__dirname, PATH, "generated"),
17 | filename: DEBUG ? "main.js" : "main-[hash].js",
18 | publicPath: "/generated/"
19 | },
20 |
21 | cache: DEBUG,
22 | debug: DEBUG,
23 |
24 | // For options, see http://webpack.github.io/docs/configuration.html#devtool
25 | devtool: DEBUG && "eval",
26 |
27 | module: {
28 | loaders: [
29 | // Load ES6/JSX
30 | { test: /\.jsx?$/,
31 | include: [
32 | path.resolve(__dirname, "src"),
33 | ],
34 | loader: "babel-loader",
35 | query: {
36 | plugins: ['transform-runtime'],
37 | presets: ['es2015', 'stage-0', 'react'],
38 | }
39 | },
40 |
41 | // Load styles
42 | { test: /\.less$/,
43 | loader: DEBUG
44 | ? "style!css!autoprefixer!less"
45 | : ExtractTextPlugin.extract("style-loader", "css-loader!autoprefixer-loader!less-loader")
46 | },
47 |
48 | // Load images
49 | { test: /\.jpg/, loader: "url-loader?limit=10000&mimetype=image/jpg" },
50 | { test: /\.gif/, loader: "url-loader?limit=10000&mimetype=image/gif" },
51 | { test: /\.png/, loader: "url-loader?limit=10000&mimetype=image/png" },
52 | { test: /\.svg/, loader: "url-loader?limit=10000&mimetype=image/svg" },
53 |
54 | // Load fonts
55 | { test: /\.woff(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "url-loader?limit=10000&mimetype=application/font-woff" },
56 | { test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "file-loader" },
57 | ]
58 | },
59 |
60 | plugins: DEBUG
61 | ? []
62 | : [
63 | new webpack.DefinePlugin({'process.env.NODE_ENV': '"production"'}),
64 | new ExtractTextPlugin("style.css", {allChunks: false}),
65 | new webpack.optimize.DedupePlugin(),
66 | new webpack.optimize.UglifyJsPlugin({
67 | compressor: {screw_ie8: true, keep_fnames: true, warnings: false},
68 | mangle: {screw_ie8: true, keep_fnames: true}
69 | }),
70 | new webpack.optimize.OccurenceOrderPlugin(),
71 | new webpack.optimize.AggressiveMergingPlugin(),
72 | ],
73 |
74 | resolveLoader: {
75 | root: path.join(__dirname, "node_modules"),
76 | },
77 |
78 | resolve: {
79 | root: path.join(__dirname, "node_modules"),
80 |
81 | modulesDirectories: ['node_modules'],
82 |
83 | alias: {
84 | environment: DEBUG
85 | ? path.resolve(__dirname, 'config', 'environments', 'development.js')
86 | : path.resolve(__dirname, 'config', 'environments', 'production.js')
87 | },
88 |
89 | // Allow to omit extensions when requiring these files
90 | extensions: ["", ".js", ".jsx"],
91 | }
92 | });
93 |
--------------------------------------------------------------------------------
/gulpfile.babel.js:
--------------------------------------------------------------------------------
1 | import del from "del";
2 | import path from "path";
3 | import gulp from "gulp";
4 | import open from "open";
5 | import gulpLoadPlugins from "gulp-load-plugins";
6 | import packageJson from "./package.json";
7 | import runSequence from "run-sequence";
8 | import webpack from "webpack";
9 | import webpackConfig from "./webpack.config";
10 | import WebpackDevServer from "webpack-dev-server";
11 |
12 |
13 | const PORT = process.env.PORT || 3000;
14 | const $ = gulpLoadPlugins({camelize: true});
15 |
16 |
17 | // Main tasks
18 | gulp.task('serve', () => runSequence('serve:clean', 'serve:index', 'serve:start'));
19 | gulp.task('dist', () => runSequence('dist:clean', 'dist:build', 'dist:index'));
20 | gulp.task('clean', ['dist:clean', 'serve:clean']);
21 | gulp.task('open', () => open('http://localhost:3000'));
22 |
23 | // Remove all built files
24 | gulp.task('serve:clean', cb => del('build', {dot: true}, cb));
25 | gulp.task('dist:clean', cb => del(['dist', 'dist-intermediate'], {dot: true}, cb));
26 |
27 | // Copy static files across to our final directory
28 | gulp.task('serve:static', () =>
29 | gulp.src([
30 | 'src/static/**'
31 | ])
32 | .pipe($.changed('build'))
33 | .pipe(gulp.dest('build'))
34 | .pipe($.size({title: 'static'}))
35 | );
36 |
37 | gulp.task('dist:static', () =>
38 | gulp.src([
39 | 'src/static/**'
40 | ])
41 | .pipe(gulp.dest('dist'))
42 | .pipe($.size({title: 'static'}))
43 | );
44 |
45 | // Copy our index file and inject css/script imports for this build
46 | gulp.task('serve:index', () => {
47 | return gulp
48 | .src('src/index.html')
49 | .pipe($.injectString.after('', ''))
50 | .pipe(gulp.dest('build'));
51 | });
52 |
53 | // Copy our index file and inject css/script imports for this build
54 | gulp.task('dist:index', () => {
55 | const app = gulp
56 | .src(["*.{css,js}"], {cwd: 'dist-intermediate/generated'})
57 | .pipe(gulp.dest('dist'));
58 |
59 | // Build the index.html using the names of compiled files
60 | return gulp.src('src/index.html')
61 | .pipe($.inject(app, {
62 | ignorePath: 'dist',
63 | starttag: ''
64 | }))
65 | .on("error", $.util.log)
66 | .pipe(gulp.dest('dist'));
67 | });
68 |
69 | // Start a livereloading development server
70 | gulp.task('serve:start', ['serve:static'], () => {
71 | const config = webpackConfig(true, 'build', PORT);
72 |
73 | return new WebpackDevServer(webpack(config), {
74 | contentBase: 'build',
75 | publicPath: config.output.publicPath,
76 | watchDelay: 100
77 | })
78 | .listen(PORT, '0.0.0.0', (err) => {
79 | if (err) throw new $.util.PluginError('webpack-dev-server', err);
80 |
81 | $.util.log(`[${packageJson.name} serve]`, `Listening at 0.0.0.0:${PORT}`);
82 | });
83 | });
84 |
85 | // Create a distributable package
86 | gulp.task('dist:build', ['dist:static'], cb => {
87 | const config = webpackConfig(false, 'dist-intermediate');
88 |
89 | webpack(config, (err, stats) => {
90 | if (err) throw new $.util.PluginError('dist', err);
91 |
92 | $.util.log(`[${packageJson.name} dist]`, stats.toString({colors: true}));
93 |
94 | cb();
95 | });
96 | });
97 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Unicorn Standard Starter Kit
2 |
3 | This starter kit provides you with the code and conventions you need to get straight into building your React/Redux based app.
4 |
5 | ## Happiness is six lines away
6 |
7 | *Prerequisites: node.js and git*
8 |
9 | ```
10 | git clone https://github.com/unicorn-standard/starter-kit.git your-repo-name
11 | cd your-repo-name
12 | npm install
13 | npm start
14 | npm run open # (from a different console window, otherwise open localhost:3000)
15 | ```
16 |
17 | Presto, you've got a ready-to-customise application!
18 |
19 | 
20 |
21 | ## Why use Unicorn Standard Starter Kit?
22 |
23 | - Your directory structure is sorted as soon as you `git clone`
24 | - ES6 compilation and automatic-reloading development server are configured for you with [webpack](https://webpack.github.io/) and [Babel](https://babeljs.io/)
25 | - [Redux](http://redux.js.org/) is an incredibly simple way of modelling your data, with great community support
26 | - [React](https://www.reactjs.org/) is an incredibly simple way of rendering your views, and is maintained by Facebook
27 | - Simple [uniloc](http://unicornstandard.com/packages/uniloc.html)-based routing is included - easy to understand, and easy to customize
28 | - The [Pacomo](http://unicornstandard.com/packages/pacomo.html) CSS conventions eliminate bugs caused by conflicting styles
29 | - The actors pattern allows you to easily react to changes on your store *without* forcing a re-render
30 | - Your redux store is already configured with navigation, data and view models
31 | - Comes with views, layouts and reducers for a simple document editor!
32 |
33 | ## Getting Started
34 |
35 | #### Put your name on it
36 |
37 | - Update name, desription and author in `package.json`
38 | - Update app title in `src/index.html`
39 | - Restart the dev server (make sure to do this after any changes to `src/index.html`)
40 |
41 | #### Make sure your editor is happy
42 |
43 | - Setup ES6 syntax highlighting on extensions `.js` and `.jsx` (see [babel-sublime](https://github.com/babel/babel-sublime))
44 |
45 | #### Start building
46 |
47 | - Add a route to `src/constants/ROUTES.js`
48 | - Add a nav menu item for your route in `src/components/ApplicationLayout.jsx`
49 | - Add a component for your route in `src/components`
50 | - Add reducers and actions for your component's view model in `src/actions` and `src/reducers/view`
51 | - Add any data models which your component reqiures in `src/reducers/data`
52 | - Add a container to map your store's `state` and `dispatch` to component props in `src/containers`
53 | - Configure your route in `src/Application.jsx`
54 | - Bask in the glory of your creation
55 | - Don't forget to commit your changes and push to Bitbucket or GitHub!
56 |
57 | #### Show your friends
58 |
59 | - Run `gulp dist` to output a web-ready build of your app to `dist`
60 |
61 | ## Structure
62 |
63 | ### Entry point
64 |
65 | `main.js` is the entry point to your application. It defines your redux store, handles any actions dispatched to your redux store, handles changes to the browser's current URL, and also makes an initial route change dispatch.
66 |
67 | Most of the above will be obvious from a quick read through `main.js` - if there is one thing which may strike you as "interesting", it'll be the block which handles actors.
68 |
69 | ### Actors
70 |
71 | *[Read the introduction to actors](http://jamesknelson.com/join-the-dark-side-of-the-flux-responding-to-actions-with-actors/)*
72 |
73 | Each time your store's state changes, a sequence of functions are called on the *current state* of your store. These functions are called **actors**.
74 |
75 | There is one important exception to this rule: actors will not be called if `main.js` is currently in the midst of calling the sequence from a previous update. This allows earlier actors in a sequence to dispatch actions to the store, with later actors in the sequence receiving the *updated* state.
76 |
77 | The code which accomplishes this is very small:
78 |
79 | ```javascript
80 | let acting = false
81 | store.subscribe(function() {
82 | // Ensure that any action dispatched by actors do not result in a new
83 | // actor run, allowing actors to dispatch with impunity.
84 | if (!acting) {
85 | acting = true
86 |
87 | for (let actor of actors) {
88 | actor(store.getState(), store.dispatch.bind(store))
89 | }
90 |
91 | acting = false
92 | }
93 | })
94 | ```
95 |
96 | The actor is defined in `src/actors/index.js`. By default, it runs the following sequence:
97 |
98 | - **redirector** - dispatch a navigation action if the current location should redirect to another location
99 | - **renderer** - renders your component with React
100 |
101 | ### Model
102 |
103 | Your model (i.e. reducers and actions) is pre-configured with three parts:
104 |
105 | #### Navigation
106 |
107 | The `navigation` state holds the following information:
108 |
109 | - `location` is the object which your `ROUTES` constant's `lookup` function returns for the current URL. With the default uniloc-based `ROUTES` object, this will have a string `name` property, and an `options` object containing any route parameters.
110 | - `transitioning` is true if a navigation `start` action has been dispatched, but the browser hasn't changed URL yet
111 |
112 | #### Data
113 |
114 | The `data` state can be thought of as the database for your application. If your application reads data from a remote server, it should be stored here. Any metadata should also be stored here, including the time it was fetched or its current version number.
115 |
116 | #### View
117 |
118 | The `view` state has a property for each of the view's in your app, holding their current state. For example, form state should be stored in the view models.
119 |
120 | ### Directories
121 |
122 | - `src/actions` - Redux action creators
123 | - `src/actors` - Handle changes to your store's state
124 | - `src/components` - React components, stateless where possible
125 | - `src/constants` - Define stateless data
126 | - `src/containers` - Unstyled "smart" components which take your store's `state` and `dispatch`, and possibly navigation `location`, and pass them to "dumb" components
127 | - `src/reducers` - Redux reducers
128 | - `src/static` - Files which will be copied across to the root directory on build
129 | - `src/styles` - Helpers for stylesheets for individual components
130 | - `src/utils` - General code which isn't specific to your application
131 | - `src/validators` - Functions which take an object containing user entry and return an object containing any errors
132 |
133 | Other directories:
134 |
135 | - `build` - Intermediate files produced by the development server. Don't touch these.
136 | - `dist` - The output of `gulp dist`, which contains your distribution-ready app.
137 | - `config/environments` - The build system will assign one of these to the `environment` module, depending on the current build environment.
138 |
139 | Main application files:
140 |
141 | - `src/Application.jsx` - Your application's top-level React component
142 | - `src/index.html` - The single page for your single page application
143 | - `src/main.js` - The application's entry point
144 | - `src/main.less` - Global styles for your application
145 |
146 | Main build files:
147 |
148 | - `gulpfile.babel.js` - Build scripts written with [gulp](http://gulpjs.com/)
149 | - `webpack.config.js` - [Webpack](http://webpack.github.io/) configuration
150 |
151 | ## TODO
152 |
153 | - Watch `static` and `index.html` for changes and copy them across to `build` when appropriate
154 |
--------------------------------------------------------------------------------