Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add mobx boilerplate #10

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ cypress/videos
# IDE
.idea
*.sublime-workspace
.vscode
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"eslint-plugin-jsx-a11y": "^3.0.2",
"eslint-plugin-react": "^6.9.0",
"jest": "^18.1.0",
"mobx-react-devtools": "^4.2.15",
"react-addons-test-utils": "^15.4.2"
},
"dependencies": {
Expand Down Expand Up @@ -66,6 +67,8 @@
"file-loader": "^0.10.1",
"helmet": "^3.4.0",
"html-webpack-plugin": "^2.28.0",
"mobx": "^3.1.16",
"mobx-react": "^4.2.1",
"morgan": "^1.7.0",
"pretty-error": "^2.0.2",
"react": "^15.4.2",
Expand All @@ -74,6 +77,7 @@
"react-helmet": "^4.0.0",
"rimraf": "^2.6.1",
"rollbar": "^0.6.5",
"serialize-javascript": "^1.3.0",
"source-map-support": "^0.4.9",
"spdy": "^3.4.4",
"url-loader": "^0.5.8",
Expand Down
49 changes: 39 additions & 10 deletions src/browser/App.react.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,44 @@
import React, { Component, PropTypes as RPT } from 'react';
import Helmet from 'react-helmet';
import React from 'react';
import { observer, inject } from 'mobx-react';
import logo from '../../assets/images/haystack_logo.png';

const App = () => (
<div>
<Helmet title="Haystack" />
<button onClick={e => window.console.log(e)}>
Haystack
<img alt="logo" src={logo} />
</button>
</div>
);
@inject('sample')
@observer
class App extends Component {
handlePlus = () => this.props.sample.plus()
handleMinus = () => this.props.sample.minus()

render() {
const { sample } = this.props;

return (
<div>
<Helmet title="Haystack" />
<button onClick={e => window.console.log(e)}>
Haystack
<img alt="logo" src={logo} />
</button>

<br /><br />

<button onClick={this.handlePlus}>+</button>
<button onClick={this.handleMinus}>-</button>

<p>{sample.count}</p>

<span dangerouslySetInnerHTML={{ __html: sample.special }} />
</div>
);
}
}

App.wrappedComponent.propTypes = {
sample: RPT.shape({
count: RPT.number.isRequired,
plus: RPT.func.isRequired,
minus: RPT.func.isRequired,
}).isRequired
};

export default App;
32 changes: 31 additions & 1 deletion src/browser/BrowserProvider.react.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,33 @@
const BrowserProvider = ({ children }) => children;
import React, { PropTypes as RPT } from 'react';
import { Provider } from 'mobx-react';
import createStores from '../common/createStores';

const stores = createStores(window.MOBX_STATE);

let AppRoot = null;

// Use mobx-react-devtools in dev build
if (process.env.APP_ENV === 'development') {
const DevTools = require('mobx-react-devtools').default; // eslint-disable-line import/no-extraneous-dependencies, global-require

AppRoot = children => (
<div>
{children}
<DevTools />
</div>
);
} else {
AppRoot = children => children;
}

const BrowserProvider = ({ children }) => (
<Provider {...stores}>
{AppRoot(children)}
</Provider>
);

BrowserProvider.propTypes = {
children: RPT.node.isRequired
};

export default BrowserProvider;
6 changes: 5 additions & 1 deletion src/browser/__tests__/App.react.spec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { shallow } from 'enzyme';
import React from 'react';
import App from '../App.react';
import createStores from '../../common/createStores';

const stores = createStores();

test('shallowly renders App', () => {
expect(shallow(<App />)).toMatchSnapshot();
const options = { context: { mobxStores: { ...stores } } };
expect(shallow(<App />, options).shallow()).toMatchSnapshot();
});
17 changes: 17 additions & 0 deletions src/browser/__tests__/__snapshots__/App.react.spec.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,22 @@ exports[`test shallowly renders App 1`] = `
alt="logo"
src="test-file-stub" />
</button>
<br />
<br />
<button
onClick={[Function]}>
+
</button>
<button
onClick={[Function]}>
-
</button>
<p />
<span
dangerouslySetInnerHTML={
Object {
"__html": "¯\\_(ツ)_/¯",
}
} />
</div>
`;
25 changes: 25 additions & 0 deletions src/browser/__tests__/store.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import SampleStore from '../store';

test('sample store actions', () => {
const store = new SampleStore();

expect(store.count).toBe(0);

store.plus();
expect(store.count).toBe(1);
store.plus();
expect(store.count).toBe(2);
store.minus();
expect(store.count).toBe(1);
});

test('sample store computed', () => {
const store = new SampleStore();

expect(store.special).toBe('¯\\_(ツ)_/¯');

store.count = 5;
expect(store.special).toBe('•_•)<br>( •_•)>⌐■-■<br>(⌐■_■)');
store.count = 4;
expect(store.special).toBe('¯\\_(ツ)_/¯');
});
19 changes: 19 additions & 0 deletions src/browser/store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { action, observable, computed } from 'mobx';

export default class SampleStore {
@observable count = 0;

@action plus() {
this.count += 1;
}

@action minus() {
this.count -= 1;
}

@computed get special() {
return this.count < 5
? '¯\\_(ツ)_/¯'
: '•_•)<br>( •_•)>⌐■-■<br>(⌐■_■)';
}
}
55 changes: 55 additions & 0 deletions src/common/__tests__/createStores.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { observable, toJS } from 'mobx';
import createStores, { createNewStores, getState } from '../createStores';
import SampleStore from '../../browser/store';

const store1 = observable({ foo: 'foo' });
class Store2 {
@observable bar = 'foo_too';
}

const expectedStoreData = {
store1: { foo: 'foo' },
store2: { bar: 'foo_too' },
};

test('createStores getState', () => {
const actual = getState({ store1, store2: new Store2() });

expect(actual).toEqual(expectedStoreData);
});

test('createStores createNewStores', () => {
const initialStoreData = {
store1: { foo: 'foo' },
store2: { bar: 'foo_too' },
};
const stores = { store1, store2: new Store2() };
const actualStores = createNewStores(stores);

expect(actualStores).toEqual(stores);
expect(toJS(actualStores.store1)).toEqual(initialStoreData.store1);
expect(toJS(actualStores.store2)).toEqual(initialStoreData.store2);

const newData = {
store1: { foo: 'xyz' },
store2: { bar: 'abc' },
};
const actualHydratedStores = createNewStores(stores, newData);

expect(actualHydratedStores).toEqual(stores);
expect(toJS(actualHydratedStores.store1)).toEqual(newData.store1);
expect(toJS(actualHydratedStores.store2)).toEqual(newData.store2);
});

test('createStores', () => {
const stores = createStores();

expect(stores.sample).toBeInstanceOf(SampleStore);
expect(toJS(stores.sample)).toEqual({ count: 0 });

const initialData = { sample: { count: 55 } };
const storesWData = createStores(initialData);

expect(storesWData.sample).toBeInstanceOf(SampleStore);
expect(toJS(storesWData.sample)).toEqual({ count: 55 });
});
42 changes: 42 additions & 0 deletions src/common/createStores.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { toJS, extendObservable } from 'mobx';
import SampleStore from '../browser/store';

const createAndHydrateStore = (store, initialData) => {
const maybeHydratedStore = initialData ? extendObservable(store, initialData) : store;
return maybeHydratedStore;
};

export const createNewStores = (stores, initialData = {}) =>
Object.keys(stores).reduce(
(finalStores, storeKey) => ({
...finalStores,
[storeKey]: createAndHydrateStore(stores[storeKey], initialData[storeKey]),
}),
{}
);

export const getState = stores =>
Object.keys(stores).reduce(
(data, storeKey) => ({
...data,
[storeKey]: toJS(stores[storeKey]),
}),
{}
);

const createStores = (initialData = {}) => {
/**
* Define the stores
* e.g {
* todos: new Todos(),
* user
* }
*/
const stores = {
sample: new SampleStore(),
};

return createNewStores(stores, initialData);
};

export default createStores;
9 changes: 7 additions & 2 deletions src/server/frontend/Html.react.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
/* eslint-disable react/no-danger */
import React, { PropTypes as RPT } from 'react';
import serialize from 'serialize-javascript';
import Rollbar from './scripts/Rollbar';
import Script from './Script.react';
import { googleTagManagerNoScript, googleTagManagerScript } from './scripts/GoogleTagManager';

const Html = ({ bodyHtml, javascripts = {}, helmet, options }) => (
const Html = ({ bodyHtml, javascripts = {}, helmet, options, mobxState }) => (
<html lang="en">
<head>
{googleTagManagerScript()}
Expand All @@ -24,6 +25,7 @@ const Html = ({ bodyHtml, javascripts = {}, helmet, options }) => (
<body>
{googleTagManagerNoScript()}
<script type="text/javascript" dangerouslySetInnerHTML={{ __html: `window.__FEATURES=${JSON.stringify(options.features)}` }} />
<script type="text/javascript" dangerouslySetInnerHTML={{ __html: `window.MOBX_STATE=${serialize(mobxState)}` }} />
<div id="app" dangerouslySetInnerHTML={{ __html: bodyHtml }} />
<Script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=Symbol" />
{!options.disableJS && javascripts.vendor && <Script src={javascripts.vendor} />}
Expand All @@ -47,7 +49,10 @@ Html.propTypes = {
}),
options: RPT.shape({
disableJS: RPT.bool
})
}),
mobxState: RPT.shape({
sample: RPT.object
}).isRequired
};

