Skip to content

Commit

Permalink
feat: new AddressChoice component
Browse files Browse the repository at this point in the history
  • Loading branch information
aldeed committed Jan 8, 2019
1 parent 3cea443 commit 772e573
Show file tree
Hide file tree
Showing 10 changed files with 495 additions and 14 deletions.
133 changes: 133 additions & 0 deletions package/src/components/AddressChoice/v1/AddressChoice.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import { withComponents } from "@reactioncommerce/components-context";
import { addressToString, CustomPropTypes } from "../../../utils";

class AddressChoice extends Component {
static propTypes = {
/**
* A list of addresses to show for selection
*/
addresses: CustomPropTypes.addressBook,
/**
* You can provide a `className` prop that will be applied to the outermost DOM element
* rendered by this component. We do not recommend using this for styling purposes, but
* it can be useful as a selector in some situations.
*/
className: PropTypes.string,
/**
* If you've set up a components context using
* [@reactioncommerce/components-context](https://github.com/reactioncommerce/components-context)
* (recommended), then this prop will come from there automatically. If you have not
* set up a components context or you want to override one of the components in a
* single spot, you can pass in the components prop directly.
*/
components: PropTypes.shape({
/**
* Pass either the Reaction AddressForm component or your own component that
* accepts compatible props.
*/
AddressForm: CustomPropTypes.component.isRequired,
/**
* A reaction SelectableList component or compatible component.
*/
SelectableList: CustomPropTypes.component.isRequired
}),
/**
* Disable editing?
*/
isReadOnly: PropTypes.bool,
/**
* Called with an address whenever the selected or entered
* address changes. If they selected one, it will be the
* complete address that was passed in `addresses`. If they're
* entering one, it will be whatever they have entered so far
* and may be partial.
*/
onChange: PropTypes.func,
/**
* The label for the "Use a different address" selection item, if it
* is shown.
*/
otherAddressLabel: PropTypes.string
};

static defaultProps = {
isReadOnly: false,
onChange() {},
otherAddressLabel: "Use a different address"
};

constructor(props) {
super(props);

let selectedOption = "OTHER";
if (Array.isArray(props.addresses) && props.addresses.length > 0) {
selectedOption = "0";
}

this.state = { selectedOption };
}

handleChangeAddress = (address) => {
this.props.onChange(address);
}

handleChangeSelection = (selectedOption) => {
const { addresses } = this.props;

this.setState({ selectedOption });

if (selectedOption !== "OTHER" && Array.isArray(addresses)) {
this.props.onChange(addresses[Number(selectedOption)]);
}
}

renderSelectList() {
const {
addresses,
components: { SelectableList },
isReadOnly,
otherAddressLabel
} = this.props;
const { selectedOption } = this.state;

if (!Array.isArray(addresses) || addresses.length === 0) return null;

const listOptions = addresses.map((address, index) => ({
id: String(index),
label: addressToString(address, { includeFullName: true }),
value: String(index)
}));

listOptions.push({
id: "OTHER",
label: otherAddressLabel,
value: "OTHER"
});

return (
<SelectableList
name="addressList"
isReadOnly={isReadOnly}
onChange={this.handleChangeSelection}
options={listOptions}
value={selectedOption}
/>
);
}

render() {
const { className, components: { AddressForm }, isReadOnly } = this.props;
const { selectedOption } = this.state;

return (
<div className={className}>
{this.renderSelectList()}
{selectedOption === "OTHER" && <AddressForm isReadOnly={isReadOnly} onChange={this.handleChangeAddress} />}
</div>
);
}
}

export default withComponents(AddressChoice);
84 changes: 84 additions & 0 deletions package/src/components/AddressChoice/v1/AddressChoice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
### Overview

