Now.js Framework Documentation

Now.js Framework Documentation

LineItemsManager - Document Line Items Manager

EN 11 Feb 2026 07:52

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

  1. Overview
  2. Installation and Import
  3. Basic Usage
  4. HTML Attributes
  5. Column Configuration
  6. API Data Binding
  7. Merge Duplicates
  8. Load from Source
  9. Calculation System
  10. Events
  11. JavaScript API
  12. Usage Examples
  13. 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 TableManager instead)
  • 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 object

Dependencies

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 call
  • data-button-class - CSS class for button
  • data-button-text - Button text
  • data-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=1

Important:

  • Use data-role or data-api-param only, NOT name attribute
  • Parameter name comes from data-role or data-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:

  1. Checks for duplicate SKU
  2. If duplicate → adds quantity to existing row
  3. If not duplicate → creates new row

Supported quantity fields:

  • quantity
  • qty
  • received_qty
  • issued_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:

  1. When PO selected → calls api/po/items?po_id=1
  2. Clears existing items (if data-load-clear="true")
  3. 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.