├── .gitignore
├── app
├── scss
│ ├── app.scss
│ ├── _variables.scss
│ ├── _base.scss
│ ├── _toolkit.scss
│ ├── _mixins.scss
│ └── _functions.scss
├── favicon.ico
├── dispatcher
│ └── AppDispatcher.js
├── components
│ ├── App
│ │ ├── _App.scss
│ │ └── App.jsx
│ ├── Menu
│ │ ├── _Menu.scss
│ │ ├── Menu.jsx
│ │ ├── MenuItem.jsx
│ │ └── __tests__
│ │ │ └── Menu-test.jsx
│ ├── Footer
│ │ ├── _Footer.scss
│ │ ├── Footer.jsx
│ │ └── __tests__
│ │ │ └── Footer-test.jsx
│ └── Body
│ │ ├── _Body.scss
│ │ └── Body.jsx
├── app.tests.js
├── constants
│ └── AppConstants.js
├── app.jsx
├── util
│ └── WebAPI.js
├── index.html
├── actions
│ └── AppActions.js
└── stores
│ ├── BaseStore.js
│ ├── ItemsStore.js
│ └── __tests__
│ └── BaseStore-test.js
├── .travis.yml
├── webpack
├── config.test.js
├── plugins.js
├── config.js
└── loaders.js
├── .editorconfig
├── karma.conf.js
├── dev-server.js
├── LICENSE
├── package.json
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | /build
3 | *.log*
4 | .DS_Store
5 |
--------------------------------------------------------------------------------
/app/scss/app.scss:
--------------------------------------------------------------------------------
1 | /* app.scss */
2 |
3 | @import "base";
4 |
--------------------------------------------------------------------------------
/app/scss/_variables.scss:
--------------------------------------------------------------------------------
1 | /* _variables.scss */
2 |
3 | $border-color: #eee;
4 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/badsyntax/react-seed/HEAD/app/favicon.ico
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "0.12"
4 | script:
5 | - npm run test-travis
6 |
--------------------------------------------------------------------------------
/app/dispatcher/AppDispatcher.js:
--------------------------------------------------------------------------------
1 | import Flux from 'flux';
2 |
3 | export default new Flux.Dispatcher();
4 |
--------------------------------------------------------------------------------
/app/scss/_base.scss:
--------------------------------------------------------------------------------
1 | /* _base.scss */
2 |
3 | html, body {
4 | margin: 0;
5 | min-height: 100%;
6 | }
7 |
--------------------------------------------------------------------------------
/app/scss/_toolkit.scss:
--------------------------------------------------------------------------------
1 | /* _toolkit.scss */
2 |
3 | @import "functions";
4 | @import "mixins";
5 | @import "variables";
6 |
--------------------------------------------------------------------------------
/app/scss/_mixins.scss:
--------------------------------------------------------------------------------
1 | /* _mixins.scss */
2 |
3 | @mixin my-padding-mixin($some-number) {
4 | padding: $some-number;
5 | }
6 |
--------------------------------------------------------------------------------
/app/components/App/_App.scss:
--------------------------------------------------------------------------------
1 | @import "toolkit";
2 |
3 | .app {
4 | font-size: inherit;
5 | @include my-padding-mixin(20px);
6 | }
7 |
--------------------------------------------------------------------------------
/app/app.tests.js:
--------------------------------------------------------------------------------
1 | import 'babel-core/polyfill';
2 |
3 | let context = require.context('.', true, /-test\.jsx?$/);
4 | context.keys().forEach(context);
5 |
--------------------------------------------------------------------------------
/app/components/Menu/_Menu.scss:
--------------------------------------------------------------------------------
1 | .menu {
2 | background: #eee;
3 | border-radius: 8px;
4 | padding: 10px 10px 10px 40px;
5 | width: 200px;
6 | }
7 |
--------------------------------------------------------------------------------
/webpack/config.test.js:
--------------------------------------------------------------------------------
1 | var config = require('./config');
2 |
3 | delete config.context;
4 | delete config.entry;
5 | delete config.output;
6 | delete config.devServer;
7 |
8 | module.exports = config;
9 |
--------------------------------------------------------------------------------
/app/components/Footer/_Footer.scss:
--------------------------------------------------------------------------------
1 | @import "toolkit";
2 |
3 | .footer {
4 | border-top: 1px solid #eee;
5 | display: flex; // testing Autoprefixer
6 | font-size: inherit;
7 | margin-top: 20px;
8 | padding-top: 20px;
9 | }
10 |
--------------------------------------------------------------------------------
/app/components/Body/_Body.scss:
--------------------------------------------------------------------------------
1 | @import "toolkit";
2 |
3 | .body {
4 | font-size: inherit;
5 | padding: 20px;
6 | }
7 |
8 | .header {
9 | border-bottom: 1px solid $border-color;
10 | margin: 0 0 20px 0;
11 | padding-bottom: 20px;
12 | }
13 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.{js,json,jshintrc,html,jsx}]
12 | indent_style = space
13 | indent_size = 2
14 |
--------------------------------------------------------------------------------
/app/constants/AppConstants.js:
--------------------------------------------------------------------------------
1 | import pkg from '../../package';
2 |
3 | export const DEBUG = (process.env.NODE_ENV !== 'production');
4 | export const APP_TITLE = pkg.name;
5 | export const ITEMS_GET_SUCCESS = 'ITEMS_GET_SUCCESS';
6 | export const ITEMS_GET_ERROR = 'ITEMS_GET_ERROR';
7 | export const ITEMS_UPDATED = 'ITEMS_UPDATED';
8 |
--------------------------------------------------------------------------------
/app/app.jsx:
--------------------------------------------------------------------------------
1 | import './favicon.ico';
2 | import './index.html';
3 | import 'babel-core/polyfill';
4 | import 'normalize.css/normalize.css';
5 | import './scss/app.scss';
6 |
7 | import React from 'react';
8 | import App from './components/App/App';
9 |
10 | React.render(
11 | ,
12 | document.getElementById('app')
13 | );
14 |
--------------------------------------------------------------------------------
/app/util/WebAPI.js:
--------------------------------------------------------------------------------
1 | export default {
2 | getItems() {
3 | return new Promise((resolve) => {
4 | setTimeout(() => {
5 | resolve(['Item 1', 'Item 2', 'Item 3'].map((item, i) => {
6 | return {
7 | id: i,
8 | label: item
9 | };
10 | }));
11 | }, 500);
12 | });
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/app/components/Footer/Footer.jsx:
--------------------------------------------------------------------------------
1 | import styles from './_Footer.scss';
2 | import React from 'react';
3 |
4 | export default class Footer extends React.Component {
5 | render() {
6 | var year = (new Date()).getFullYear();
7 | return (
8 |
9 | © Your Company {year}
10 |
11 | );
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/app/scss/_functions.scss:
--------------------------------------------------------------------------------
1 | /* _functions.scss */
2 |
3 | /** NOTE: this function is only useful when importing Sass files with Sass instead
4 | of requiring the Sass files with the webpack sass-loader. */
5 | $imported-once-files: ();
6 | @function import-once($filename) {
7 | @if index($imported-once-files, $filename) {
8 | @return false;
9 | }
10 | $imported-once-files: append($imported-once-files, $filename);
11 | @return true;
12 | }
13 |
--------------------------------------------------------------------------------
/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | <%= title %>
7 | <% if (debug === false) { %>
8 |
9 | <% } %>
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/components/Footer/__tests__/Footer-test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react/addons';
2 | import Footer from '../Footer.jsx';
3 | import { expect } from 'chai';
4 |
5 | let { TestUtils } = React.addons;
6 |
7 | describe('Footer', () => {
8 | it('Should have the correct footer element', () => {
9 | let footer = TestUtils.renderIntoDocument(
10 |
11 | );
12 | let footerElem = React.findDOMNode(footer);
13 | expect(footerElem.tagName.toLowerCase()).to.equal('footer');
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/app/actions/AppActions.js:
--------------------------------------------------------------------------------
1 | import AppDispatcher from '../dispatcher/AppDispatcher';
2 | import WebAPI from '../util/WebAPI';
3 |
4 | import {
5 | ITEMS_GET_SUCCESS,
6 | ITEMS_GET_ERROR
7 | } from '../constants/AppConstants';
8 |
9 | export default {
10 | getItems() {
11 | WebAPI.getItems()
12 | .then((items) => {
13 | AppDispatcher.dispatch({
14 | actionType: ITEMS_GET_SUCCESS,
15 | items: items
16 | });
17 | })
18 | .catch(() => {
19 | AppDispatcher.dispatch({
20 | actionType: ITEMS_GET_ERROR
21 | });
22 | });
23 | }
24 | };
25 |
--------------------------------------------------------------------------------
/app/components/Menu/Menu.jsx:
--------------------------------------------------------------------------------
1 | import styles from './_Menu.scss';
2 | import React from 'react';
3 | import MenuItem from './MenuItem';
4 |
5 | let { Component, PropTypes } = React;
6 |
7 | export default class Menu extends Component {
8 |
9 | static defaultProps = {
10 | items: []
11 | };
12 |
13 | static propTypes = {
14 | items: PropTypes.array.isRequired
15 | };
16 |
17 | render() {
18 | return (
19 |
20 | {this.props.items.map((item) => {
21 | return ( );
22 | }, this)}
23 |
24 | );
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/components/Menu/MenuItem.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | let { Component, PropTypes } = React;
4 |
5 | export default class MenuItem extends Component {
6 |
7 | static propTypes = {
8 | item: PropTypes.object.isRequired
9 | };
10 |
11 | onItemClick = (e) => {
12 | e.preventDefault();
13 | window.alert('You clicked ' + this.props.item.label);
14 | }
15 |
16 | render() {
17 | return (
18 |
19 |
20 | {this.props.item.label}
21 |
22 |
23 | );
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/stores/BaseStore.js:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from 'events';
2 |
3 | export default class BaseStore extends EventEmitter {
4 |
5 | constructor(...args) {
6 | super(...args);
7 | this.data = new Set([]);
8 | }
9 |
10 | setAll(items) {
11 | this.data = new Set(items);
12 | this.emitChange();
13 | }
14 |
15 | getAll() {
16 | return Array.from(this.data);
17 | }
18 |
19 | set(item) {
20 | if (!this.data.has(item)) {
21 | this.data.add(item);
22 | this.emitChange();
23 | }
24 | }
25 |
26 | remove(item) {
27 | this.data.delete(item);
28 | this.emitChange();
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | module.exports = function(config) {
2 | config.set({
3 | basePath: '',
4 | frameworks: ['source-map-support', 'mocha', 'sinon'],
5 | files: [
6 | 'app/app.tests.js'
7 | ],
8 | exclude: [],
9 | preprocessors: {
10 | 'app/app.tests.js': ['webpack', 'sourcemap'],
11 | },
12 | reporters: ['mocha'],
13 | port: 9876,
14 | colors: true,
15 | logLevel: config.LOG_INFO,
16 | autoWatch: true,
17 | browsers: [/*'Chrome', */'PhantomJS'],
18 | singleRun: false,
19 | webpack: require('./webpack/config.test'),
20 | webpackMiddleware: {
21 | noInfo: true
22 | }
23 | });
24 | };
25 |
--------------------------------------------------------------------------------
/dev-server.js:
--------------------------------------------------------------------------------
1 | var util = require('util');
2 | var webpack = require('webpack');
3 | var WebpackDevServer = require('webpack-dev-server');
4 | var opn = require('opn');
5 | var pkg = require('./package.json');
6 |
7 | var port = pkg.config.devPort;
8 | var host = pkg.config.devHost;
9 |
10 | var configPath = process.argv[2] || './webpack/config';
11 | var config = require(configPath);
12 |
13 | var server = new WebpackDevServer(
14 | webpack(config),
15 | config.devServer
16 | );
17 |
18 | server.listen(port, host, function (err) {
19 | if (err) { console.log(err); }
20 | var url = util.format('http://%s:%d', host, port);
21 | console.log('Listening at %s', url);
22 | opn(url);
23 | });
24 |
--------------------------------------------------------------------------------
/app/stores/ItemsStore.js:
--------------------------------------------------------------------------------
1 | import BaseStore from './BaseStore';
2 | import AppDispatcher from '../dispatcher/AppDispatcher';
3 |
4 | import {
5 | ITEMS_UPDATED,
6 | ITEMS_GET_SUCCESS
7 | } from '../constants/AppConstants';
8 |
9 | class ItemsStore extends BaseStore {
10 |
11 | emitChange() {
12 | this.emit(ITEMS_UPDATED);
13 | }
14 |
15 | addChangeListener(callback) {
16 | this.on(ITEMS_UPDATED, callback);
17 | }
18 |
19 | removeChangeListener(callback) {
20 | this.removeListener(ITEMS_UPDATED, callback);
21 | }
22 | }
23 |
24 | let store = new ItemsStore();
25 |
26 | AppDispatcher.register((action) => {
27 | switch(action.actionType) {
28 | case ITEMS_GET_SUCCESS:
29 | store.setAll(action.items);
30 | break;
31 | default:
32 | }
33 | });
34 |
35 | export default store;
36 |
--------------------------------------------------------------------------------
/app/components/App/App.jsx:
--------------------------------------------------------------------------------
1 | import styles from './_App.scss';
2 |
3 | import React from 'react';
4 | import AppActions from '../../actions/AppActions';
5 | import ItemsStore from '../../stores/ItemsStore';
6 | import Body from '../Body/Body';
7 | import Footer from '../Footer/Footer';
8 |
9 | function getAppState() {
10 | return {
11 | items: ItemsStore.getAll()
12 | };
13 | }
14 |
15 | export default class App extends React.Component {
16 |
17 | state = getAppState()
18 |
19 | componentDidMount() {
20 | ItemsStore.addChangeListener(this.onChange);
21 | AppActions.getItems();
22 | }
23 |
24 | componentWillUnmount() {
25 | ItemsStore.removeChangeListener(this.onChange);
26 | }
27 |
28 | onChange = () => {
29 | this.setState(getAppState());
30 | }
31 |
32 | render() {
33 | return (
34 |
35 |
36 |
37 |
38 | );
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/webpack/plugins.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var util = require('util');
3 | var ExtractTextPlugin = require('extract-text-webpack-plugin');
4 | var webpack = require('webpack');
5 | var pkg = require('../package.json');
6 |
7 | var DEBUG = process.env.NODE_ENV === 'development';
8 | var TEST = process.env.NODE_ENV === 'test';
9 |
10 | var cssBundle = path.join('css', util.format('[name].%s.css', pkg.version));
11 |
12 | var plugins = [
13 | new webpack.optimize.OccurenceOrderPlugin()
14 | ];
15 | if (DEBUG) {
16 | plugins.push(
17 | new webpack.HotModuleReplacementPlugin()
18 | );
19 | } else if (!TEST) {
20 | plugins.push(
21 | new ExtractTextPlugin(cssBundle, {
22 | allChunks: true
23 | }),
24 | new webpack.optimize.UglifyJsPlugin(),
25 | new webpack.optimize.DedupePlugin(),
26 | new webpack.DefinePlugin({
27 | 'process.env': {
28 | NODE_ENV: JSON.stringify('production')
29 | }
30 | }),
31 | new webpack.NoErrorsPlugin()
32 | );
33 | }
34 |
35 | module.exports = plugins;
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Richard Willis
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 |
23 |
--------------------------------------------------------------------------------
/app/components/Body/Body.jsx:
--------------------------------------------------------------------------------
1 | import styles from './_Body.scss';
2 | import React from 'react';
3 | import Menu from '../Menu/Menu';
4 |
5 | let { PropTypes } = React;
6 |
7 | export default class Body extends React.Component {
8 |
9 | static defaultProps = {
10 | items: []
11 | };
12 |
13 | static propTypes = {
14 | items: PropTypes.array.isRequired
15 | };
16 |
17 | render() {
18 | return (
19 |
20 |
React Seed
21 |
This is an example seed app, powered by React, ES6 & webpack.
22 |
Here is some example data:
23 |
24 |
Getting started
25 |
Here's a couple of things you can do to get familiar with the project:
26 |
27 | Change some of the text the body component. You can find it here: ./app/components/Body/Body.jsx
28 | Style up the Body component. Give it a background color. (You shouldn't need to reload your browser to view the changes). Find the Sass file here: ./app/components/Body/_Body.scss
29 | Change the data rendered above. Look in: ./app/components/App/App.jsx Understand how data flows from the actions into the stores and then into the Body component.
30 |
31 |
32 | );
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/webpack/config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var util = require('util');
3 | var autoprefixer = require('autoprefixer-core');
4 | var pkg = require('../package.json');
5 |
6 | var loaders = require('./loaders');
7 | var plugins = require('./plugins');
8 |
9 | var DEBUG = process.env.NODE_ENV === 'development';
10 | var TEST = process.env.NODE_ENV === 'test';
11 |
12 | var jsBundle = path.join('js', util.format('[name].%s.js', pkg.version));
13 |
14 | var entry = {
15 | app: ['./app.jsx']
16 | };
17 |
18 | if (DEBUG) {
19 | entry.app.push(
20 | util.format(
21 | 'webpack-dev-server/client?http://%s:%d',
22 | pkg.config.devHost,
23 | pkg.config.devPort
24 | )
25 | );
26 | entry.app.push('webpack/hot/dev-server');
27 | }
28 |
29 | var config = {
30 | context: path.join(__dirname, '../app'),
31 | cache: DEBUG,
32 | debug: DEBUG,
33 | target: 'web',
34 | devtool: DEBUG || TEST ? 'inline-source-map' : false,
35 | entry: entry,
36 | output: {
37 | path: path.resolve(pkg.config.buildDir),
38 | publicPath: '/',
39 | filename: jsBundle,
40 | pathinfo: false
41 | },
42 | module: {
43 | loaders: loaders
44 | },
45 | postcss: [
46 | autoprefixer
47 | ],
48 | plugins: plugins,
49 | resolve: {
50 | extensions: ['', '.js', '.json', '.jsx']
51 | },
52 | devServer: {
53 | contentBase: path.resolve(pkg.config.buildDir),
54 | hot: true,
55 | noInfo: false,
56 | inline: true,
57 | stats: { colors: true }
58 | }
59 | };
60 |
61 | module.exports = config;
62 |
--------------------------------------------------------------------------------
/app/stores/__tests__/BaseStore-test.js:
--------------------------------------------------------------------------------
1 | import 'babel-core/polyfill';
2 | import BaseStore from '../BaseStore.js';
3 | import { expect } from 'chai';
4 |
5 | const ITEMS_UPDATED = 'ITEMS_UPDATED';
6 |
7 | class TestStore extends BaseStore {
8 | emitChange() {
9 | this.emit(ITEMS_UPDATED);
10 | }
11 | addChangeListener(callback) {
12 | this.on(ITEMS_UPDATED, callback);
13 | }
14 | removeChangeListener(callback) {
15 | this.removeListener(ITEMS_UPDATED, callback);
16 | }
17 | }
18 |
19 | describe('BaseStore', () => {
20 |
21 | it('Should set, get and remove data', function() {
22 |
23 | let store = new TestStore();
24 |
25 | expect(store.getAll()).to.eql([]);
26 |
27 | let item = {
28 | foo: 'bar'
29 | };
30 |
31 | store.setAll([item]);
32 | expect(store.getAll()).to.eql([item]);
33 |
34 | let item2 = {
35 | foobaz: 'bar'
36 | };
37 |
38 | store.set(item2);
39 | store.set(item2); // intentional check for unique items
40 | expect(store.getAll()).to.eql([item, item2]);
41 |
42 | store.remove(item);
43 | expect(store.getAll()).to.eql([item2]);
44 | });
45 |
46 | it('Should call the change listener when data changes', () => {
47 |
48 | let store = new TestStore();
49 | let onChange = sinon.spy();
50 | store.addChangeListener(onChange);
51 |
52 | store.setAll([{
53 | foo: 'bar'
54 | }]);
55 | store.set([{
56 | foobaz: 'bar'
57 | }]);
58 | store.remove({
59 | foo: 'bar'
60 | });
61 | expect(onChange.callCount).to.equal(3);
62 | });
63 |
64 | it('Should remove the change listener', () => {
65 |
66 | let store = new TestStore();
67 | let onChange = sinon.spy();
68 | store.addChangeListener(onChange);
69 | store.setAll([{
70 | foo: 'bar'
71 | }]);
72 | store.removeChangeListener(onChange);
73 | store.setAll([{
74 | foo: 'bar'
75 | }]);
76 | expect(onChange.callCount).to.equal(1);
77 | });
78 | });
79 |
--------------------------------------------------------------------------------
/app/components/Menu/__tests__/Menu-test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react/addons';
2 | import { expect } from 'chai';
3 |
4 | import Menu from '../Menu.jsx';
5 | import MenuItem from '../MenuItem.jsx';
6 |
7 | describe('Menu', () => {
8 |
9 | let { TestUtils } = React.addons;
10 |
11 | let menuItems = [
12 | { id: 1, label: 'Option 1' },
13 | { id: 2, label: 'Option 2' }
14 | ];
15 |
16 | describe('Rendering', () => {
17 |
18 | // Here we create a mocked MenuItem component.
19 | class MockedMenuItem extends MenuItem {
20 | render() {
21 | return (
22 | {this.props.item.label}
23 | );
24 | }
25 | }
26 |
27 | // Here we set the mocked MenuItem component.
28 | Menu.__Rewire__('MenuItem', MockedMenuItem);
29 |
30 | let menu = TestUtils.renderIntoDocument(
31 |
32 | );
33 | let menuElem = React.findDOMNode(menu);
34 | let items = menuElem.querySelectorAll('li');
35 |
36 | it('Should render the menu items', () => {
37 | expect(items.length).to.equal(2);
38 | });
39 |
40 | it('Should render the menu item labels', () => {
41 | Array.prototype.forEach.call(items, (item, i) => {
42 | expect(item.textContent.trim()).to.equal(menuItems[i].label);
43 | });
44 | });
45 |
46 | it('Should render the mocked menu item', () => {
47 | expect(menuElem.querySelectorAll('li')[0].className).to.equal('mocked-menu-item');
48 | });
49 | });
50 |
51 | describe('Events', () => {
52 |
53 | // Example of simulating browser events.
54 | it('Should handle click events', () => {
55 |
56 | var clicked = 0;
57 |
58 | class MockedMenuItemWithClickHandler extends MenuItem {
59 | onItemClick = () => {
60 | clicked++;
61 | }
62 | }
63 |
64 | Menu.__Rewire__('MenuItem', MockedMenuItemWithClickHandler);
65 |
66 | let menu = TestUtils.renderIntoDocument(
67 |
68 | );
69 | let menuElem = React.findDOMNode(menu);
70 | let items = menuElem.querySelectorAll('li');
71 | let node = items[0].querySelector('a');
72 |
73 | TestUtils.Simulate.click(node);
74 | TestUtils.Simulate.click(node);
75 |
76 | expect(clicked).to.equal(2);
77 | });
78 | });
79 | });
80 |
--------------------------------------------------------------------------------
/webpack/loaders.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var pkg = require('../package.json');
3 | var ExtractTextPlugin = require('extract-text-webpack-plugin');
4 |
5 | var DEBUG = process.env.NODE_ENV === 'development';
6 | var TEST = process.env.NODE_ENV === 'test';
7 |
8 | var jsxLoader;
9 | var sassLoader;
10 | var cssLoader;
11 | var fileLoader = 'file-loader?name=[path][name].[ext]';
12 | var htmlLoader = [
13 | 'file-loader?name=[path][name].[ext]',
14 | 'template-html-loader?' + [
15 | 'raw=true',
16 | 'engine=lodash',
17 | 'version=' + pkg.version,
18 | 'title=' + pkg.name,
19 | 'debug=' + DEBUG
20 | ].join('&')
21 | ].join('!');
22 | var jsonLoader = ['json-loader'];
23 |
24 | var sassParams = [
25 | 'outputStyle=expanded',
26 | 'includePaths[]=' + path.resolve(__dirname, '../app/scss'),
27 | 'includePaths[]=' + path.resolve(__dirname, '../node_modules')
28 | ];
29 |
30 | if (DEBUG || TEST) {
31 | jsxLoader = [];
32 | if (!TEST) {
33 | jsxLoader.push('react-hot');
34 | }
35 | jsxLoader.push('babel-loader?optional[]=runtime&stage=0&plugins=rewire');
36 | sassParams.push('sourceMap', 'sourceMapContents=true');
37 | sassLoader = [
38 | 'style-loader',
39 | 'css-loader?sourceMap&modules&localIdentName=[name]__[local]___[hash:base64:5]',
40 | 'postcss-loader',
41 | 'sass-loader?' + sassParams.join('&')
42 | ].join('!');
43 | cssLoader = [
44 | 'style-loader',
45 | 'css-loader?sourceMap&modules&localIdentName=[name]__[local]___[hash:base64:5]',
46 | 'postcss-loader'
47 | ].join('!');
48 | } else {
49 | jsxLoader = ['babel-loader?optional[]=runtime&stage=0&plugins=rewire'];
50 | sassLoader = ExtractTextPlugin.extract('style-loader', [
51 | 'css-loader?modules&localIdentName=[hash:base64:5]',
52 | 'postcss-loader',
53 | 'sass-loader?' + sassParams.join('&')
54 | ].join('!'));
55 | cssLoader = ExtractTextPlugin.extract('style-loader', [
56 | 'css-loader?modules&localIdentName=[hash:base64:5]',
57 | 'postcss-loader'
58 | ].join('!'));
59 | }
60 |
61 | var loaders = [
62 | {
63 | test: /\.jsx?$/,
64 | exclude: /node_modules/,
65 | loaders: jsxLoader
66 | },
67 | {
68 | test: /\.css$/,
69 | loader: cssLoader
70 | },
71 | {
72 | test: /\.jpe?g$|\.gif$|\.png$|\.ico|\.svg$|\.woff$|\.ttf$/,
73 | loader: fileLoader
74 | },
75 | {
76 | test: /\.json$/,
77 | exclude: /node_modules/,
78 | loaders: jsonLoader
79 | },
80 | {
81 | test: /\.html$/,
82 | loader: htmlLoader
83 | },
84 | {
85 | test: /\.scss$/,
86 | loader: sassLoader
87 | }
88 | ];
89 |
90 | module.exports = loaders;
91 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-seed",
3 | "version": "0.0.16",
4 | "description": "Seed project for React apps using ES6 & webpack.",
5 | "repository": "https://github.com/badsyntax/react-seed",
6 | "license": "MIT",
7 | "config": {
8 | "buildDir": "./build",
9 | "buildDirTests": "./build_tests",
10 | "devHost": "localhost",
11 | "devPort": 8000
12 | },
13 | "scripts": {
14 | "build": "NODE_ENV=production npm run webpack",
15 | "clean": "rimraf $npm_package_config_buildDir && mkdir $npm_package_config_buildDir",
16 | "env": "env",
17 | "lint": "eslint --ext .js --ext .jsx ./app ./webpack && echo No linting errors.",
18 | "prebuild": "npm run clean",
19 | "prestart": "npm install",
20 | "pretest": "npm install && npm run lint",
21 | "pretest-travis": "npm install && npm run lint",
22 | "start": "NODE_ENV=development node dev-server ./webpack/config",
23 | "test": "NODE_ENV=test karma start",
24 | "test-travis": "NODE_ENV=test karma start --single-run",
25 | "webpack": "webpack --colors --progress --config ./webpack/config"
26 | },
27 | "dependencies": {
28 | "classnames": "^2.1.3",
29 | "flux": "^2.0.3",
30 | "normalize.css": "^3.0.3",
31 | "react": "^0.13.3"
32 | },
33 | "devDependencies": {
34 | "autoprefixer-core": "^5.2.1",
35 | "babel-core": "^5.8.3",
36 | "babel-eslint": "^3.1.23",
37 | "babel-loader": "^5.3.1",
38 | "babel-plugin-rewire": "^0.1.8",
39 | "babel-runtime": "^5.6.15",
40 | "chai": "^3.0.0",
41 | "css-loader": "^0.15.2",
42 | "eslint": "^0.24.0",
43 | "eslint-plugin-react": "^2.6.4",
44 | "extract-text-webpack-plugin": "^0.8.2",
45 | "file-loader": "^0.8.4",
46 | "glob": "^5.0.13",
47 | "html-loader": "^0.3.0",
48 | "json-loader": "^0.5.2",
49 | "karma": "^0.13.2",
50 | "karma-chrome-launcher": "^0.2.0",
51 | "karma-cli": "0.1.0",
52 | "karma-mocha": "^0.2.0",
53 | "karma-mocha-reporter": "^1.0.2",
54 | "karma-phantomjs-launcher": "^0.2.0",
55 | "karma-sinon": "^1.0.4",
56 | "karma-source-map-support": "^1.0.0",
57 | "karma-sourcemap-loader": "^0.3.5",
58 | "karma-webpack": "^1.5.1",
59 | "lodash": "^3.10.0",
60 | "mocha": "^2.2.5",
61 | "mocha-loader": "^0.7.1",
62 | "node-libs-browser": "^0.5.2",
63 | "node-sass": "^3.2.0",
64 | "opn": "^3.0.2",
65 | "phantomjs": "^1.9.17",
66 | "postcss-loader": "^0.5.1",
67 | "react-hot-loader": "^1.2.8",
68 | "rimraf": "^2.4.1",
69 | "sass-loader": "^1.0.2",
70 | "sinon": "^1.15.4",
71 | "source-map-support": "^0.3.2",
72 | "style-loader": "^0.12.3",
73 | "template-html-loader": "0.0.3",
74 | "webpack": "^1.10.1",
75 | "webpack-dev-server": "^1.10.1"
76 | },
77 | "engines": {
78 | "node": ">=0.12.0"
79 | },
80 | "eslintConfig": {
81 | "env": {
82 | "browser": true,
83 | "node": true,
84 | "es6": true
85 | },
86 | "ecmaFeatures": {
87 | "modules": true,
88 | "jsx": true
89 | },
90 | "globals": {
91 | "describe": true,
92 | "it": true,
93 | "sinon": true
94 | },
95 | "parser": "babel-eslint",
96 | "plugins": [
97 | "react"
98 | ],
99 | "rules": {
100 | "strict": [
101 | 2,
102 | "global"
103 | ],
104 | "indent": [
105 | 2,
106 | 2
107 | ],
108 | "quotes": [
109 | 2,
110 | "single"
111 | ],
112 | "no-alert": 0,
113 | "no-underscore-dangle": 0,
114 | "react/display-name": 0,
115 | "react/jsx-quotes": 1,
116 | "react/jsx-no-undef": 1,
117 | "react/jsx-sort-props": 1,
118 | "react/jsx-uses-react": 1,
119 | "react/jsx-uses-vars": 1,
120 | "react/no-did-mount-set-state": 1,
121 | "react/no-did-update-set-state": 1,
122 | "react/no-multi-comp": 1,
123 | "react/no-unknown-property": 1,
124 | "react/prop-types": 0,
125 | "react/react-in-jsx-scope": 1,
126 | "react/self-closing-comp": 1,
127 | "react/wrap-multilines": 1
128 | }
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React seed [](https://travis-ci.org/badsyntax/react-seed)
2 |
3 | A boilerplate for building React apps with ES6 and webpack.
4 |
5 | **_This is old. You should use [create-react-app](https://github.com/facebookincubator/create-react-app) instead._**
6 |
7 | ## What you get
8 |
9 | * React 0.13
10 | * ES6, ES7 & JSX to ES5 via babel
11 | * webpack with react hot loader, and other useful loaders
12 | * [Local CSS](https://github.com/webpack/css-loader#local-scope)
13 | * Karma, mocha, chai & sinon for testing with mocking examples
14 | * Basic flux architecture with app actions, stores and example web API usage
15 | * React router ([feature/react-router](https://github.com/badsyntax/react-seed/tree/feature/react-router))
16 | * Material UI ([feature/material-ui](https://github.com/badsyntax/react-seed/tree/feature/material-ui))
17 |
18 | ## Getting started
19 |
20 | ### Installing with git
21 |
22 | ```bash
23 | git clone --depth=1 https://github.com/badsyntax/react-seed.git my-project
24 | ```
25 |
26 | ### Installing with yeoman
27 |
28 | 1. `npm install -g yo`
29 | 2. `npm install -g generator-react-seed`
30 | 3. Use the generator like so: `yo react-seed`
31 |
32 | ## npm scripts
33 |
34 | * `npm start` - Build and start the app in dev mode at http://localhost:8000
35 | * `npm test` - Run the tests
36 | * `npm run build` - Run a production build
37 |
38 | ## Examples
39 |
40 | ### Writing components:
41 |
42 | ```js
43 | // Filename: Menu.jsx
44 |
45 | 'use strict';
46 |
47 | import styles from './_Menu.scss';
48 | import React from 'react';
49 | import MenuItem from './MenuItem';
50 |
51 | let { Component, PropTypes } = React;
52 |
53 | export default class Menu extends Component {
54 |
55 | static defaultProps = {
56 | items: []
57 | };
58 |
59 | static propTypes = {
60 | items: PropTypes.array.isRequired
61 | };
62 |
63 | render() {
64 | return (
65 |
66 | {this.props.items.map((item) => {
67 | return ( );
68 | }, this)}
69 |
70 | );
71 | }
72 | }
73 | ```
74 |
75 | ###Writing tests:
76 |
77 | ```js
78 | // Filename: __tests__/Menu-test.jsx
79 |
80 | 'use strict';
81 |
82 | import React from 'react/addons';
83 | import { expect } from 'chai';
84 |
85 | import Menu from '../Menu.jsx';
86 | import MenuItem from '../MenuItem.jsx';
87 |
88 | // Here we create a mocked MenuItem component.
89 | class MockedMenuItem extends MenuItem {
90 | render() {
91 | return (
92 | {this.props.item.label}
93 | );
94 | }
95 | }
96 |
97 | // Here we set the mocked MenuItem component.
98 | Menu.__Rewire__('MenuItem', MockedMenuItem);
99 |
100 | describe('Menu', () => {
101 |
102 | let { TestUtils } = React.addons;
103 |
104 | let menuItems = [
105 | { id: 1, label: 'Option 1' },
106 | { id: 2, label: 'Option 2' }
107 | ];
108 |
109 | let menu = TestUtils.renderIntoDocument(
110 |
111 | );
112 | let menuElem = React.findDOMNode(menu);
113 | let items = menuElem.querySelectorAll('li');
114 |
115 | it('Should render the menu items', () => {
116 | expect(items.length).to.equal(2);
117 | });
118 |
119 | it('Should render the menu item labels', () => {
120 | Array.prototype.forEach.call(items, (item, i) => {
121 | expect(item.textContent.trim()).to.equal(menuItems[i].label);
122 | });
123 | })
124 |
125 | it('Should render the mocked menu item', () => {
126 | expect(items[0].className).to.equal('mocked-menu-item');
127 | });
128 | });
129 |
130 | ```
131 |
132 | ## Sass, CSS & webpack
133 |
134 | `import` Sass and CSS files from within your JavaScript component files:
135 |
136 | ```js
137 | // Filename: app.jsx
138 | import 'normalize.css/normalize.css';
139 | import styles from './scss/app.scss';
140 | ```
141 |
142 | * **Note:** If you're importing component Sass files from within your JavaScript component files, then each sass file will be compiled as part of a different compile process, and thus you cannot share global references. See [this issue](https://github.com/jtangelder/sass-loader/issues/105) for more information.
143 | * Sass include paths can be adjusted in the `webpack/loaders.js` file.
144 | * All CSS (compiled or otherwise) is run through Autoprefixer and style-loader. Any images/fonts etc referenced in the CSS will be copied to the build dir.
145 | * CSS files are combined in the order in which they are imported in JavaScript, thus
146 | you should always import your CSS/Sass before importing any other JavaScript files.
147 | * If not using local CSS, use an approach like [BEM](http://cssguidelin.es/#bem-like-naming) to avoid specificity
148 | issues that might exist due to unpredicatable order of CSS rules.
149 |
150 | ## HTML files
151 |
152 | All required `.html` files are compiled with lodash.template and synced into the `./build` directory:
153 |
154 | ```js
155 | // Filename: app.jsx
156 | import './index.html';
157 | ```
158 |
159 | * You can adjust the lodash template data in the `webpack/loaders.js` file.
160 |
161 | ## Conventions
162 |
163 | * Use fat arrows for anonymous functions
164 | * Don't use `var`. Use `let` and `const`.
165 |
166 |
167 | ## Releasing
168 |
169 | 1. `npm version patch`
170 | 2. `git push --follow-tags`
171 | 3. `npm login` (Optional)
172 | 4. `npm publish`
173 |
174 | ## Credits
175 |
176 | This project was initially forked from https://github.com/tcoopman/react-es6-browserify
177 |
178 | ## License
179 |
180 | Copyright (c) 2015 Richard Willis
181 |
182 | MIT (http://opensource.org/licenses/MIT)
183 |
--------------------------------------------------------------------------------