This component comes with no batteries included, but allows for a great deal of flexibility. It was initially designed for shopping cart type of functionality and product collections (i.e., for creating and editing orders), but is likely to be applicable in other contexts where a selection of some kind is involved.
$ npm install react-shopping-cart-starter-kit
The following third-party components and assets are used in the examples: React-Bootstrap, Bootstrap, Griddle (griddle-react), React DnD, Font Awesome, and the Lato Font.
This example also shows how to implement notifications using the various onItem*
callbacks.
Add products to the cart by dragging the product thumbnail to the drop area.
Assign a unique id to each item used in your application. For demonstration, we will use the following key-value object with a catalog of five products in subsequent examples.
const myProducts = {
"product-1" : { "Name" : "Canned Unicorn Meat", "Price" : "9.99" },
"product-2" : { "Name" : "Disappearing Ink Pen", "Price" : "14.99" },
"product-3" : { "Name" : "USB Rocket Launcher", "Price" : "29.99" },
"product-4" : { "Name" : "Airzooka Air Gun", "Price" : "29.99" },
"product-5" : { "Name" : "Star Trek Paper Clips", "Price" : "19.99" }
}
To get started, we print out the products in a list, pass the column names to the cart component, and implement an onClick
handler which adds the selected item to the cart by calling addItem
.
import React from 'react'
import Cart from 'react-shopping-cart-starter-kit'
const myProducts = {
"product-1" : { "Name" : "Canned Unicorn Meat", "Price" : "9.99" },
"product-2" : { "Name" : "Disappearing Ink Pen", "Price" : "14.99" },
"product-3" : { "Name" : "USB Rocket Launcher", "Price" : "29.99" },
"product-4" : { "Name" : "Airzooka Air Gun", "Price" : "29.99" },
"product-5" : { "Name" : "Star Trek Paper Clips", "Price" : "19.99" }
}
const MyComponent = React.createClass({
submit() {
const selection = this.refs.cart.getSelection()
alert(JSON.stringify(selection))
},
addItem(key) {
this.refs.cart.addItem(key, 1, this.props.products[key])
},
render() {
const products = this.props.products
return (
<div>
<h4>Products</h4>
<ul>
{Object.keys(products).map(key => {
return (
<li key={key}>
<a href='#' onClick={() => this.addItem(key)}>
{products[key]['Name']}
</a>
</li>
)
})}
</ul>
<hr />
<Cart ref='cart' columns={['Name', 'Price']} />
<hr />
<button onClick={this.submit}>
Submit
</button>
</div>
)
}
})
React.render(
<MyComponent products={myProducts} />,
document.getElementById('main')
)
Next, we'll implement a row iterator to sum up the order total. It will appear in the table footer (See 'Customization' for details).
<Cart iterator={this.rowIterator} ref='cart' columns={['Name', 'Price']} />
This function is first called once to allow initialization, and then for each item in the cart. The object we return is being passed on as an argument to the subsequent call, together with the row item.
rowIterator(context, row) {
if (!context) {
/* Initialization call */
return {
total : 0
}
} else {
/* Invoked once for each row */
const price = Number(row.data['Price'])
return {
total : context.total + row.quantity * price
}
}
},
Finally, we'd like to have the submit button disappear when nothing is present in the cart. To achieve this, we introduce a canSubmit
flag.
<Cart
ref = 'cart'
onChange = {this.cartChanged}
iterator = {this.rowIterator}
columns = {['Name', 'Price']} />
<hr />
{this.state.canSubmit && (
<button onClick={this.submit}>
Submit
</button>
)}
Here are the implementations for cartChanged
and getInitialState
, which we add to MyComponent
.
getInitialState() {
return {
canSubmit : false
}
},
cartChanged() {
this.setState({
canSubmit : !this.refs.cart.isEmpty()
})
},
Up to this point, we have assumed that the cart is initially empty. When working with an existing order or selection, we can provide an array of items to the cart's selection
prop.
<Cart
/* ... as before ... */
selection = {[
{
id : 'product-2',
quantity : 15,
data : myProducts['product-2']
},
{
id : 'product-3',
quantity : 1,
data : myProducts['product-3']
}
]}
To allow the user to revert any changes back to the order's initial state, we add a button that triggers the cart's reset
method.
<button onClick={this.undoChanges}>
Undo changes
</button>
To make the submit button appear in edit mode, we add a call to cartChanged
after the component has mounted.
undoChanges() {
this.refs.cart.reset()
},
componentDidMount() {
this.cartChanged()
},
To change how the component renders the cart's contents, implement the containerComponent
and/or rowComponent
props. (See Customization)
Property | Type | Description |
---|---|---|
columns | Array | The columns used in the table of items currently in the cart. Items added to the cart should have keys matching the entries of this array. |
Property | Type | Description | Default |
---|---|---|---|
items | Object | Normally, you pass an item's data with the call to addItem . As an alternative, you can provide an object here, mapping each key to an object with the item's attributes. |
|
selection | Array | Initial selection. (Used when editing an existing order or selection of items). | [] |
onItemAdded | Function | Called when an item is added to the cart. | () => {} |
onItemRemoved | Function | Called when an item is removed from the cart. | () => {} |
onItemQtyChanged | Function | Called when an item's quantity has changed. | () => {} |
onChange | Function | Called when the state of the component changes. (You may want to implement this callback to toggle the visibility of a submit button, based on whether the cart is empty or not.) | () => {} |
iterator | Function | A function used to pass state between rows. The real raison d'être for this function is to sum up the price of each product in an order and output a total in the footer. | () => { ... } |
containerComponent | Component | A custom container component. | See 'Customization' |
rowComponent | Component | A custom row component. | See 'Customization' |
tableClassName | String | The CSS class name to apply to the table element. Whether this value is actually used or not depends on the implementation of containerComponent . |
|
cartEmptyMessage | Node | A message shown when the cart is empty. | 'The cart is empty.' |
When editing an existing selection, use the selection
prop to pass the collection as an array in the following format.
const orderData = [
{
"id" : "item-1",
"quantity" : 2,
"data" : { "name": "Canned Unicorn Meat", "price" : "9.99" }
},
{
"id" : "item-2",
"quantity" : 1,
"data" : { "name": "Disappearing Ink Pen", "price" : "14.99" }
}
]
To add an item to the cart, provide its id, a quantity, and the item itself. (The third argument may not be required if you have previously supplied an object to the component's items
props.)
cart.addItem('product-1', 1, myProducts['product-1'])
If an item with the given id already exists in the cart, no new item is inserted. Instead, the quantity is adjusted accordingly for the existing entry.
cart.addItem('product-1', 1, myProducts['product-1'])
cart.addItem('product-1', 1)
cart.getSelection()
[
{
"id" : "product-1",
"quantity" : 2,
"data" : { "name": "Canned Unicorn Meat", "price" : "9.99" }
}
]
Clears the cart.
Does the same as emptyCart()
, unless you specify the selection
prop, in which case the initial selection will be restored. That is, when editing an existing order, this method will revert the cart back to a state consistent with the order being edited.
Return the current selection.
[
{
"id" : "item-1",
"quantity" : 2,
"data" : { "name": "Canned Unicorn Meat", "price" : "9.99" }
},
{
"id" : "item-2",
"quantity" : 1,
"data" : { "name": "Disappearing Ink Pen", "price" : "14.99" }
}
]
Returns true
if the cart is empty, otherwise false
.
In a typical implementation, it is not necessary to call this method directly. Instead, use this.props.removeItem
from within the row component.
In a typical implementation, it is not necessary to call this method directly. Instead, use this.props.setItemQty
from within the row component.
To gain more control over how the cart is rendered (beyond what can be done with CSS), it is possible to implement a custom row and/or container component.
<Cart
rowComponent = {MyRowComponent}
containerComponent = {MyContainerComponent}
/>
The row component renders individual rows. The default implementation uses a <table>
element at the top, and thus row data appears within a <tr>
tag, however these could be pretty much anything, as long as the node tree follows the normal JSX rules.
Property | Type | Description |
---|---|---|
item | Object | The row item (see below). |
columns | Array | The array of column names. |
removeItem | Function | Callback to invoke to remove the item from the cart. |
setItemQty | Function | Callback to invoke to change the selected quantity of an item. |
This object holds the id, current quantity of the item, and its properties (the data you specified when calling addItem
). The format is as follows.
{
"id" : "item-1",
"quantity" : 2,
"data" : { "name": "Canned Unicorn Meat", "price" : "9.99" }
}
const RowComponent = React.createClass({
handleChange(event) {
const value = event.target.value
if (!isNaN(value) && value > 0) {
this.props.setItemQty(value)
}
},
render() {
return (
<tr>
{this.props.columns.map(column => {
return (
<td key={column}>
{this.props.item.data[column]}
</td>
)
})}
<td>
<input
style = {{textAlign: 'right', width: '100px'}}
type = 'number'
value = {this.props.item.quantity}
onChange = {this.handleChange} />
</td>
<td>
<button
onClick = {this.props.removeItem}>
Remove
</button>
</td>
</tr>
)
}
})
This component is responsible for rendering the container in which the cart's contents appear. For more creative layouts, you may want to use something other than a <table>
here.
Property | Type | Description |
---|---|---|
columns | Array | The array of column names. |
tableClassName | String | A CSS class name to be applied to the table element. (May be ignored by custom implementations.) |
body | Node | The node tree generated by the row component. Typically rendered in the body portion of a table. |
context | Object | A state object generated by the rowIterator , if one is used. |
const ContainerComponent = React.createClass({
render() {
return (
<table className={this.props.tableClassName}>
<thead>
<tr>
{this.props.columns.map(column => {
return (
<th key={column}>
{column}
</th>
)
})}
<th>
Quantity
</th>
<th />
</tr>
</thead>
<tbody>
{this.props.body}
</tbody>
{this.props.context.total && (
<tfoot>
<tr>
<td colSpan={this.props.columns.length-1} style={{textAlign: 'right'}}>
<strong>Total:</strong>
</td>
<td colSpan={3}>
{this.props.context.total.toFixed(2)}
</td>
</tr>
</tfoot>
)}
</table>
)
}
})
In this example, we create a row component to "bootstrapify" the table. See examples for a more complete implementation.
const BootstrapRowComponent = React.createClass({
handleChange(event) {
const value = event.target.value
if (!isNaN(value) && value > 0) {
this.props.setItemQty(value)
}
},
increment() {
const value = this.props.item.quantity + 1
this.props.setItemQty(value)
},
decrement() {
const value = this.props.item.quantity - 1
if (value) {
this.props.setItemQty(value)
}
},
render() {
return (
<tr>
{this.props.columns.map(column => {
return (
<td key={column}>
{this.props.item.data[column]}
</td>
)
})}
<td>
<div className='input-group input-group-sm' style={{maxWidth: '110px'}}>
<span className='input-group-btn'>
<button
className = 'btn btn-default btn-sm'
onClick = {this.decrement}>
<i className='fa fa-minus' />
</button>
</span>
<input
style = {{textAlign: 'right'}}
className = 'form-control'
type = 'text'
value = {this.props.item.quantity}
onChange = {this.handleChange} />
<span className='input-group-btn'>
<button
onClick = {this.increment}
className = 'btn btn-default btn-sm'>
<i className='fa fa-plus' />
</button>
</span>
</div>
</td>
<td>
<button
className = 'btn btn-default btn-sm'
onClick = {this.props.removeItem}>
<i className='fa fa-remove' />
</button>
</td>
</tr>
)
}
})
const MyComponent = React.createClass({
/*
Code left out for brevity. Copy this portion from previous examples.
*/
render() {
const products = this.props.products
return (
<div>
<h4>Products</h4>
<ul>
{Object.keys(products).map(key => {
return (
<li key={key}>
<a href='#' onClick={() => this.addItem(key)}>
{products[key]['Name']}
</a>
</li>
)
})}
</ul>
<hr />
<Cart
ref = 'cart'
tableClassName = 'table'
rowComponent = {BootstrapRowComponent}
onChange = {this.cartChanged}
iterator = {this.rowIterator}
columns = {['Name', 'Price']} />
<hr />
{this.state.canSubmit && (
<button
className = 'btn btn-block btn-default btn-primary btn-sm'
onClick = {this.submit}>
Submit
</button>
)}
</div>
)
}
})
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
<title></title>
</head>
<body>
<div id="main"></div>
<script src="bundle.js"></script>
</body>
</html>
This software is provided under the terms and conditions of the BSD License.