Skip to content
This repository was archived by the owner on Jun 19, 2018. It is now read-only.

Commit ef4e112

Browse files
committed
Merge branch 'master' into zetlen/rich-content-component
2 parents 8bfa3c2 + 99e6ac4 commit ef4e112

File tree

11 files changed

+368
-227
lines changed

11 files changed

+368
-227
lines changed

package-lock.json

Lines changed: 136 additions & 99 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@magento/peregrine",
3-
"version": "0.2.1",
3+
"version": "0.4.0",
44
"description": "The core runtime of Magento PWA",
55
"license": "(OSL-3.0 OR AFL-3.0)",
66
"author": "Magento Commerce",
@@ -56,6 +56,7 @@
5656
"eslint-plugin-jsx-a11y": "^6.0.3",
5757
"eslint-plugin-react": "^7.5.1",
5858
"execa": "^0.10.0",
59+
"intl": "^1.2.5",
5960
"jest": "^22.4.3",
6061
"jest-fetch-mock": "^1.4.1",
6162
"npm-merge-driver": "^2.3.5",

src/Peregrine/Peregrine.js

Lines changed: 24 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,36 @@
11
import { createElement } from 'react';
2-
import { Provider } from 'react-redux';
3-
import { render } from 'react-dom';
2+
import { Provider as ReduxProvider } from 'react-redux';
43
import createStore from '../store';
54
import MagentoRouter from '../Router';
65

