Create Invoice
Here's the
Create Invoice
form, using cached data for clients and products. The Save
button is wired to a dummy API endpoint, try it as you explore the form to see how validation works when no client is selected, and/or when no line items are added.Try saving in various stages of completion
The
CreateInvoice
component encapsulates all the form logic. Let's review the key points.The Form tag
This Form contains a dropdown menu of products, two date pickers, and an expandable set of lineitems. These parts separately feed into the form's
$data
context variable which accumulates the JSON payload sent to the server on submit with successful validation.<Form
id="invoiceForm"
onCancel="invoiceForm.reset()">
The payload
A valid payload looks like this.
{
"issueDate": "2025-06-10",
"dueDate": "2025-07-10",
"client": "Abstergo Industries",
"lineItems": [
{
"quantity": "1",
"amount": 105,
"product": "API Integration",
"price": 105
},
{
"quantity": "1",
"amount": 115,
"product": "Brand Strategy Consulting",
"price": 115
}
]
}
The form's
Cancel
button resets all of its components and empties this data structure.Nested FormItems
There is a top-level
FormItem
for client
, issue_date
, due_date
, and lineItems
. Their bindTo
attributes name and populate corresponding fields in $data
.Nested within
lineItems
there is a FormItem
for product
, quantity
, price
, and amount
. Their bindTo
attributes name and define slots in an array of lineItems
that's dynamically built on each click of the Add Item
button.<Card title="Create New Invoice">
<FlowLayout>
<FormItem
width="50%"
type="select"
placeholder="select client"
bindTo="client"
label="Client"
required="true"
>
<Items data="/api/clients">
<Option value="{$item.name}" label="{$item.name}"/>
</Items>
</FormItem>
<FormItem
type="datePicker"
dateFormat="yyyy-MM-dd"
initialValue="{ formatToday() }"
bindTo="issueDate"
label="Issue date" width="25%"
/>
<FormItem
type="datePicker"
dateFormat="yyyy-MM-dd"
initialValue="{ formatToday(30) }"
bindTo="dueDate"
label="Due date"
width="25%" />
</FlowLayout>
<H2>Line Items</H2>
<FlowLayout
fontWeight="bold"
backgroundColor="$color-surface-100"
padding="$space-2"
>
<Text width="20%">Product/Service</Text>
<Text width="20%">Description</Text>
<Text width="20%">Quantity</Text>
<Text width="20%">Price</Text>
<Text width="20%">Amount</Text>
</FlowLayout>
<FormItem
bindTo="lineItems"
type="items"
id="lineItemsForm"
required="true"
requiredInvalidMessage="At least one line item is required."
>
<FlowLayout width="100%">
<DataSource
id="productDetails"
url="/api/products/byname/{$item.product}"
when="{$item.product != null}"
method="GET"
/>
<FormItem
bindTo="product"
type="select"
placeholder="select product"
width="20%"
required="true"
>
<Items data="/api/products">
<Option value="{$item.name}" label="{$item.name}"/>
</Items>
</FormItem>
<Text width="20%">{ productDetails.value[0].description }</Text>
<FormItem
width="20%"
bindTo="quantity"
type="number"
initialValue="1"
minValue="1"
/>
<FormItem
width="20%"
bindTo="price"
startText="$"
initialValue="{ productDetails.value[0].price }"
/>
<FormItem
width="13%"
bindTo="amount"
startText="$"
enabled="false"
initialValue="{ $item.price ? $item.quantity * $item.price : '' } "
/>
<Button
width="2rem"
onClick="lineItemsForm.removeItem($itemIndex)">
X
</Button>
</FlowLayout>
</FormItem>
<HStack>
<Button onClick="lineItemsForm.addItem()">
Add Item
</Button>
<SpaceFiller/>
<Text>
Total: ${ window.lineItemTotal($data.lineItems) }
</Text>
</HStack>
</Card>
Total for lineItems
The
Add Item
button invokes the addItem
method of FormItem to add a new empty row to the array. (Above we see the corresponding removeItem
used when clicking the button at the end of a row.)The app defines a function,
lineItemTotal
, to receive the lineItems
array and add up the amounts.<HStack>
<Button onClick="lineItemsForm.addItem()">
Add Item
</Button>
<SpaceFiller />
<Text>
Total: ${ window.lineItemTotal($data.lineItems) }
</Text>
</HStack>
The same function runs when the APICall runs on form submission.
<Form>
<!-- ... -->
<event name="submit">
<APICall
url="https://httpbin.org/post"
method="POST"
inProgressNotificationMessage="Saving invoice..."
completedNotificationMessage="Invoice saved successfully"
body="{
{
client: $param.client,
issueDate: $param.issueDate,
dueDate: $param.dueDate,
total: window.lineItemTotal($data.lineItems),
items: JSON.stringify($param.lineItems || [])
}
}"
onSuccess="Actions.navigate('/invoices')"
/>
</event>
<!-- ... -->
</Form>