Html.defaultProps = {
Expand Down
20 changes: 19 additions & 1 deletion src/server/frontend/ServerProvider.react.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
const ServerProvider = ({ children }) => children;
import React, { PropTypes as RPT } from 'react';
import { Provider } from 'mobx-react';

const ServerProvider = ({ children, stores }) => (
<Provider {...stores}>
{children}
</Provider>
);

ServerProvider.propTypes = {
children: RPT.node.isRequired,
stores: RPT.shape({
sample: RPT.shape({
count: RPT.number.isRequired,
plus: RPT.func.isRequired,
minus: RPT.func.isRequired,
}).isRequired
}).isRequired
};

export default ServerProvider;
3 changes: 2 additions & 1 deletion src/server/frontend/__tests__/Html.react.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import Html from '../Html.react';

const props = {
bodyHtml: '<div />',
javascripts: { app: 'app.xxxx.js' }
javascripts: { app: 'app.xxxx.js' },
mobxState: { sample: { count: 0 } }
};

test('shallowly renders Html', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ exports[`test shallowly renders Html 1`] = `
}
}
type="text/javascript" />
<script
dangerouslySetInnerHTML={
Object {
"__html": "window.MOBX_STATE={\"sample\":{\"count\":0}}",
}
}
type="text/javascript" />
<div
dangerouslySetInnerHTML={
Object {
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
exports[`test render 1`] = `"<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\"/><meta content=\"width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no\" name=\"viewport\"/><meta content=\"ie=edge\" http-equiv=\"x-ua-compatible\"/><title data-react-helmet=\"true\"></title></head><body><script type=\"text/javascript\">window.__FEATURES=undefined</script><div id=\"app\"><div id=\"app\"><div data-reactroot=\"\" data-reactid=\"1\" data-react-checksum=\"1998851930\"></div></div></div><script src=\"https://cdn.polyfill.io/v2/polyfill.min.js?features=Symbol\" type=\"text/javascript\"></script><script src=\"app.xxx.js\" type=\"text/javascript\"></script></body></html>"`;
exports[`test render 1`] = `"<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\"/><meta content=\"width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no\" name=\"viewport\"/><meta content=\"ie=edge\" http-equiv=\"x-ua-compatible\"/><title data-react-helmet=\"true\"></title></head><body><script type=\"text/javascript\">window.__FEATURES=undefined</script><script type=\"text/javascript\">window.MOBX_STATE={\"sample\":{\"count\":2}}</script><div id=\"app\"><div id=\"app\"><div data-reactroot=\"\" data-reactid=\"1\" data-react-checksum=\"1998851930\"></div></div></div><script src=\"https://cdn.polyfill.io/v2/polyfill.min.js?features=Symbol\" type=\"text/javascript\"></script><script src=\"app.xxx.js\" type=\"text/javascript\"></script></body></html>"`;
Loading