The `AddressChoice` component is a way of collecting an address while giving an option of choosing an already known address. If no already known addresses are provided, it will render an [AddressForm](./#!/AddressForm). Otherwise it will render a choice list where each address is in the list and the final option is to enter a new address.

### Usage

#### Simple
If you don't pass in any `addresses`, it renders an [AddressForm](./#!/AddressForm).

```jsx
<AddressChoice onChange={console.log.bind(console)} />
```

#### Read Only
Use `isReadOnly` prop to disable editing, such as when submitting the form or loading something.

```jsx
<AddressChoice isReadOnly />
```

#### With Addresses
When you provide one or more `addresses`, they are presented in a [SelectableList](./#!/SelectableList) along with a final option that shows the [AddressForm](./#!/AddressForm) when selected.

`onChange` prop is called whenever the address changes, whether by selecting an existing address or changing a field in the custom address form.

```jsx
const addresses = [
{
_id: "20",
address1: "7742 Hwy 23",
address2: "",
country: "US",
city: "Belle Chasse",
fullName: "Salvos Seafood",
postal: "70037",
region: "LA",
phone: "(504) 393-7303"
},
{
_id: "21",
address1: "35 Akin Adesola St",
address2: "",
country: "NG",
city: "Lagos",
fullName: "Ocean Basket Victoria Island",
postal: "101241",
region: "Victoria Island",
phone: "234 816 059 1821"
}
];

<AddressChoice addresses={addresses} onChange={console.log.bind(console)} />
```

#### Read Only With Addresses

```jsx
const addresses = [
{
_id: "20",
address1: "7742 Hwy 23",
address2: "",
country: "US",
city: "Belle Chasse",
fullName: "Salvos Seafood",
postal: "70037",
region: "LA",
phone: "(504) 393-7303"
},
{
_id: "21",
address1: "35 Akin Adesola St",
address2: "",
country: "NG",
city: "Lagos",
fullName: "Ocean Basket Victoria Island",
postal: "101241",
region: "Victoria Island",
phone: "234 816 059 1821"
}
];

<AddressChoice addresses={addresses} isReadOnly onChange={console.log.bind(console)} />
```
101 changes: 101 additions & 0 deletions package/src/components/AddressChoice/v1/AddressChoice.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import React from "react";
import renderer from "react-test-renderer";
import { mount } from "enzyme";
import { ComponentsProvider } from "@reactioncommerce/components-context";
import mockComponents from "../../../tests/mockComponents";
import realComponents from "../../../tests/realComponents";
import AddressChoice from "./AddressChoice";

const addresses = [
{
_id: "20",
address1: "7742 Hwy 23",
address2: "",
country: "US",
city: "Belle Chasse",
fullName: "Salvos Seafood",
postal: "70037",
region: "LA",
phone: "(504) 393-7303"
},
{
_id: "21",
address1: "35 Akin Adesola St",
address2: "",
country: "NG",
city: "Lagos",
fullName: "Ocean Basket Victoria Island",
postal: "101241",
region: "Victoria Island",
phone: "234 816 059 1821"
}
];

test("simple snapshot", () => {
const component = renderer.create(<AddressChoice components={mockComponents} />);

const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});

test("simple snapshot read only", () => {
const component = renderer.create(<AddressChoice components={mockComponents} isReadOnly />);

const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});

test("calls onChange when a field changes", () => {
const onChange = jest.fn();

const wrapper = mount((
<ComponentsProvider value={realComponents}>
<AddressChoice onChange={onChange} />
</ComponentsProvider>
));

onChange.mockClear();

wrapper.find('input[name="address1"]').simulate("blur", { target: { value: "FOO" } });

expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenLastCalledWith({
address1: "FOO",
address2: null,
addressName: "",
city: null,
country: null,
fullName: null,
isCommercial: false,
phone: null,
postal: null,
region: null
});
});

test("snapshot with addresses", () => {
const component = renderer.create(<AddressChoice components={mockComponents} addresses={addresses} />);

const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});

test("calls onChange on mount and when a different provided address is selected", () => {
const onChange = jest.fn();

const wrapper = mount((
<ComponentsProvider value={realComponents}>
<AddressChoice addresses={addresses} onChange={onChange} />
</ComponentsProvider>
));

expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenLastCalledWith(addresses[0]);

onChange.mockClear();

wrapper.find('input[value="1"]').simulate("change", { target: { checked: true } });

expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenLastCalledWith(addresses[1]);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`simple snapshot 1`] = `
<div
className={undefined}
>
AddressForm({"isReadOnly":false})
</div>
`;

exports[`simple snapshot read only 1`] = `
<div
className={undefined}
>
AddressForm({"isReadOnly":true})
</div>
`;

exports[`snapshot with addresses 1`] = `
<div
className={undefined}
>
SelectableList({"name":"addressList","isReadOnly":false,"options":"[Object]","value":"0"})
</div>
`;
1 change: 1 addition & 0 deletions package/src/components/AddressChoice/v1/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./AddressChoice";
Loading

0 comments on commit 772e573

Please sign in to comment.