SurveyJS Server Integration · PHP / Laravel I.6 · Async functions: calculations and validation
You work in a private copy of the demo database, kept until it has been unused for 24 hours.

I.6

Async functions: calculations and validation

You need this: When a value or a rule depends on something only your server knows.

Register a function that asks your server a question, then use it in any expression: a calculated value, a validator, visibleIf. With isAsync: true the function can return a promise; the expression updates when it resolves.

This step on the Server Integration page Source on GitHub

Try this

  1. Type taken@example.com as the email and leave the field: after GET /api/customers/exists returns, the form shows "This email is already registered".
  2. Type any other email: the same request returns {"exists": false} and the error goes away.
  3. Type a postcode (10115, 10999, 80331 or SW1A 1AA): Shipping updates when GET /api/shipping returns. The longest matching prefix wins.
  4. Type a postcode the server has no rate for, such as 99999: the same request returns {"price": null}, and the form shows "We don't deliver to this postcode yet". The validator and the calculated value share one request per postcode.

Code that just ran

These regions are read from the files that served this page, the same lines the Server Integration page shows.

Client · shared/client/async-functions.js · sjs:I.6.client View on GitHub
import { registerFunction } from "survey-core";

// Validation: does a customer with this email already exist?
registerFunction({
  name: "emailExists",
  isAsync: true,
  func: async ([email]) => {
    if (!email) return false;
    const res = await fetch(`/api/customers/exists?email=${encodeURIComponent(email)}`);
    return (await res.json()).exists;
  }
});

// Calculation and validation: the shipping price for a postcode, null where you don't deliver
const prices = new Map();   // one request per postcode serves both the calculated value and the validator
registerFunction({
  name: "shippingCost",
  isAsync: true,
  func: async ([postcode]) => {
    if (!postcode) return 0;
    if (!prices.has(postcode)) prices.set(postcode, fetch(`/api/shipping?postcode=${encodeURIComponent(postcode)}`).then(r => r.json()));
    return (await prices.get(postcode)).price;
  }
});
Server · routes/examples/async-functions.php · sjs:I.6.server View on GitHub
// GET /api/customers/exists?email= — answers emailExists()
Route::get('/api/customers/exists', function (Request $request) {
    $email = Str::lower(trim((string) $request->query('email')));

    return ['exists' => DB::table('customers')->where('email', $email)->exists()];
});

// GET /api/shipping?postcode= — answers shippingCost(): the longest matching postcode prefix wins,
// { "price": null } when none matches (the form shows "We don't deliver to this postcode yet")
Route::get('/api/shipping', function (Request $request) {
    $postcode = Str::upper(str_replace(' ', '', (string) $request->query('postcode')));
    $rate = DB::table('shipping_rates')->get()
        ->filter(fn ($rate) => str_starts_with($postcode, $rate->postcode_prefix))
        ->sortByDesc(fn ($rate) => strlen($rate->postcode_prefix))
        ->first();

    return ['price' => $rate ? (float) $rate->price : null];
});

Definition: shared/definitions/async-functions.json