Now.js Framework Documentation
LineItemsManager - Document Line Items Manager
LineItemsManager - Document Line Items Manager
Documentation for LineItemsManager, a component for managing line items in documents such as purchase orders, quotations, and receipts. It supports adding, editing, deleting items, and automatic calculation.
📋 Table of Contents
- Overview
- Installation and Import
- Basic Usage
- HTML Attributes
- Column Configuration
- API Data Binding
- Merge Duplicates
- Load from Source
- Calculation System
- Events
- JavaScript API
- Usage Examples
- Best Practices
Overview
LineItemsManager is a component for managing line items in documents. It allows you to add products via autocomplete and automatically creates table rows with editable form inputs.
Key Features
- ✅ Event-Driven: Listens for 'change' event on product search input
- ✅ Detail API: Fetches full product data from API when selected
- ✅ Flexible API Parameters: Send multiple parameters to API (qty, warehouse, batch, etc.)
- ✅ Flexible Columns: Define columns via
<th data-field> - ✅ Editable/Readonly Fields: Support for both editable and readonly fields
- ✅ Display-Only Fields: Display text only without input
- ✅ Custom Buttons: Custom buttons per cell (e.g., VAT calculation)
- ✅ Auto-Calculation: Automatic calculation (e.g., quantity × price = subtotal)
- ✅ Merge Duplicates: Automatically merge duplicate items (optional)
- ✅ External Callbacks: Call external functions via
data-on-calculate - ✅ Auto-Load: Load items from source automatically (e.g., PO, Quotation)
- ✅ Data Binding: Support
data-attr="data:items"like TableManager
When to Use LineItemsManager
✅ Use LineItemsManager when:
- Managing product line items in documents (PO, Invoice, Receipt)
- Users need to add products via autocomplete
- Editing data in table (quantity, price, notes)
- Automatic total calculation needed
- Merging duplicates or loading from other sources needed
❌ Don't use LineItemsManager when:
- Displaying read-only data tables (use
TableManagerinstead) - No need for data editing in table
- Need detailed control over row rendering
Installation and Import
LineItemsManager is loaded with the Now.js Framework and is ready to use via the window object:
// No import needed - ready to use immediately
console.log(window.LineItemsManager); // LineItemsManager objectDependencies
LineItemsManager requires:
- HttpClient or fetch – For API calls
- ElementManager – For creating inputs (optional but recommended)
Basic Usage
1. Complete HTML Structure
<!-- Product Search -->
<input type="text" name="product_text" data-role="product"
data-autocomplete="true"
data-source="api/products/search">
<!-- Additional Parameters -->
<input type="number" name="qty" value="1" data-role="qty">
<input type="number" name="warehouse" value="1" data-role="warehouse">
<!-- Line Items Table -->
<table data-line-items="items"
data-detail-api="api/products/get"
data-listen-select="[data-role='product']"
data-api-params="[data-role]"
data-allow-delete="true"
data-merge="true"
data-on-calculate="calculateItems">
<thead>
<tr>
<th data-field="sku">SKU</th>
<th data-field="name" data-type="text">Product Name</th>
<th data-field="quantity" data-type="number"
data-role="quantity" data-min="1">Quantity</th>
<th data-field="unit_price" data-type="currency"
data-role="price">Price</th>
<th data-field="subtotal" data-type="currency"
data-readonly="true">Subtotal</th>
</tr>
</thead>
<tbody></tbody>
</table>2. JavaScript-Based Usage
// Create instance for table
const instance = LineItemsManager.create('#my-table', {
detailApi: 'api/products/get',
listenSelect: '[data-role="product"]',
allowDelete: true,
mergeOnDuplicate: true
});
// Add item
instance.addItem({
sku: 'P001',
name: 'Product A',
quantity: 1,
unit_price: 100
});3. Auto-Initialization
LineItemsManager automatically creates instances for all <table data-line-items>:
<!-- Will be created automatically when page loads -->
<table data-line-items="items"></table>HTML Attributes
Basic Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
data-line-items |
string | 'items' |
Field name for form submission (required) |
data-detail-api |
string | - | API URL for fetching product details |
data-listen-select |
string | '[data-role="product-search"]' |
Selector for product search input |
data-api-params |
string | '[data-role]' |
Selector pattern for collecting API parameters |
data-allow-delete |
boolean | true |
Show delete button |
data-delete-confirm |
boolean | false |
Show confirmation before delete |
data-reindex-on-remove |
boolean | false |
If true, reindex input names/ids after a remove so array indices are continuous |
Merge Duplicates Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
data-merge |
boolean | true |
Auto-merge duplicate items |
data-merge-key |
string | 'sku' |
Field for checking duplicates |
Load from Source Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
data-load-from |
string | - | Selector of select element for loading (e.g., #po_id) |
data-load-api |
string | - | API URL for loading items |
data-load-param |
string | 'id' |
Parameter name sent to API |
data-load-clear |
boolean | true |
Clear existing items before loading |
Note: The default for data-listen-select is '[data-role="product-search"]'. To avoid selector mismatches with your search input, either set data-listen-select explicitly on the <table> (examples above use data-listen-select="[data-role='product']") or make your search input use data-role="product-search" to match the default.
Calculation Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
data-on-calculate |
string | - | Function name for calculation (global function) |
Column Configuration
Columns are defined via <th> in <thead> using data-field and other attributes.
Column Attributes
| Attribute | Description | Example |
|---|---|---|
data-field |
Field name (required) | data-field="quantity" |
data-type |
Input type | data-type="number" |
data-readonly |
Read-only | data-readonly="true" |
data-display-only |
Display only (no input) | data-display-only="true" |
data-hidden |
Hide column | data-hidden="true" |
data-role |
Role name for calculation | data-role="quantity" |
Input Types (data-type)
| Type | Description | Additional Attributes |
|---|---|---|
number |
Number | data-min, data-max, data-step |
currency |
Currency (2 decimal places) | data-min, data-max, data-step |
text |
Text | data-maxlength, data-size |
select |
Dropdown | - |
checkbox |
Checkbox | - |
Display Modes
1. Editable
<th data-field="quantity" data-type="number">Quantity</th>→ Creates editable input
2. Readonly Input (for form submission)
<th data-field="subtotal" data-type="currency" data-readonly="true">Subtotal</th>→ Creates readonly input for form submission
3. Display Only
<th data-field="unit_name" data-display-only="true">Unit</th>→ Displays text only, no input, no form submission
4. Hidden
<th data-field="product_id" data-hidden="true">ID</th>→ Creates hidden input
Cell Buttons
You can add custom buttons in cells:
<th data-field="unit_price"
data-type="currency"
data-button-click="addVat"
data-button-class="icon-plus"
data-button-title="Add VAT">Price</th>Attributes:
data-button-click- Function name to calldata-button-class- CSS class for buttondata-button-text- Button textdata-button-title- Button tooltip
Example Function:
function addVat(currentValue, button, rowData, instance) {
return currentValue * 1.07; // Add 7% VAT
}API Data Binding
API Parameter Collection
LineItemsManager collects parameters from inputs matching data-api-params:
<!-- Example: data-api-params="[data-role]" -->
<input type="text" data-role="product" value="P001">
<input type="number" data-role="qty" value="5">
<input type="number" data-role="warehouse" value="1">When selecting product, sends parameters:
product=P001&qty=5&warehouse=1Important:
- Use
data-roleordata-api-paramonly, NOTnameattribute - Parameter name comes from
data-roleordata-api-param
Expected Response Format
API should return:
{
"success": true,
"data": {
"sku": "P001",
"name": "Product A",
"quantity": 1,
"unit_price": 100.00,
"unit_name": "pcs",
"warehouse_name": "Main Warehouse"
}
}Or supports nested data:
{
"success": true,
"data": {
"data": {
"sku": "P001",
"name": "Product A",
...
}
}
}Merge Duplicates
When data-merge="true" is enabled (default), LineItemsManager merges duplicate items automatically:
<table data-line-items="items"
data-merge="true"
data-merge-key="sku">How it works:
- Checks for duplicate SKU
- If duplicate → adds quantity to existing row
- If not duplicate → creates new row
Supported quantity fields:
quantityqtyreceived_qtyissued_qty
Disable Merging
<table data-line-items="items" data-merge="false">Load from Source
Can load items from other sources (e.g., PO, Quotation) automatically:
Example: Load from PO
<!-- Select PO -->
<select id="po_id" name="po_id">
<option value="">-- Select PO --</option>
<option value="1">PO-2024-001</option>
<option value="2">PO-2024-002</option>
</select>
<!-- Table -->
<table data-line-items="items"
data-load-from="#po_id"
data-load-api="api/po/items"
data-load-param="po_id"
data-load-clear="true">
<thead>...</thead>
<tbody></tbody>
</table>How it works:
- When PO selected → calls
api/po/items?po_id=1 - Clears existing items (if
data-load-clear="true") - Adds items from API
Response Format
{
"success": true,
"data": {
"items": [
{"sku": "P001", "name": "Product A", "quantity": 5, "unit_price": 100},
{"sku": "P002", "name": "Product B", "quantity": 10, "unit_price": 50}
]
}
}Or
{
"success": true,
"data": [
{"sku": "P001", ...},
{"sku": "P002", ...}
]
}Calculation System
LineItemsManager uses external callbacks for calculations for flexibility and to avoid hardcoded logic.
Setup Callback
<table data-line-items="items" data-on-calculate="calculateItems">Example Calculation Function
function calculateItems({items, instance}) {
let totalQty = 0;
let totalAmount = 0;
// Calculate each row
const updatedItems = items.map(item => {
const qty = parseFloat(item.quantity) || 0;
const price = parseFloat(item.unit_price) || 0;
const subtotal = qty * price;
totalQty += qty;
totalAmount += subtotal;
return {
subtotal: subtotal.toFixed(2)
};
});
// Update totals
return {
items: updatedItems,
'#total_qty': totalQty,
'#total_amount': totalAmount.toFixed(2)
};
}Callback Structure
Input:
{
items: Array, // All items from DOM
instance: Object // LineItemsManager instance
}Output:
{
items: Array, // Each element = object of fields to update
'#selector': value // Update value in elements outside table
}Advanced Example: Discount and VAT Calculation
function calculateItems({items, instance}) {
let subtotal = 0;
const updatedItems = items.map(item => {
const qty = parseFloat(item.quantity) || 0;
const price = parseFloat(item.unit_price) || 0;
const itemSubtotal = qty * price;
subtotal += itemSubtotal;
return {subtotal: itemSubtotal.toFixed(2)};
});
// Read discount from input
const discountPercent = parseFloat(document.getElementById('discount')?.value) || 0;
const discount = subtotal * (discountPercent / 100);
const afterDiscount = subtotal - discount;
const vat = afterDiscount * 0.07;
const total = afterDiscount + vat;
return {
items: updatedItems,
'#subtotal': subtotal.toFixed(2),
'#discount_amount': discount.toFixed(2),
'#after_discount': afterDiscount.toFixed(2),
'#vat': vat.toFixed(2),
'#total': total.toFixed(2)
};
}Manual Recalculation
// From instance
instance.calculate();
// From element
const instance = LineItemsManager.getInstance('#my-table');
instance.calculate();
// From anywhere (recalculate all)
LineItemsManager.recalculate();Call from Template Events
<input type="number" id="discount"
data-on="input:LineItemsManager.recalculate">Events
LineItemsManager emits events when changes occur.
Event Types
| Event | When | Detail |
|---|---|---|
lineitems:add |
Row added | {row, rowIndex, instance} |
lineitems:update |
Row updated | {row, rowIndex, instance} |
lineitems:remove |
Row removed | {rowIndex, instance} |
lineitems:merge |
Duplicate merged | {row, rowIndex, instance} |
lineitems:calculate |
Calculation complete | {rows, instance} |
lineitems:clear |
All items cleared | {instance} |
lineitems:action |
Cell button clicked | {action, field, rowIndex, rowData, button, instance} |
lineitems:sourceLoaded |
Source loaded successfully | {source, items, count, instance} |
lineitems:sourceError |
Source load error | {source, error, instance} |
Listen to Events
const table = document.querySelector('[data-line-items]');
table.addEventListener('lineitems:add', (e) => {
console.log('Item added:', e.detail.row);
console.log('At row:', e.detail.rowIndex);
});
table.addEventListener('lineitems:calculate', (e) => {
console.log('Calculation complete, rows:', e.detail.rows);
});
table.addEventListener('lineitems:sourceLoaded', (e) => {
console.log('Loaded from source:', e.detail.source);
console.log('Item count:', e.detail.count);
});JavaScript API
Create Instance
const instance = LineItemsManager.create(table, options);
// Example
const instance = LineItemsManager.create('#my-table', {
detailApi: 'api/products/get',
mergeOnDuplicate: true,
allowDelete: true
});Get Instance
// From element
const instance = LineItemsManager.getInstance(table);
// From selector
const instance = LineItemsManager.getInstance('#my-table');Manage Items
// Add item
instance.addItem({
sku: 'P001',
name: 'Product A',
quantity: 1,
unit_price: 100
});
// Update row (by rowIndex)
instance.updateRow(0, {
quantity: 5,
unit_price: 90
});
// Remove row
instance.removeRow(0);
// Clear all
instance.clear();Manage Data
// Get all data
const data = instance.getData();
// [{sku: 'P001', name: 'Product A', ...}, ...]
// Set data (clear and add new)
instance.setData([
{sku: 'P001', name: 'Product A', quantity: 1, unit_price: 100},
{sku: 'P002', name: 'Product B', quantity: 2, unit_price: 50}
]);Load from Source
// Load items from source (e.g., PO ID = 123)
await instance.loadFromSource(123);Calculate
// Trigger calculation
instance.calculate();Destroy Instance
instance.destroy();Usage Examples
Example 1: Purchase Order
<form id="po-form">
<!-- Search Product -->
<label>Product</label>
<input type="text" name="product_text" data-role="product"
data-autocomplete="true"
data-source="api/products/search">
<label>Quantity</label>
<input type="number" name="qty" value="1" data-role="qty" min="1">
<!-- Line Items Table -->
<table data-line-items="items"
data-detail-api="api/products/get"
data-listen-select="[data-role='product']"
data-api-params="[data-role]"
data-on-calculate="calculatePO">
<thead>
<tr>
<th data-field="sku" data-display-only="true">SKU</th>
<th data-field="name" data-display-only="true">Product</th>
<th data-field="quantity" data-type="number" data-min="1">Quantity</th>
<th data-field="unit_price" data-type="currency">Price</th>
<th data-field="subtotal" data-type="currency" data-readonly="true">Subtotal</th>
</tr>
</thead>
<tbody></tbody>
</table>
<!-- Total -->
<div>
<label>Total</label>
<input type="text" id="total_amount" readonly>
</div>
<button type="submit">Save</button>
</form>
<script>
function calculatePO({items}) {
let total = 0;
const updatedItems = items.map(item => {
const qty = parseFloat(item.quantity) || 0;
const price = parseFloat(item.unit_price) || 0;
const subtotal = qty * price;
total += subtotal;
return {subtotal: subtotal.toFixed(2)};
});
return {
items: updatedItems,
'#total_amount': total.toFixed(2)
};
}
</script>Example 2: Goods Receipt - Load from PO
<form id="gr-form">
<!-- Select PO -->
<label>Purchase Order</label>
<select id="po_id" name="po_id">
<option value="">-- Select PO --</option>
</select>
<!-- Line Items Table (load from PO) -->
<table data-line-items="items"
data-load-from="#po_id"
data-load-api="api/po/items"
data-load-param="po_id"
data-on-calculate="calculateGR">
<thead>
<tr>
<th data-field="sku" data-display-only="true">SKU</th>
<th data-field="name" data-display-only="true">Product</th>
<th data-field="ordered_qty" data-display-only="true">Ordered</th>
<th data-field="received_qty" data-type="number" data-min="0">Received</th>
<th data-field="warehouse" data-type="select">Warehouse</th>
</tr>
</thead>
<tbody></tbody>
</table>
<button type="submit">Save</button>
</form>Example 3: Invoice - with VAT and Discount
<form id="invoice-form">
<!-- Search Product -->
<input type="text" data-role="product"
data-autocomplete="true"
data-source="api/products/search">
<!-- Line Items Table -->
<table data-line-items="items"
data-detail-api="api/products/get"
data-listen-select="[data-role='product']"
data-on-calculate="calculateInvoice">
<thead>
<tr>
<th data-field="name">Product</th>
<th data-field="quantity" data-type="number">Quantity</th>
<th data-field="unit_price" data-type="currency"
data-button-click="addVat"
data-button-class="icon-plus"
data-button-title="+ VAT 7%">Price</th>
<th data-field="subtotal" data-type="currency" data-readonly="true">Subtotal</th>
</tr>
</thead>
<tbody></tbody>
</table>
<!-- Summary -->
<div>
<label>Subtotal</label>
<input type="text" id="subtotal" readonly>
</div>
<div>
<label>Discount (%)</label>
<input type="number" id="discount_percent" value="0"
data-on="input:LineItemsManager.recalculate">
</div>
<div>
<label>Discount (Amount)</label>
<input type="text" id="discount_amount" readonly>
</div>
<div>
<label>After Discount</label>
<input type="text" id="after_discount" readonly>
</div>
<div>
<label>VAT 7%</label>
<input type="text" id="vat" readonly>
</div>
<div>
<label>Grand Total</label>
<input type="text" id="total" readonly>
</div>
</form>
<script>
// Add VAT 7% button
function addVat(currentValue) {
const price = parseFloat(currentValue) || 0;
return (price * 1.07).toFixed(2);
}
// Calculate Invoice
function calculateInvoice({items}) {
let subtotal = 0;
const updatedItems = items.map(item => {
const qty = parseFloat(item.quantity) || 0;
const price = parseFloat(item.unit_price) || 0;
const itemSubtotal = qty * price;
subtotal += itemSubtotal;
return {subtotal: itemSubtotal.toFixed(2)};
});
const discountPercent = parseFloat(document.getElementById('discount_percent')?.value) || 0;
const discount = subtotal * (discountPercent / 100);
const afterDiscount = subtotal - discount;
const vat = afterDiscount * 0.07;
const total = afterDiscount + vat;
return {
items: updatedItems,
'#subtotal': subtotal.toFixed(2),
'#discount_amount': discount.toFixed(2),
'#after_discount': afterDiscount.toFixed(2),
'#vat': vat.toFixed(2),
'#total': total.toFixed(2)
};
}
</script>Best Practices
1. Use data-role Instead of name
❌ Don't:
<input name="product" data-api-param="product">✅ Do:
<input name="product_text" data-role="product">2. Define data-field Clearly
❌ Don't:
<th>Product</th>✅ Do:
<th data-field="name">Product</th>3. Use data-display-only for Non-Editable Fields
❌ Don't:
<th data-field="sku" data-readonly="true">SKU</th> <!-- Creates readonly input -->✅ Do:
<th data-field="sku" data-display-only="true">SKU</th> <!-- Display text only -->4. Use data-readonly for Form Submission Fields
✅ Correct:
<th data-field="subtotal" data-type="currency" data-readonly="true">Subtotal</th>
<!-- Creates readonly input for form submission -->5. Check API Response Format
✅ Good Response:
{
"success": true,
"data": {
"sku": "P001",
"name": "Product A",
"quantity": 1,
"unit_price": 100.00
}
}6. Handle Errors in Callback
✅ Good Function:
function calculateItems({items, instance}) {
try {
// Calculate
return {items: updatedItems};
} catch (err) {
console.error('Calculate error:', err);
return {items: []};
}
}7. Use Event Listeners for Additional Logic
table.addEventListener('lineitems:add', (e) => {
// Additional logic after adding item
console.log('Item added:', e.detail.row);
});8. Cleanup Instance When Not Needed
// When no longer needed
instance.destroy();Summary
LineItemsManager is a powerful tool for managing product line items in documents. It provides high flexibility and supports:
- ✅ Event-driven architecture
- ✅ Flexible API parameters
- ✅ External calculation callbacks
- ✅ Auto-load from sources
- ✅ Merge duplicates
- ✅ Customizable columns
- ✅ Rich input types
This makes it easy to quickly and efficiently create document forms such as PO, Invoice, and Receipt.