Skip to content

Commit

Permalink
Router working with real routes (#21)
Browse files Browse the repository at this point in the history
* Working router implementation using GraphQL API

* First fully working iteration of Router integrated with API

* Add docs for router

* Fix misleading comment

* Bug fix

* Add test to validate we dont double-fetch components

* Router >> using
  • Loading branch information
DrewML authored and Andrew Levine committed Jun 13, 2018
1 parent e17e89a commit 275e23e
Show file tree
Hide file tree
Showing 19 changed files with 501 additions and 393 deletions.
26 changes: 26 additions & 0 deletions packages/peregrine/docs/Router.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Peregrine Router

The Peregrine Router is a client-side router that is designed to understand the
different storefront routes within Magento 2. If using Peregrine to bootstrap
your PWA, it is configured automatically. If not, the Router can be manually
consumed.

## Manual Usage

```jsx
import ReactDOM from 'react-dom';
import { Router } from '@magento/peregrine';

ReactDOM.render(
<Router apiBase="https://mystore.com" />,
document.querySelector('main')
);
```

## Props

| Prop Name | Required? | Description |
| ------------- | :-------: | -----------------------------------------------------------------------------------------------------: |
| `apiBase` || Root URL of the Magento store, including protocol and hostname |
| `using` | | The Router implementation to use from React-Router. Can be `BrowserRouter`/`HashRouter`/`MemoryRouter` |
| `routerProps` | | Any additional props to be passed to React-Router |
5 changes: 4 additions & 1 deletion packages/peregrine/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
module.exports = {
setupFiles: ['<rootDir>/scripts/shim.js'],
setupFiles: [
'<rootDir>/scripts/shim.js',
'<rootDir>/scripts/fetch-mock.js'
],
verbose: true,
collectCoverageFrom: ['src/**/*.js']
};
9 changes: 9 additions & 0 deletions packages/peregrine/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/peregrine/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"eslint-plugin-jsx-a11y": "^6.0.2",
"eslint-plugin-react": "^7.5.1",
"jest": "^21.3.0-beta.13",
"jest-fetch-mock": "^1.4.1",
"npm-run-all": "^4.1.2",
"prettier": "^1.8.2",
"prettier-check": "^2.0.0",
Expand Down
1 change: 1 addition & 0 deletions packages/peregrine/scripts/fetch-mock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
global.fetch = require('jest-fetch-mock');
32 changes: 21 additions & 11 deletions packages/peregrine/src/Peregrine/Peregrine.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,37 @@
import { createElement } from 'react';
import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import { render } from 'react-dom';
import createStore from '../store';
import MagentoRouter from '../Router';

/**
* @class
* @prop {Component} component
* @prop {Element} container
* @prop {ReactElement} element
* @prop {Store} store
* @prop {string} apiBase
* @prop {string} __tmp_webpack_public_path__
*/
class Peregrine {
/**
* Create a Peregrine instance.
*
* @param {Component} component The root component.
* @param {object} opts
* @param {string} opts.apiBase Base URL for the store's GraphQL API, including host/port
* @param {string} opts.__tmp_webpack_public_path__ Temporary until PWA module extends GraphQL API
*/
constructor(component) {
this.component = component;
constructor(opts = {}) {
const { apiBase = location.origin, __tmp_webpack_public_path__ } = opts;
this.apiBase = apiBase;
this.__tmp_webpack_public_path__ = __tmp_webpack_public_path__;
if (!__tmp_webpack_public_path__ && process.env.NODE_ENV === 'test') {
// Since __tmp_webpack_public_path__ is temporary, we're
// defaulting it here in tests to lessen tests that need to change
// when this property is removed
this.__tmp_webpack_public_path__ = 'https://temporary.com/pub';
}
this.store = createStore();
this.container = null;
this.element = null;
}

/**
Expand All @@ -29,13 +41,11 @@ class Peregrine {
* @returns {ReactElement}
*/
render() {
const { component: Component, store } = this;
const { store, apiBase, __tmp_webpack_public_path__ } = this;

return (
<Provider store={store}>
<BrowserRouter>
<Component />
</BrowserRouter>
<MagentoRouter {...{ apiBase, __tmp_webpack_public_path__ }} />
</Provider>
);
}
Expand All @@ -51,7 +61,7 @@ class Peregrine {
this.container = container;
this.element = this.render();

return render(this.element, ...arguments);
render(this.element, ...arguments);
}

/**
Expand Down
28 changes: 11 additions & 17 deletions packages/peregrine/src/Peregrine/__tests__/Peregrine.test.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import ReactDOM from 'react-dom';
import { createElement } from 'react';
import { configure, render } from 'enzyme';
import { configure, shallow } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

import Peregrine from '..';

configure({ adapter: new Adapter() });
jest.mock('react-dom');

const SimpleComponent = () => <i />;
configure({ adapter: new Adapter() });

beforeAll(() => {
for (const method of Object.keys(console)) {
Expand All @@ -21,11 +22,10 @@ afterAll(() => {
});

test('Constructs a new Peregrine instance', () => {
const received = new Peregrine(SimpleComponent);
const received = new Peregrine();

expect(received).toMatchObject(
expect.objectContaining({
component: SimpleComponent,
store: expect.objectContaining({
dispatch: expect.any(Function),
getState: expect.any(Function)
Expand All @@ -35,28 +35,22 @@ test('Constructs a new Peregrine instance', () => {
});

test('Renders a Peregrine instance', () => {
const app = new Peregrine(SimpleComponent);
const received = app.render();
const expected = <SimpleComponent />;

expect(render(received)).toEqual(render(expected));
const app = new Peregrine();
const wrapper = shallow(app.render());
expect(wrapper.find('MagentoRouter')).toHaveLength(1);
});

test('Mounts a Peregrine instance', () => {
const container = document.createElement('div');
const received = new Peregrine(SimpleComponent);
const expected = <SimpleComponent />;
const received = new Peregrine();

received.mount(container);

expect(received.container).toEqual(container);
expect(render(received.element)).toEqual(render(expected));
expect(container.firstChild.outerHTML).toEqual(render(expected).toString());
expect(ReactDOM.render).toHaveBeenCalledWith(expect.anything(), container);
});

test('Adds a reducer to the store', () => {
const expected = {};
const app = new Peregrine(SimpleComponent);
const app = new Peregrine();
const fooReducer = (state = null, { type }) =>
type === 'bar' ? expected : state;

Expand Down
68 changes: 68 additions & 0 deletions packages/peregrine/src/Router/MagentoRouteHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { createElement, Component } from 'react';
import { string, shape } from 'prop-types';
import resolveUnknownRoute from './resolveUnknownRoute';
import fetchRootComponent from './fetchRootComponent';

export default class MagentoRouteHandler extends Component {
static propTypes = {
apiBase: string.isRequired,
__tmp_webpack_public_path__: string.isRequired,
location: shape({
pathname: string.isRequired
}).isRequired
};

state = {};

componentDidMount() {
this.getRouteComponent(this.props.location.pathname);
}

componentWillReceiveProps(nextProps) {
const { location } = this.props;
const changed = nextProps.location.pathname !== location.pathname;
const seen = !!this.state[nextProps.location.pathname];

if (changed && !seen) {
this.getRouteComponent(nextProps.location.pathname);
}
}

getRouteComponent(pathname) {
const { apiBase, __tmp_webpack_public_path__ } = this.props;

resolveUnknownRoute({
route: pathname,
apiBase,
__tmp_webpack_public_path__
})
.then(({ rootChunkID, rootModuleID, matched }) => {
if (!matched) {
// TODO: User-defined 404 page
// when the API work is done to support it
throw new Error('404');
}
return fetchRootComponent(rootChunkID, rootModuleID);
})
.then(Component => {
this.setState({
[pathname]: Component
});
})
.catch(err => {
console.log('Routing resolve failed\n', err);
});
}

render() {
const { location } = this.props;
const Page = this.state[location.pathname];

if (!Page) {
// TODO (future iteration): User-defined loading content
return <div>Loading</div>;
}

return <Page />;
}
}
57 changes: 0 additions & 57 deletions packages/peregrine/src/Router/RenderComponentForRoute.js

This file was deleted.

Loading

0 comments on commit 275e23e

Please sign in to comment.