SurveyJS PHP Backend Integration with Laravel /

Async Calculations and Validation

Async Calculations and Validation

Use when: Calculations or validation rules depend on data from your server.

Read this step in the docs

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.
  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".
  5. Press Start over to try other emails and postcodes in an empty form.

    No requests yet.

      What the server stored

      Source on GitHub Definition: shared/definitions/async-functions.json

      Client · shared/client/async-functions.js · sjs:async-functions.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:async-functions.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')));
          // only a whole German (10115) or UK (SW1A 1AA) postcode gets a price, not anything that starts with a prefix
          if (! preg_match('/^(\d{5}|[A-Z]{1,2}\d[A-Z\d]?\d[A-Z]{2})$/', $postcode)) {
              return ['price' => null];
          }
          $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];
      });