Creating an Invoice Component with Dynamic Line Items using React
This post walks through the steps to creating an Invoice component in React that supports adding + removing line items and features automatic calculation of totals as a user inputs values.
The source code to follow along with is available on github at: https://github.com/firxworx/react-simple-invoice
A live demo can be viewed at: https://demo.firxworx.com/react-simple-invoice
I use SCSS Modules for styling but you could easily refactor the code to use your favourite method for styling components.
SCSS Modules are an easy choice because the latest v2 of create-react-app
(released Oct 1 2018) introduces out-of-the-box support for CSS Modules that can be written in CSS (default) or SASS/SCSS with the addition of the node-sass
package. Version 1 required users to manually customize their webpack configuration if they wanted to use CSS Modules.
The code is relevant to React v16.6.3.
Project Setup
This project is based on the create-react-app
starter. To get started with the yarn package manager:
yarn create react-app react-simple-invoice
The following dependencies are installed:
yarn add node-sass
yarn add react-icons
The create-react-app boilerplate can then be customized to use sass modules: all .css
files are renamed to .scss
and the .module.scss
suffix filename convention is applied where applicable.
I added a bare-bones global stylesheet in styles/index.scss
where I import Normalize.css (as _normalize.scss
).
All of the component styles assume box-sizing border-box
and that normalize.css is in place.
Implementing an Invoice Component
The most significant part of an Invoice component are arguably the line items that can be added and removed. The following provides an overview for how this functionality is implemented:
Initial scaffolding
Start by creating components/Invoice.js
and components/Invoice.modules.scss
.
Tear up the initial Invoice component as a class-based component. Import a couple helpful icons from react-icons
and the Invoice
scss module:
import React, { Component } from 'react'
import { MdAddCircle as AddIcon, MdCancel as DeleteIcon } from 'react-icons/md'
import styles from './Invoice.module.scss'
class Invoice extends Component {
locale = 'en-US'
currency = 'USD'
render = () => {
return (
<div><h1>I am an Invoice</h1></div>
)
}
}
export default Invoice
The locale and currency are stored in the class for the sake of example. In a broader app, these might be injected as props and/or come in from a context or global state.
React will move towards functional components across the board in upcoming versions. However, for now, class-based components still reign for interactive/dynamic components that maintain their own state.
Define state
The Invoice’s state maintains a tax rate and an array of line item objects that have the following properties: name, description, quantity, and price.
Define the initial state with a 0% tax rate and a single blank line item:
state = {
taxRate: 0.00,
lineItems: [
{
name: '',
description: '',
quantity: 0,
price: 0.00,
},
]
}
Displaying line items
Inside the component’s render()
method, JSX is used to display each line item reflected in the component’s state.
The Array map()
function is used to iterate over each line item.
The key
for each line item is simply set to its index in the state array. For more information on the necessity of keys in React, refer to the docs regarding Lists and Keys.
Each form input element is created as a Controlled Component. This means that React completely controls the element’s state (including whatever value is currently being stored by the form element Component) rather than leaving this to the element itself. To accomplish this, each input specifies an onChange
event handler whose job it is to update the component’s state every time a user changes the value of an input.
Each input’s value is set to its corresponding value in the Invoice’s state.
The various styles and functions referenced will be implemented next:
{this.state.lineItems.map((item, i) => (
<div className={`${styles.row} ${styles.editable}`} key={i}>
<div>{i+1}</div>
<div><input name="name" type="text" value={item.name} onChange={this.handleLineItemChange(i)} /></div>
<div><input name="description" type="text" value={item.description} onChange={this.handleLineItemChange(i)} /></div>
<div><input name="quantity" type="number" step="1" value={item.quantity} onChange={this.handleLineItemChange(i)} onFocus={this.handleFocusSelect} /></div>
<div className={styles.currency}><input name="price" type="number" step="0.01" min="0.00" max="9999999.99" value={item.price} onChange={this.handleLineItemChange(i)} onFocus={this.handleFocusSelect} /></div>
<div className={styles.currency}>{this.formatCurrency( item.quantity * item.price )}</div>
<div>
<button type="button"
className={styles.deleteItem}
onClick={this.handleRemoveLineItem(i)}
><DeleteIcon size="1.25em" /></button>
</div>
</div>
))}
Implement onChange handler
When a user types a value into an input, the onChange
event fires and the handleLineItemChange(elementIndex)
function is called.
The Invoice’s state is updated to reflect the input’s latest value:
handleLineItemChange = (elementIndex) => (event) => {
let lineItems = this.state.lineItems.map((item, i) => {
if (elementIndex !== i) return item
return {...item, [event.target.name]: event.target.value}
})
this.setState({lineItems})
}
The handleLineItemChange()
handler accepts an elementIndex
param that corresponds to the line item’s position in the lineItems
array. As an event handler, the function is also passed an event
object.
The Invoice’s state is updated by creating a new version of the lineItems
array. The new version features a line item object and property (name, description, quantity, price) modified to correspond to the changed input’s new value. The this.setState()
function is then called to update the Invoice component with the updated state.
The new array is created by calling map()
on the this.state.lineItems
‘s Array and passing a function that updates the appropriate value.
As map()
loops through each element, our function checks if that element’s index matches that of the input that triggered handleLineItemChange()
. When it matches, an updated version of the line item is returned. When it doesn’t match, the line item is returned as-is.
The implementation works because the name of each form input input (available as event.target.name
) corresponds to a the property name of the line item.
Implement onFocus Handler
It is sometimes convenient for users to have an input automatically select its entire value whenever it receives focus.
I think this applies to the quantity and price inputs so I added an onFocus
handler called onFocusSelect()
. It is implemented as follows:
handleFocusSelect = (event) => {
event.target.select()
}
Implement Handler for Adding a Line Item
When the “Add Line Item” button is clicked, the onClick()
event calls the handleAddLineItem()
function.
A new line item is added to the Invoice by adding a new line item object to the component state’s lineItems
array.
The Array concat()
method is used to create a new array based on the current lineItems array. It concatenates a second array containing a new blank line item object. setState()
is then called to update the state.
handleAddLineItem = (event) => {
this.setState({
lineItems: this.state.lineItems.concat(
[{ name: '', description: '', quantity: 0, price: 0.00 }]
)
})
}
Implement Handler for Removing a Line Item
Each line item features a Delete button to remove it from the invoice.
Each Delete button’s onClick()
event calls this.handleRemoveLineItem(i)
where i
is the index of line item.
The Array filter()
method is used to return a new array that omits the object at the i
‘th position of the original array. this.setState()
updates the component state.
handleRemoveLineItem = (elementIndex) => (event) => {
this.setState({
lineItems: this.state.lineItems.filter((item, i) => {
return elementIndex !== i
})
})
}
Implement Calculation and Formatting Functions
The component implements a number of helper functions to calculate and format tax and total amounts:
formatCurrency = (amount) => {
return (new Intl.NumberFormat(this.locale, {
style: 'currency',
currency: this.currency,
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(amount))
}
calcTaxAmount = (c) => {
return c * (this.state.taxRate / 100)
}
calcLineItemsTotal = () => {
return this.state.lineItems.reduce((prev, cur) => (prev + (cur.quantity * cur.price)), 0)
}
calcTaxTotal = () => {
return this.calcLineItemsTotal() * (this.state.taxRate / 100)
}
calcGrandTotal = () => {
return this.calcLineItemsTotal() + this.calcTaxTotal()
}
Implement Styles
CSS Modules (or SCSS Modules in this case) are great for ensuring there are no naming conflicts in projects with multiple Components that might use the same class names.
The ComponentName.modules.scss
file looks and works just like any normal SCSS file except the classes are invoked in JSX slightly differently.
Notice the import line: import styles from './Invoice.module.scss'
To apply a give .example
style to a given component, you would refer to styles.example
in the className
prop:
<ExampleComponent className={styles.example}>
For multiple and/or conditional styles, ES6 strings + interpolation can be used to add additional expressions:
<ExampleComponent className={`${styles.example} ${styles.anotherExample}`} />
Refer to the repo on github to see how it all comes together.