76
/**
8-
* @class
9-
* @prop {Element} container
10-
* @prop {ReactElement} element
11-
* @prop {Store} store
12-
* @prop {string} apiBase
13-
* @prop {string} __tmp_webpack_public_path__
7+
*
8+
* @param {string} apiBase Absolute URL pointing to the GraphQL endpoint
9+
* @param {string} __tmp_webpack_public_path__ Temporary hack. Expects the `__webpack_public_path__` value
10+
* @returns {{ store: Store, Provider: () => JSX.Element }}
1411
*/
15-
class Peregrine {
16-
/**
17-
* Create a Peregrine instance.
18-
* @param {object} opts
19-
* @param {string} opts.apiBase Base URL for the store's GraphQL API, including host/port
20-
* @param {string} opts.__tmp_webpack_public_path__ Temporary until PWA module extends GraphQL API
21-
*/
22-
constructor(opts = {}) {
23-
const { apiBase = location.origin, __tmp_webpack_public_path__ } = opts;
24-
this.apiBase = apiBase;
25-
this.__tmp_webpack_public_path__ =
26-
__tmp_webpack_public_path__ &&
27-
ensureDirURI(__tmp_webpack_public_path__);
28-
if (!__tmp_webpack_public_path__ && process.env.NODE_ENV === 'test') {
29-
// Since __tmp_webpack_public_path__ is temporary, we're
30-
// defaulting it here in tests to lessen tests that need to change
31-
// when this property is removed
32-
this.__tmp_webpack_public_path__ = 'https://temporary.com/pub/';
33-
}
34-
this.store = createStore();
35-
this.container = null;
36-
this.element = null;
37-
}
38-
39-
/**
40-
* Create an instance of the root component, wrapped with store and routing
41-
* components.
42-
*
43-
* @returns {ReactElement}
44-
*/
45-
render() {
46-
const { store, apiBase, __tmp_webpack_public_path__ } = this;
47-
48-
return (
49-
<Provider store={store}>
50-
<MagentoRouter {...{ apiBase, __tmp_webpack_public_path__ }} />
51-
</Provider>
12+
export default function bootstrap({ apiBase, __tmp_webpack_public_path__ }) {
13+
// Remove deprecation warning after 2 version bumps
14+
if (process.env.NODE_ENV !== 'production' && this instanceof bootstrap) {
15+
throw new Error(
16+
'The API for Peregrine has changed. ' +
17+
'Please see the Release Notes on Github ' +
18+
'for instructions to update your application'
5219
);
5320
}
5421

55-
/**
56-
* Render and mount the React tree into a DOM element.
57-
*
58-
* @param {Element} container The target DOM element.
59-
* @param {Function} callback A function called after mounting.
60-
* @returns {void}
61-
*/
62-
mount(container) {
63-
this.container = container;
64-
this.element = this.render();
65-
66-
render(this.element, ...arguments);
67-
}
68-
69-
/**
70-
* Add a reducer (slice) to the store (root).
71-
* The store replaces the root reducer with one containing the new slice.
72-
*
73-
* @param {String} key The name of the slice.
74-
* @param {Function} reducer The reducing function.
75-
* @returns {void}
76-
*/
77-
addReducer(key, reducer) {
78-
this.store.addReducer(key, reducer);
79-
}
22+
const store = createStore();
23+
const routerProps = {
24+
apiBase,
25+
__tmp_webpack_public_path__: ensureDirURI(__tmp_webpack_public_path__)
26+
};
27+
const Provider = () => (
28+
<ReduxProvider store={store}>
29+
<MagentoRouter {...routerProps} />
30+
</ReduxProvider>
31+
);
32+
33+
return { store, Provider };
8034
}
8135

8236
/**
@@ -86,5 +40,3 @@ class Peregrine {
8640
function ensureDirURI(uri) {
8741
return uri.endsWith('/') ? uri : uri + '/';
8842
}
89-
90-
export default Peregrine;
Lines changed: 22 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
import ReactDOM from 'react-dom';
21
import { createElement } from 'react';
32
import { configure, shallow } from 'enzyme';
43
import Adapter from 'enzyme-adapter-react-16';
5-
6-
import Peregrine from '..';
7-
8-
jest.mock('react-dom');
4+
import { Provider as ReduxProvider } from 'react-redux';
5+
import MagentoRouter from '../../Router';
6+
import bootstrap from '..';
97

108
configure({ adapter: new Adapter() });
119

@@ -25,59 +23,29 @@ afterAll(() => {
2523
}
2624
});
2725

28-
test('Constructs a new Peregrine instance', () => {
29-
const received = new Peregrine();
30-
31-
expect(received).toMatchObject(
32-
expect.objectContaining({
33-
store: expect.objectContaining({
34-
dispatch: expect.any(Function),
35-
getState: expect.any(Function)
36-
})
37-
})
38-
);
26+
test('Throws descriptive error when using former API', () => {
27+
const fn = () => new bootstrap({});
28+
expect(fn).toThrowError(/The API for Peregrine has changed/);
3929
});
4030

41-
test('Renders a Peregrine instance', () => {
42-
const app = new Peregrine();
43-
const wrapper = shallow(app.render());
44-
expect(wrapper.find('MagentoRouter')).toHaveLength(1);
45-
});
46-
47-
test('Mounts a Peregrine instance', () => {
48-
const container = document.createElement('div');
49-
const received = new Peregrine();
50-
51-
received.mount(container);
52-
expect(ReactDOM.render).toHaveBeenCalledWith(expect.anything(), container);
53-
});
54-
55-
test('Adds a reducer to the store', () => {
56-
const expected = {};
57-
const app = new Peregrine();
58-
const fooReducer = (state = null, { type }) =>
59-
type === 'bar' ? expected : state;
60-
61-
app.addReducer('foo', fooReducer);
62-
const received = app.store.getState().foo;
63-
expect(received).toBe(null);
64-
65-
app.store.dispatch({ type: 'bar' });
66-
const next = app.store.getState().foo;
67-
expect(next).toBe(expected);
68-
});
69-
70-
// https://github.com/magento-research/venia-pwa-concept/pull/57
71-
test('__tmp_webpack_public_path__ is normalized to include a trailing / when not present', () => {
72-
const app = new Peregrine({
73-
__tmp_webpack_public_path__: 'https://foo.bar/test'
31+
test('Exposes the Redux store', () => {
32+
const { store } = bootstrap({
33+
apiBase: '/graphql',
34+
__tmp_webpack_public_path__: 'foobar'
35+
});
36+
expect(store).toMatchObject({
37+
dispatch: expect.any(Function),
38+
getState: expect.any(Function),
39+
addReducer: expect.any(Function)
7440
});
75-
expect(app.__tmp_webpack_public_path__).toBe('https://foo.bar/test/');
7641
});
7742

78-
test('Does not double up trailing slash in __tmp_webpack_public_path__ when one exists', () => {
79-
const app = new Peregrine({
80-
__tmp_webpack_public_path__: 'https://foo.bar/test/'
43+
test('Provider includes Redux + Router', () => {
44+
const { Provider } = bootstrap({
45+
apiBase: '/graphql',
46+
__tmp_webpack_public_path__: 'foobar'
8147
});
82-
expect(app.__tmp_webpack_public_path__).toBe('https://foo.bar/test/');
48+
const wrapper = shallow(<Provider />);
49+
expect(wrapper.find(ReduxProvider).length).toBe(1);
50+
expect(wrapper.find(MagentoRouter).length).toBe(1);
8351
});

src/Price/Price.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { createElement, PureComponent, Fragment } from 'react';
2+
import { number, string, shape } from 'prop-types';
3+
4+
export default class Price extends PureComponent {
5+
static propTypes = {
6+
value: number.isRequired,
7+
currencyCode: string.isRequired,
8+
classes: shape({
9+
currency: string,
10+
integer: string,
11+
decimal: string,
12+
fraction: string
13+
})
14+
};
15+
16+
static defaultProps = {
17+
classes: {}
18+
};
19+
20+
render() {
21+
const { value, currencyCode, classes } = this.props;
22+
const parts = Intl.NumberFormat(undefined, {
23+
style: 'currency',
24+
currency: currencyCode
25+
}).formatToParts(value);
26+
27+
return (
28+
<Fragment>
29+
{parts.map((part, i) => {
30+
const partClass = classes[part.type];
31+
const key = `${i}-${part.value}`;
32+
return (
33+
<span key={key} className={partClass}>
34+
{part.value}
35+
</span>
36+
);
37+
})}
38+
</Fragment>
39+
);
40+
}
41+
}

src/Price/__docs__/Price.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Price
2+
3+
The `Price` component is used anywhere a price is rendered in PWA Studio.
4+
5+
Formatting of prices and currency symbol selection is handled entirely by the ECMAScript Internationalization API available in modern browsers. A [polyfill](https://www.npmjs.com/package/intl) will need to be loaded for any JavaScript runtime that does not have [`Intl.NumberFormat.prototype.formatToParts`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat/formatToParts).
6+
7+
## Usage
8+
9+
```jsx
10+
import Price from '@peregrine/Price';
11+
import cssModule from './my-pricing-styles';
12+
13+
<Price value={100.99} currencyCode="USD" classes={cssModule} />;
14+
/*
15+
<span className="curr">$</span>
16+
<span className="int">88</span>
17+
<span className="dec">.</span>
18+
<span className="fract">81</span>
19+
*/
20+
```
21+
22+
## Props
23+
24+
| Prop Name | Required? | Description |
25+
| -------------- | :-------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
26+
| `classes` || A classname object. |
27+
| `value` || Numeric price |
28+
| `currencyCode` || A string of with any currency code supported by [`Intl.NumberFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat) |

src/Price/__stories__/Price.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { createElement } from 'react';
2+
import { storiesOf } from '@storybook/react';
3+
import { withReadme } from 'storybook-readme';
4+
import Price from '../Price';
5+
import docs from '../__docs__/Price.md';
6+
7+
const stories = storiesOf('Price', module);
8+
9+
stories.add(
10+
'USD',
11+
withReadme(docs, () => <Price value={100.99} currencyCode="USD" />)
12+
);
13+
14+
stories.add(
15+
'EUR',
16+
withReadme(docs, () => <Price value={100.99} currencyCode="EUR" />)
17+
);
18+
19+
stories.add(
20+
'JPY',
21+
withReadme(docs, () => <Price value={100.99} currencyCode="JPY" />)
22+
);
23+
24+
stories.add(
25+
'Custom Styles',
26+
withReadme(docs, () => {
27+
const classes = {
28+
currency: 'curr',
29+
integer: 'int',
30+
decimal: 'dec',
31+
fraction: 'fract'
32+
};
33+
return (
34+
<div>
35+
<style>{`
36+
.curr { color: green; font-weight: bold; }
37+
.int { color: red; }
38+
.dec { color: black; }
39+
.fract { color: blue; }
40+
`}</style>
41+
<Price value={100.99} currencyCode="USD" classes={classes} />
42+
</div>
43+
);
44+
})
45+
);

0 commit comments

Comments
 (0)