Advanced Portal Designer Documentation
Index

Field Validation with JavaScript

APD provides real-time field validation that triggers Business Central's field validation logic, ensuring data integrity without page refreshes.

APD_validate Function

Syntax

APD_validate(entry, table, field)

Parameters:

Parameter Type Description
entry HTMLElement The input element that changed
table number Business Central table number
field number Business Central field number

How It Works

  1. User modifies a field value
  2. onchange event triggers APD_validate()
  3. AJAX request sent to Business Central
  4. BC validates the field and runs any OnValidate triggers
  5. Response updates all related fields in the form
  6. If validation fails, original value is restored

Basic Usage

<input type="text" 
       name="18:2"
       value="<?field(Customer,Name)?>"
       onfocus="APD_Focus(this, 18, 2)"
       onchange="APD_validate(this, 18, 2)">

Field Focus Tracking

APD_Focus Function

Always pair APD_validate() with APD_Focus() to enable value rollback on validation failure.

APD_Focus(entry, table, field)

Example:

<input type="text" 
       name="27:6"
       value="<?field(Item,No.)?>"
       onfocus="APD_Focus(this, 27, 6)"
       onchange="APD_validate(this, 27, 6)">

What Happens

  1. On Focus: Original value stored in xRecValue
  2. On Change: Validation attempted
  3. If Error: Value reverts to xRecValue, field refocused

Validation Response Handling

Successful Validation

When validation succeeds:

  1. Server returns DATA1 prefix with updated field values
  2. All form fields for the table are updated
  3. Related fields reflect any changes from OnValidate triggers

Example Response:

DATA118:1=10000&18:2=Contoso%20Ltd.&18:5=123%20Main%20St&18:21=50000

Failed Validation

When validation fails:

  1. Server returns ERROR prefix with message
  2. APDAlert() displays the error
  3. Field value reverts to original
  4. Field receives focus for correction

Error Code Tracking

The errorCode Variable

After validation, check errorCode:

// 0 = success, 1 = error
if (errorCode === 0) {
    console.log('Validation successful');
} else {
    console.log('Validation failed');
}

Usage in Custom Logic

function validateAndContinue() {
    APD_validate(document.getElementById('myField'), 18, 2);
    
    // Check result
    if (errorCode === 0) {
        proceedToNextStep();
    }
}

Custom Post-Validation Logic

ExtraValidateFunction

Define a function to run after each successful validation:

var ExtraValidateFunction = function(form) {
    // This runs after every successful APD_validate call
    
    // Example: Update calculated totals
    updateTotals();
    
    // Example: Enable/disable fields
    updateFieldStates();
    
    // Example: Update UI indicators
    showSaveStatus('Modified');
};

Example: Order Line Totals

var ExtraValidateFunction = function(form) {
    // Get current values
    var qty = parseFloat(APD_GetSingleValue(37, 15)) || 0;
    var price = parseFloat(APD_GetSingleValue(37, 22)) || 0;
    var discount = parseFloat(APD_GetSingleValue(37, 27)) || 0;
    
    // Calculate (though BC does this server-side too)
    var lineAmount = qty * price * (1 - discount / 100);
    
    // Update display
    document.getElementById('lineTotal').textContent = lineAmount.toFixed(2);
    
    // Update order totals
    recalculateOrderTotal();
};

Validation Examples

Customer Card

<?open(Customer)?>
<?filter(Customer,"No.",Parm('id'))?>
<?findfirst(Customer)?>

<form id="customerCard">
    <input type="hidden" name="TABLE" value="18">
    <!-- Primary key as a hidden input (<?key?> only writes URL query fragments) -->
    <input type="hidden" name="18:1" value="<?field(Customer,"No.")?>">

    <!-- Customer No. - typically not editable -->
    <div class="field">
        <label>No.</label>
        <input type="text" value="<?field(Customer,"No.")?>" disabled>
    </div>
    
    <!-- Name - with validation -->
    <div class="field">
        <label>Name</label>
        <input type="text" 
               name="18:2" 
               value="<?field(Customer,Name)?>"
               onfocus="APD_Focus(this, 18, 2)"
               onchange="APD_validate(this, 18, 2)">
    </div>
    
    <!-- Country/Region - might trigger address format changes -->
    <div class="field">
        <label>Country/Region</label>
        <input type="text" 
               name="18:35" 
               value="<?field(Customer,Country/Region Code)?>"
               onfocus="APD_Focus(this, 18, 35)"
               onchange="APD_validate(this, 18, 35)">
        <button type="button" 
                onclick="APD_Lookup(18, 35, 9, 1, 2, this)">...</button>
    </div>
    
    <!-- VAT Registration - has format validation -->
    <div class="field">
        <label>VAT Registration No.</label>
        <input type="text" 
               name="18:86" 
               value="<?field(Customer,VAT Registration No.)?>"
               onfocus="APD_Focus(this, 18, 86)"
               onchange="APD_validate(this, 18, 86)">
    </div>
    
    <!-- Credit Limit -->
    <div class="field">
        <label>Credit Limit (LCY)</label>
        <input type="number" 
               name="18:21" 
               value="<?field(Customer,Credit Limit (LCY))?>"
               step="0.01"
               onfocus="APD_Focus(this, 18, 21)"
               onchange="APD_validate(this, 18, 21)">
    </div>
