Welcome folks today in this blog post we will be building a bootstrap 5 invoice pdf generator
in browser using jspdf
and html2canvas
library in javascript. All the full source code of the application is shown below.
Get Started
In order to get started you need to create a brand new react.js
project using the below command as shown below
npx create-react-app reactpdf
cd reactpdf
And now you need to install the below libraries
using the below command as shown below
npm i jspdf
npm i html2canvas
npm i filesaver.js
npm i react-icons
npm i bootstrap
npm i react- bootstrap
And now we need to see the final
directory structure of the react.js
app as shown below
And now we need to edit the App.js
file of the react.js
project as shown below
App.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import React, { Component } from 'react'; import 'bootstrap/dist/css/bootstrap.min.css'; import './App.css'; import Container from 'react-bootstrap/Container'; import InvoiceForm from './components/InvoiceForm'; class App extends Component { render() { return ( <div className="App d-flex flex-column align-items-center justify-content-center w-100"> <Container> <InvoiceForm/> </Container> </div> ); }} export default App; |
As you can see we are including the bootstrap components
from the react-bootstrap
library and then we are including the container
and invoiceform
components. Now we need to make the components
folder and inside it you need to make the InvoiceForm.js
file
components/InvoiceForm.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 |
import React from 'react'; import 'bootstrap/dist/css/bootstrap.min.css'; import Row from 'react-bootstrap/Row'; import Col from 'react-bootstrap/Col'; import Button from 'react-bootstrap/Button'; import Form from 'react-bootstrap/Form'; import Card from 'react-bootstrap/Card'; import InvoiceItem from './InvoiceItem'; import InvoiceModal from './InvoiceModal'; import InputGroup from 'react-bootstrap/InputGroup'; class InvoiceForm extends React.Component { constructor(props) { super(props); this.state = { isOpen: false, currency: '$', currentDate: '', invoiceNumber: 1, dateOfIssue: '', billTo: '', billToEmail: '', billToAddress: '', billFrom: '', billFromEmail: '', billFromAddress: '', notes: '', total: '0.00', subTotal: '0.00', taxRate: '', taxAmmount: '0.00', discountRate: '', discountAmmount: '0.00' }; this.state.items = [ { id: 0, name: '', description: '', price: '1.00', quantity: 1 } ]; this.editField = this.editField.bind(this); } componentDidMount(prevProps) { this.handleCalculateTotal() } handleRowDel(items) { var index = this.state.items.indexOf(items); this.state.items.splice(index, 1); this.setState(this.state.items); }; handleAddEvent(evt) { var id = (+ new Date() + Math.floor(Math.random() * 999999)).toString(36); var items = { id: id, name: '', price: '1.00', description: '', quantity: 1 } this.state.items.push(items); this.setState(this.state.items); } handleCalculateTotal() { var items = this.state.items; var subTotal = 0; items.map(function(items) { subTotal = parseFloat(subTotal + (parseFloat(items.price).toFixed(2) * parseInt(items.quantity))).toFixed(2) }); this.setState({ subTotal: parseFloat(subTotal).toFixed(2) }, () => { this.setState({ taxAmmount: parseFloat(parseFloat(subTotal) * (this.state.taxRate / 100)).toFixed(2) }, () => { this.setState({ discountAmmount: parseFloat(parseFloat(subTotal) * (this.state.discountRate / 100)).toFixed(2) }, () => { this.setState({ total: ((subTotal - this.state.discountAmmount) + parseFloat(this.state.taxAmmount)) }); }); }); }); }; onItemizedItemEdit(evt) { var item = { id: evt.target.id, name: evt.target.name, value: evt.target.value }; var items = this.state.items.slice(); var newItems = items.map(function(items) { for (var key in items) { if (key == item.name && items.id == item.id) { items[key] = item.value; } } return items; }); this.setState({items: newItems}); this.handleCalculateTotal(); }; editField = (event) => { this.setState({ [event.target.name]: event.target.value }); this.handleCalculateTotal(); }; onCurrencyChange = (selectedOption) => { this.setState(selectedOption); }; openModal = (event) => { event.preventDefault() this.handleCalculateTotal() this.setState({isOpen: true}) }; closeModal = (event) => this.setState({isOpen: false}); render() { return (<Form onSubmit={this.openModal}> <Row> <Col md={8} lg={9}> <Card className="p-4 p-xl-5 my-3 my-xl-4"> <div className="d-flex flex-row align-items-start justify-content-between mb-3"> <div class="d-flex flex-column"> <div className="d-flex flex-column"> <div class="mb-2"> <span className="fw-bold">Current Date: </span> <span className="current-date">{new Date().toLocaleDateString()}</span> </div> </div> <div className="d-flex flex-row align-items-center"> <span className="fw-bold d-block me-2">Due Date:</span> <Form.Control type="date" value={this.state.dateOfIssue} name={"dateOfIssue"} onChange={(event) => this.editField(event)} style={{ maxWidth: '150px' }} required="required"/> </div> </div> <div className="d-flex flex-row align-items-center"> <span className="fw-bold me-2">Invoice Number: </span> <Form.Control type="number" value={this.state.invoiceNumber} name={"invoiceNumber"} onChange={(event) => this.editField(event)} min="1" style={{ maxWidth: '70px' }} required="required"/> </div> </div> <hr className="my-4"/> <Row className="mb-5"> <Col> <Form.Label className="fw-bold">Bill to:</Form.Label> <Form.Control placeholder={"Who is this invoice to?"} rows={3} value={this.state.billTo} type="text" name="billTo" className="my-2" onChange={(event) => this.editField(event)} autoComplete="name" required="required"/> <Form.Control placeholder={"Email address"} value={this.state.billToEmail} type="email" name="billToEmail" className="my-2" onChange={(event) => this.editField(event)} autoComplete="email" required="required"/> <Form.Control placeholder={"Billing address"} value={this.state.billToAddress} type="text" name="billToAddress" className="my-2" autoComplete="address" onChange={(event) => this.editField(event)} required="required"/> </Col> <Col> <Form.Label className="fw-bold">Bill from:</Form.Label> <Form.Control placeholder={"Who is this invoice from?"} rows={3} value={this.state.billFrom} type="text" name="billFrom" className="my-2" onChange={(event) => this.editField(event)} autoComplete="name" required="required"/> <Form.Control placeholder={"Email address"} value={this.state.billFromEmail} type="email" name="billFromEmail" className="my-2" onChange={(event) => this.editField(event)} autoComplete="email" required="required"/> <Form.Control placeholder={"Billing address"} value={this.state.billFromAddress} type="text" name="billFromAddress" className="my-2" autoComplete="address" onChange={(event) => this.editField(event)} required="required"/> </Col> </Row> <InvoiceItem onItemizedItemEdit={this.onItemizedItemEdit.bind(this)} onRowAdd={this.handleAddEvent.bind(this)} onRowDel={this.handleRowDel.bind(this)} currency={this.state.currency} items={this.state.items}/> <Row className="mt-4 justify-content-end"> <Col lg={6}> <div className="d-flex flex-row align-items-start justify-content-between"> <span className="fw-bold">Subtotal: </span> <span>{this.state.currency} {this.state.subTotal}</span> </div> <div className="d-flex flex-row align-items-start justify-content-between mt-2"> <span className="fw-bold">Discount:</span> <span> <span className="small ">({this.state.discountRate || 0}%)</span> {this.state.currency} {this.state.discountAmmount || 0}</span> </div> <div className="d-flex flex-row align-items-start justify-content-between mt-2"> <span className="fw-bold">Tax: </span> <span> <span className="small ">({this.state.taxRate || 0}%)</span> {this.state.currency} {this.state.taxAmmount || 0}</span> </div> <hr/> <div className="d-flex flex-row align-items-start justify-content-between" style={{ fontSize: '1.125rem' }}> <span className="fw-bold">Total: </span> <span className="fw-bold">{this.state.currency} {this.state.total || 0}</span> </div> </Col> </Row> <hr className="my-4"/> <Form.Label className="fw-bold">Notes:</Form.Label> <Form.Control placeholder="Thanks for your business!" name="notes" value={this.state.notes} onChange={(event) => this.editField(event)} as="textarea" className="my-2" rows={1}/> </Card> </Col> <Col md={4} lg={3}> <div className="sticky-top pt-md-3 pt-xl-4"> <Button variant="primary" type="submit" className="d-block w-100">Review Invoice</Button> <InvoiceModal showModal={this.state.isOpen} closeModal={this.closeModal} info={this.state} items={this.state.items} currency={this.state.currency} subTotal={this.state.subTotal} taxAmmount={this.state.taxAmmount} discountAmmount={this.state.discountAmmount} total={this.state.total}/> <Form.Group className="mb-3"> <Form.Label className="fw-bold">Currency:</Form.Label> <Form.Select onChange={event => this.onCurrencyChange({currency: event.target.value})} className="btn btn-light my-1" aria-label="Change Currency"> <option value="$">USD (United States Dollar)</option> <option value="£">GBP (British Pound Sterling)</option> <option value="¥">JPY (Japanese Yen)</option> <option value="$">CAD (Canadian Dollar)</option> <option value="$">AUD (Australian Dollar)</option> <option value="$">SGD (Signapore Dollar)</option> <option value="¥">CNY (Chinese Renminbi)</option> <option value="₿">BTC (Bitcoin)</option> </Form.Select> </Form.Group> <Form.Group className="my-3"> <Form.Label className="fw-bold">Tax rate:</Form.Label> <InputGroup className="my-1 flex-nowrap"> <Form.Control name="taxRate" type="number" value={this.state.taxRate} onChange={(event) => this.editField(event)} className="bg-white border" placeholder="0.0" min="0.00" step="0.01" max="100.00"/> <InputGroup.Text className="bg-light fw-bold text-secondary small"> % </InputGroup.Text> </InputGroup> </Form.Group> <Form.Group className="my-3"> <Form.Label className="fw-bold">Discount rate:</Form.Label> <InputGroup className="my-1 flex-nowrap"> <Form.Control name="discountRate" type="number" value={this.state.discountRate} onChange={(event) => this.editField(event)} className="bg-white border" placeholder="0.0" min="0.00" step="0.01" max="100.00"/> <InputGroup.Text className="bg-light fw-bold text-secondary small"> % </InputGroup.Text> </InputGroup> </Form.Group> </div> </Col> </Row> </Form>) } } export default InvoiceForm; |
As you can see we are importing the form
components from the react-bootstrap and also we are importing the icons
from the react-icons
library. And then we have declared different state variables
for the invoice form of the pdf document. Now we need to create the InvoiceItem.js
file and copy paste the following code
components/InvoiceItem.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
import React from 'react'; import 'bootstrap/dist/css/bootstrap.min.css'; import Table from 'react-bootstrap/Table'; import Button from 'react-bootstrap/Button'; import { BiTrash } from "react-icons/bi"; import EditableField from './EditableField'; class InvoiceItem extends React.Component { render() { var onItemizedItemEdit = this.props.onItemizedItemEdit; var currency = this.props.currency; var rowDel = this.props.onRowDel; var itemTable = this.props.items.map(function(item) { return ( <ItemRow onItemizedItemEdit={onItemizedItemEdit} item={item} onDelEvent={rowDel.bind(this)} key={item.id} currency={currency}/> ) }); return ( <div> <Table> <thead> <tr> <th>ITEM</th> <th>QTY</th> <th>PRICE/RATE</th> <th className="text-center">ACTION</th> </tr> </thead> <tbody> {itemTable} </tbody> </Table> <Button className="fw-bold" onClick={this.props.onRowAdd}>Add Item</Button> </div> ); } } class ItemRow extends React.Component { onDelEvent() { this.props.onDelEvent(this.props.item); } render() { return ( <tr> <td style={{width: '100%'}}> <EditableField onItemizedItemEdit={this.props.onItemizedItemEdit} cellData={{ type: "text", name: "name", placeholder: "Item name", value: this.props.item.name, id: this.props.item.id, }}/> <EditableField onItemizedItemEdit={this.props.onItemizedItemEdit} cellData={{ type: "text", name: "description", placeholder: "Item description", value: this.props.item.description, id: this.props.item.id }}/> </td> <td style={{minWidth: '70px'}}> <EditableField onItemizedItemEdit={this.props.onItemizedItemEdit} cellData={{ type: "number", name: "quantity", min: 1, step: "1", value: this.props.item.quantity, id: this.props.item.id, }}/> </td> <td style={{minWidth: '130px'}}> <EditableField onItemizedItemEdit={this.props.onItemizedItemEdit} cellData={{ leading: this.props.currency, type: "number", name: "price", min: 1, step: "0.01", presicion: 2, textAlign: "text-end", value: this.props.item.price, id: this.props.item.id, }}/> </td> <td className="text-center" style={{minWidth: '50px'}}> <BiTrash onClick={this.onDelEvent.bind(this)} style={{height: '33px', width: '33px', padding: '7.5px'}} className="text-white mt-1 btn btn-danger"/> </td> </tr> ); } } export default InvoiceItem; |
As you can see inside this component we are allowing the users to add the series of items
to be inserted inside the invoice pdf document. And inside this item we are displaying the table
in which we have different properties such as the name
of the item and the description
of the item. And then we have the form fields for setting the price
and also a delete trash
icon to delete the product also.
Now we need to create the EditableField.js
component inside the components
folder and copy paste the below code
components/EditableField.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
import React from 'react'; import 'bootstrap/dist/css/bootstrap.min.css'; import Form from 'react-bootstrap/Form'; import InputGroup from 'react-bootstrap/InputGroup'; class EditableField extends React.Component { render() { return ( <InputGroup className="my-1 flex-nowrap"> { this.props.cellData.leading != null && <InputGroup.Text className="bg-light fw-bold border-0 text-secondary px-2"> <span className="border border-2 border-secondary rounded-circle d-flex align-items-center justify-content-center small" style={{width: '20px', height: '20px'}}> {this.props.cellData.leading} </span> </InputGroup.Text> } <Form.Control className={this.props.cellData.textAlign} type={this.props.cellData.type} placeholder={this.props.cellData.placeholder} min={this.props.cellData.min} name={this.props.cellData.name} id={this.props.cellData.id} value={this.props.cellData.value} step={this.props.cellData.step} presicion={this.props.cellData.presicion} aria-label={this.props.cellData.name} onChange={this.props.onItemizedItemEdit} required /> </InputGroup> ); } } export default EditableField; |
As you can see inside this component we are making the editable
form control fields where we can edit the different
information related to the invoice pdf document
Now we need to create the InvoiceModal.js
component inside the components
folder for showing the invoice
information inside the modal
window.
Components/InvoiceModal.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 |
import React from 'react'; import 'bootstrap/dist/css/bootstrap.min.css'; import Row from 'react-bootstrap/Row'; import Col from 'react-bootstrap/Col'; import Button from 'react-bootstrap/Button'; import Table from 'react-bootstrap/Table'; import Modal from 'react-bootstrap/Modal'; import { BiPaperPlane, BiCloudDownload } from "react-icons/bi"; import html2canvas from 'html2canvas'; import jsPDF from 'jspdf' function GenerateInvoice() { html2canvas(document.querySelector("#invoiceCapture")).then((canvas) => { const imgData = canvas.toDataURL('image/png', 1.0); const pdf = new jsPDF({ orientation: 'portrait', unit: 'pt', format: [612, 792] }); pdf.internal.scaleFactor = 1; const imgProps= pdf.getImageProperties(imgData); const pdfWidth = pdf.internal.pageSize.getWidth(); const pdfHeight = (imgProps.height * pdfWidth) / imgProps.width; pdf.addImage(imgData, 'PNG', 0, 0, pdfWidth, pdfHeight); pdf.save('invoice-001.pdf'); }); } class InvoiceModal extends React.Component { constructor(props) { super(props); } render() { return( <div> <Modal show={this.props.showModal} onHide={this.props.closeModal} size="lg" centered> <div id="invoiceCapture"> <div className="d-flex flex-row justify-content-between align-items-start bg-light w-100 p-4"> <div className="w-100"> <h4 className="fw-bold my-2">{this.props.info.billFrom||'John Uberbacher'}</h4> <h6 className="fw-bold text-secondary mb-1"> Invoice #: {this.props.info.invoiceNumber||''} </h6> </div> <div className="text-end ms-4"> <h6 className="fw-bold mt-1 mb-2">Amount Due:</h6> <h5 className="fw-bold text-secondary"> {this.props.currency} {this.props.total}</h5> </div> </div> <div className="p-4"> <Row className="mb-4"> <Col md={4}> <div className="fw-bold">Billed to:</div> <div>{this.props.info.billTo||''}</div> <div>{this.props.info.billToAddress||''}</div> <div>{this.props.info.billToEmail||''}</div> </Col> <Col md={4}> <div className="fw-bold">Billed From:</div> <div>{this.props.info.billFrom||''}</div> <div>{this.props.info.billFromAddress||''}</div> <div>{this.props.info.billFromEmail||''}</div> </Col> <Col md={4}> <div className="fw-bold mt-2">Date Of Issue:</div> <div>{this.props.info.dateOfIssue||''}</div> </Col> </Row> <Table className="mb-0"> <thead> <tr> <th>QTY</th> <th>DESCRIPTION</th> <th className="text-end">PRICE</th> <th className="text-end">AMOUNT</th> </tr> </thead> <tbody> {this.props.items.map((item, i) => { return ( <tr id={i} key={i}> <td style={{width: '70px'}}> {item.quantity} </td> <td> {item.name} - {item.description} </td> <td className="text-end" style={{width: '100px'}}>{this.props.currency} {item.price}</td> <td className="text-end" style={{width: '100px'}}>{this.props.currency} {item.price * item.quantity}</td> </tr> ); })} </tbody> </Table> <Table> <tbody> <tr> <td> </td> <td> </td> <td> </td> </tr> <tr className="text-end"> <td></td> <td className="fw-bold" style={{width: '100px'}}>SUBTOTAL</td> <td className="text-end" style={{width: '100px'}}>{this.props.currency} {this.props.subTotal}</td> </tr> {this.props.taxAmmount != 0.00 && <tr className="text-end"> <td></td> <td className="fw-bold" style={{width: '100px'}}>TAX</td> <td className="text-end" style={{width: '100px'}}>{this.props.currency} {this.props.taxAmmount}</td> </tr> } {this.props.discountAmmount != 0.00 && <tr className="text-end"> <td></td> <td className="fw-bold" style={{width: '100px'}}>DISCOUNT</td> <td className="text-end" style={{width: '100px'}}>{this.props.currency} {this.props.discountAmmount}</td> </tr> } <tr className="text-end"> <td></td> <td className="fw-bold" style={{width: '100px'}}>TOTAL</td> <td className="text-end" style={{width: '100px'}}>{this.props.currency} {this.props.total}</td> </tr> </tbody> </Table> {this.props.info.notes && <div className="bg-light py-3 px-4 rounded"> {this.props.info.notes} </div>} </div> </div> <div className="pb-4 px-4"> <Row> <Col md={6}> <Button variant="primary" className="d-block w-100" onClick={GenerateInvoice}> <BiPaperPlane style={{width: '15px', height: '15px', marginTop: '-3px'}} className="me-2"/>Send Invoice </Button> </Col> <Col md={6}> <Button variant="outline-primary" className="d-block w-100 mt-3 mt-md-0" onClick={GenerateInvoice}> <BiCloudDownload style={{width: '16px', height: '16px', marginTop: '-3px'}} className="me-2"/> Download Copy </Button> </Col> </Row> </div> </Modal> <hr className="mt-4 mb-3"/> </div> ) } } export default InvoiceModal; |
Now inside this component we are finally showing the list of items
inside the invoice pdf document and we are showing this items in the modal
window and then we have the simple button to export this information
to pdf document. And here we are importing the jspdf
and html2canvas
library for exporting the html invoice template
to pdf document
And now basically if you click
the export button to download the pdf
document. Here we are using the filesaver.js
library in react.js to save the pdf document
as an attachment inside the browser.