Skip to content

Commit

Permalink
Introduce the new App and Layout components. Upgrade history module…
Browse files Browse the repository at this point in the history
…. (#869)

* Introduce the new App and Layout components. Upgrade `history` module.

* Some clean up and farther improvements
  • Loading branch information
koistya authored Sep 23, 2016
1 parent 886124e commit 448121f
Show file tree
Hide file tree
Showing 23 changed files with 387 additions and 326 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.

### [Unreleased][unreleased]

- Split the `App` component into `App` setting context variables and `Layout` setting general look and feel of the app (BREAKING CHANGE)
- Upgrade `history` npm module to v4.x, update `Link` component (BREAKING CHANGE)
- Remove `core/createHistory.js` in favor of initializing a new history instance inside `server.js` and `client.js` (BREAKING CHANGE)
- Remove Jade dependency in favor of React-based templates: `src/views/index.jade => src/components/Html`
(BREAKING CHANGE) [#711](https://github.com/kriasoft/react-starter-kit/pull/711)
- Update `isomorphic-style-loader` to `v1.0.0`, it adds comparability with ES2015+ decorators.
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ $ npm run test:watch # Launch unit test runner and start watching for changes
```

By default, [Mocha](https://mochajs.org/) test runner is looking for test files
matching the `src/**/*.test.js` pattern. Take a look at `src/components/App/App.test.js`
matching the `src/**/*.test.js` pattern. Take a look at `src/components/Layout/Layout.test.js`
as an example.

To deploy the app, run:
Expand Down
13 changes: 8 additions & 5 deletions docs/testing-your-application.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,24 @@ npm test
### Basic example

To help you on your way RSK comes with the following
[basic test case](https://github.com/kriasoft/react-starter-kit/blob/master/src/components/App/App.test.js)
[basic test case](https://github.com/kriasoft/react-starter-kit/blob/master/src/components/Layout/Layout.test.js)
you can use as a starting point:

```js
import React from 'react';
import { expect } from 'chai';
import { shallow } from 'enzyme';
import App from './App';
import App from '../App';
import Layout from './Layout';

describe('App', () => {
describe('Layout', () => {

it('renders children correctly', () => {
const wrapper = shallow(
<App context={{ insertCss: () => {} }}>
<div className="child" />
<Layout>
<div className="child" />
</Layout>
</App>
);

Expand All @@ -58,7 +61,7 @@ describe('App', () => {
});
```

### React-intl example
### React-intl exampleß

React-intl users MUST render/wrap components inside an IntlProvider like the example below:

Expand Down
37 changes: 19 additions & 18 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"classnames": "2.2.5",
"cookie-parser": "1.4.3",
"core-js": "2.4.1",
"eventemitter3": "1.2.0",
"eventemitter3": "2.0.0",
"express": "4.14.0",
"express-graphql": "0.5.4",
"express-jwt": "5.0.0",
Expand All @@ -21,32 +21,33 @@
"front-matter": "2.1.0",
"graphiql": "0.7.8",
"graphql": "0.7.0",
"history": "3.0.0",
"history": "4.2.0",
"isomorphic-style-loader": "1.0.0",
"jsonwebtoken": "7.1.9",
"markdown-it": "7.0.1",
"node-fetch": "1.6.0",
"markdown-it": "8.0.0",
"node-fetch": "1.6.1",
"normalize.css": "4.2.0",
"passport": "0.3.2",
"passport-facebook": "2.1.1",
"pretty-error": "2.0.0",
"react": "15.3.1",
"react-dom": "15.3.1",
"sequelize": "3.24.2",
"query-string": "4.2.3",
"react": "15.3.2",
"react-dom": "15.3.2",
"sequelize": "3.24.3",
"source-map-support": "0.4.2",
"sqlite3": "3.1.4",
"universal-router": "1.2.2",
"whatwg-fetch": "1.0.0"
},
"devDependencies": {
"assets-webpack-plugin": "^3.4.0",
"autoprefixer": "^6.4.0",
"autoprefixer": "^6.4.1",
"babel-cli": "^6.14.0",
"babel-core": "^6.14.0",
"babel-eslint": "^6.1.2",
"babel-loader": "^6.2.5",
"babel-plugin-react-transform": "^2.0.2",
"babel-plugin-rewire": "^1.0.0-rc-7",
"babel-plugin-rewire": "^1.0.0",
"babel-plugin-transform-react-constant-elements": "^6.9.1",
"babel-plugin-transform-react-inline-elements": "^6.8.0",
"babel-plugin-transform-react-remove-prop-types": "^0.2.9",
Expand All @@ -58,22 +59,22 @@
"babel-register": "^6.14.0",
"babel-template": "^6.15.0",
"babel-types": "^6.15.0",
"browser-sync": "^2.14.3",
"browser-sync": "^2.16.0",
"chai": "^3.5.0",
"css-loader": "^0.25.0",
"del": "^2.2.2",
"enzyme": "^2.4.1",
"eslint": "^3.4.0",
"eslint": "^3.5.0",
"eslint-config-airbnb": "^11.1.0",
"eslint-loader": "^1.5.0",
"eslint-plugin-import": "^1.14.0",
"eslint-plugin-import": "^1.15.0",
"eslint-plugin-jsx-a11y": "^2.2.2",
"eslint-plugin-react": "^6.2.1",
"eslint-plugin-react": "^6.3.0",
"extend": "^3.0.0",
"file-loader": "^0.9.0",
"gaze": "^1.1.1",
"git-repository": "^0.1.4",
"glob": "^7.0.6",
"glob": "^7.1.0",
"json-loader": "^0.5.4",
"mkdirp": "^0.5.1",
"mocha": "^3.0.2",
Expand All @@ -95,13 +96,13 @@
"postcss-selector-matches": "^2.0.5",
"postcss-selector-not": "^2.0.0",
"raw-loader": "^0.5.1",
"react-addons-test-utils": "15.3.1",
"react-addons-test-utils": "15.3.2",
"react-transform-catch-errors": "^1.0.2",
"react-transform-hmr": "^1.0.4",
"redbox-react": "^1.3.0",
"redbox-react": "^1.3.1",
"sinon": "^2.0.0-pre.2",
"stylelint": "^7.2.0",
"stylelint-config-standard": "^13.0.0",
"stylelint": "^7.3.1",
"stylelint-config-standard": "^13.0.2",
"url-loader": "^0.5.7",
"webpack": "^1.13.2",
"webpack-hot-middleware": "^2.12.2",
Expand Down
75 changes: 43 additions & 32 deletions src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,32 @@
*/

import 'babel-polyfill';
import React from 'react';
import ReactDOM from 'react-dom';
import FastClick from 'fastclick';
import UniversalRouter from 'universal-router';
import { readState, saveState } from 'history/lib/DOMStateStorage';
import history from './core/history';
import queryString from 'query-string';
import createBrowserHistory from 'history/createBrowserHistory';
import App from './components/App';
import {
addEventListener,
removeEventListener,
windowScrollX,
windowScrollY,
} from './core/DOMUtils';

// Global (context) variables that can be easily accessed from any React component
// https://facebook.github.io/react/docs/context.html
const context = {
// Navigation manager, e.g. history.push('/home')
// https://github.com/mjackson/history
history: createBrowserHistory(),
// Enables critical path CSS rendering
// https://github.com/kriasoft/isomorphic-style-loader
insertCss: (...styles) => {
const removeCss = styles.map(style => style._insertCss()); // eslint-disable-line no-underscore-dangle, max-len
return () => {
removeCss.forEach(f => f());
};
// eslint-disable-next-line no-underscore-dangle
const removeCss = styles.map(x => x._insertCss());
return () => { removeCss.forEach(f => f()); };
},
};

Expand Down Expand Up @@ -55,22 +63,25 @@ function updateCustomMeta(name, value) { // eslint-disable-line no-unused-vars
}

// Restore the scroll position if it was saved into the state
function restoreScrollPosition({ state, hash }) {
if (state && state.scrollY !== undefined) {
window.scrollTo(state.scrollX, state.scrollY);
return;
}

const targetHash = hash && hash.substr(1);
if (targetHash) {
const target = document.getElementById(targetHash);
if (target) {
window.scrollTo(0, windowScrollY() + target.getBoundingClientRect().top);
return;
let locationStates = {};
function restoreScrollPosition({ key, hash }) {
let scrollX = 0;
let scrollY = 0;
const state = locationStates[key];
if (state) {
scrollX = state.scrollX;
scrollY = state.scrollY;
} else {
const targetHash = hash && hash.substr(1);
if (targetHash) {
const target = document.getElementById(targetHash);
if (target) {
scrollY = windowScrollY() + target.getBoundingClientRect().top;
}
}
}

window.scrollTo(0, 0);
window.scrollTo(scrollX, scrollY);
}

let onRenderComplete = function initialRenderComplete() {
Expand Down Expand Up @@ -102,7 +113,7 @@ function render(route, location) {
return new Promise((resolve, reject) => {
try {
ReactDOM.render(
route.component,
<App context={context}>{route.component}</App>,
container,
onRenderComplete.bind(undefined, route, location)
);
Expand All @@ -115,27 +126,26 @@ function render(route, location) {
// Make taps on links and buttons work fast on mobiles
FastClick.attach(document.body);

let currentLocation = history.getCurrentLocation();
let currentLocation = context.history.location;
let routes = require('./routes').default;

// Re-render the app when window.location changes
async function onLocationChange(location) {
// Save the page scroll position into the current location's state
if (currentLocation.key) {
saveState(currentLocation.key, {
...readState(currentLocation.key),
scrollX: windowScrollX(),
scrollY: windowScrollY(),
});
locationStates[currentLocation.key] = {
scrollX: windowScrollX(),
scrollY: windowScrollY(),
};
if (history.action === 'PUSH') {
delete locationStates[location.key];
}
currentLocation = location;

try {
const route = await UniversalRouter.resolve(routes, {
path: location.pathname,
query: location.query,
query: queryString.parse(location.search),
state: location.state,
context,
});

await render(route, location);
Expand All @@ -146,8 +156,8 @@ async function onLocationChange(location) {
}

// Add History API listener and trigger initial change
const removeHistoryListener = history.listen(onLocationChange);
history.replace(currentLocation);
const removeHistoryListener = context.history.listen(onLocationChange);
onLocationChange(currentLocation);

// Switch off the native scroll restoration behavior and handle it manually
// https://developers.google.com/web/updates/2015/09/history-api-scroll-restoration
Expand All @@ -161,6 +171,7 @@ if (window.history && 'scrollRestoration' in window.history) {
addEventListener(window, 'pagehide', function onPageHide() {
removeEventListener(window, 'pagehide', onPageHide);
removeHistoryListener();
locationStates = {};
if (originalScrollRestoration) {
window.history.scrollRestoration = originalScrollRestoration;
originalScrollRestoration = undefined;
Expand All @@ -172,6 +183,6 @@ if (module.hot) {
module.hot.accept('./routes', () => {
routes = require('./routes').default; // eslint-disable-line global-require

onLocationChange(history.getCurrentLocation());
onLocationChange(context.history.location);
});
}
57 changes: 57 additions & 0 deletions src/components/App.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* React Starter Kit (https://www.reactstarterkit.com/)
*
* Copyright © 2014-2016 Kriasoft, LLC. All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE.txt file in the root directory of this source tree.
*/

import React, { PropTypes } from 'react';

const ContextType = {
// Navigation manager, e.g. history.push('/home')
// https://github.com/mjackson/history
history: PropTypes.object.isRequired,
// Enables critical path CSS rendering
// https://github.com/kriasoft/isomorphic-style-loader
insertCss: PropTypes.func.isRequired,
};

/**
* The top-level React component setting context (global) variables
* that can be accessed from all the child components.
*
* https://facebook.github.io/react/docs/context.html
*
* Usage example:
*
* const context = {
* history: createBrowserHistory(),
* store: createStore(),
* };
*
* ReactDOM.render(<App context={context}><HomePage /></App>, container);
*/
class App extends React.Component {

static propTypes = {
context: PropTypes.shape(ContextType).isRequired,
children: PropTypes.element.isRequired,
};

static childContextTypes = ContextType;

getChildContext() {
return this.props.context;
}

render() {
// NOTE: If you need to add or modify header, footer etc. of the app,
// please do that inside the Layout component.
return React.Children.only(this.props.children);
}

}

export default App;
Loading

0 comments on commit 448121f

Please sign in to comment.