</form>

Sales Line with Cascading Updates

<form id="salesLine">
    <input type="hidden" name="TABLE" value="37">
    <!-- Sales Line primary key: Document Type (1), Document No. (3), Line No. (4) -->
    <input type="hidden" name="37:1" value="<?field("Sales Line","Document Type")?>">
    <input type="hidden" name="37:3" value="<?field("Sales Line","Document No.")?>">
    <input type="hidden" name="37:4" value="<?field("Sales Line","Line No.")?>">

    <!-- Item No. - triggers description, price, UOM updates -->
    <div class="field">
        <label>Item No.</label>
        <input type="text"
               name="37:6"
               value="<?field("Sales Line","No.")?>"
               onfocus="APD_Focus(this, 37, 6)"
               onchange="APD_validate(this, 37, 6)">
        <button onclick="APD_Lookup(37, 6, 27, 1, 3, this)">...</button>
    </div>
    
    <!-- Description - auto-filled from item -->
    <div class="field">
        <label>Description</label>
        <input type="text" 
               name="37:11" 
               value="<?field(Sales Line,Description)?>"
               onfocus="APD_Focus(this, 37, 11)"
               onchange="APD_validate(this, 37, 11)">
    </div>
    
    <!-- Quantity - triggers amount calculation -->
    <div class="field">
        <label>Quantity</label>
        <input type="number" 
               name="37:15" 
               value="<?field(Sales Line,Quantity)?>"
               min="0"
               onfocus="APD_Focus(this, 37, 15)"
               onchange="APD_validate(this, 37, 15)">
    </div>
    
    <!-- Unit Price - auto-filled, but can be overridden -->
    <div class="field">
        <label>Unit Price</label>
        <input type="number" 
               name="37:22" 
               value="<?field(Sales Line,Unit Price)?>"
               step="0.01"
               onfocus="APD_Focus(this, 37, 22)"
               onchange="APD_validate(this, 37, 22)">
    </div>
    
    <!-- Line Amount - calculated, display only -->
    <div class="field">
        <label>Amount</label>
        <span id="lineAmount"><?field(Sales Line,Line Amount)?></span>
    </div>
</form>

<script>
var ExtraValidateFunction = function(form) {
    // Update displayed amount after validation
    var amount = APD_GetSingleValue(37, 103);  // Line Amount field
    document.getElementById('lineAmount').textContent = amount;
};
</script>

Validation with Lookups

Combine validation with lookup windows:

<div class="lookup-field">
    <input type="text" 
           name="18:86"
           value="<?field(Customer,Salesperson Code)?>"
           onfocus="APD_Focus(this, 18, 86)"
           onchange="APD_validate(this, 18, 86)">
    <button type="button" 
            onclick="APD_Lookup(18, 86, 13, 1, 2, this)"
            title="Select Salesperson">
        ...
    </button>
</div>

When a value is selected from lookup:

  1. Value is placed in the input field
  2. APD_validate() is automatically triggered
  3. Related fields update based on selection

Message Handling

Server Messages

The server can send messages via the _MESSAGE field:

// In APD_validate response handling
if (vaerdi[0] == "_MESSAGE") {
    Message = 1;
}

// Display message
if (Message == 1) {
    var msg = getvar["_MESSAGE"];
    msg = msg.replace(/\x2B/g, " ");
    msg = msg.replace(/\\/g, "\n");
    APDAlert(msg);
}

Custom Alert Implementation

Override APDAlert for custom error display:

// Modal-based alert
APDAlert = function(message) {
    var modal = document.createElement('div');
    modal.className = 'apd-alert-modal';
    modal.innerHTML = `
        <div class="apd-alert-content">
            <p>${message}</p>
            <button onclick="this.parentElement.parentElement.remove()">OK</button>
        </div>
    `;
    document.body.appendChild(modal);
};
.apd-alert-modal {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0,0,0,0.5);
    display: flex;
    align-items: center;
    justify-content: center;
}

.apd-alert-content {
    background: white;
    padding: 20px;
    border-radius: 8px;
    max-width: 400px;
}

Best Practices

Always Pair Focus and Change

<input onfocus="APD_Focus(this, T, F)" 
       onchange="APD_validate(this, T, F)">

Let BC Handle Calculations

Don't duplicate BC logic client-side. Let validation update calculated fields:

// Don't do this
var total = qty * price;
document.getElementById('total').value = total;

// Do this - let BC calculate
APD_validate(qtyField, 37, 15);
// Total is updated automatically in response

Provide Visual Feedback

input:focus {
    border-color: #007bff;
}

input.validating {
    background-color: #fffde7;
}

input.valid {
    border-color: #28a745;
}

input.invalid {
    border-color: #dc3545;
}

Handle Network Errors

var originalAlert = APDAlert;
APDAlert = function(message) {
    if (message.indexOf('Communication error') >= 0) {
        // Retry logic or special handling
        showRetryDialog(message);
    } else {
        originalAlert(message);
    }
};