docs

Mocking Bound Pages

Mock up bound pages in a browser and check markup against the app's schema.

On this page

You can create and style data-bound pages in a browser using mock data, without running the app. Designers can work on the mockup without compiling native code.

The same script can check the page's markup against the app's schema during a build step— a renamed field fails the build instead of breaking the page at runtime.

Example Page

The examples on this page use hud.html, which displays player data and settings:

HTML
<!doctype html>
<html>
<head>
  <script src="ul-mock.js"></script>
</head>
<body>
  <h1 ul-text="player.name"></h1>
  <p>Level {{player.level}}, {{player.gold|comma}} gold</p>
  <progress max="100" ul-attr:value="player.stats.health"></progress>
  <p>Armor: {{player.stats.armor}}</p>
  <button ul-on:click="player.usePotion">Drink Potion</button>
  <ul ul-for="player.items">
    <template>
      <li>{{name}} x{{count}} <button ul-on:click="drop">Drop</button></li>
    </template>
  </ul>
  <input type="range" min="0" max="1" step="0.1" ul-value="settings.volume">
</body>
</html>

The page binds fields from player, displays formatted gold using the comma formatter from Showing Data in Markup, and provides a range slider for settings.volume.

Saving the Schema from Native Code

Native code writes the app's schema and current values to a JSON file by calling Context::DumpSchema():

C++
// Development builds only: the file holds the app's real data.
if (debug_key_pressed)
  ctx.DumpSchema("ui/schema.json");  // written at the end of the next Sync()

The library writes the file at the end of the next Sync(), so you can call DumpSchema() from any thread (such as a debug hotkey handler).

The file records the types and current values for every model bound at that moment— make sure to bind all models before dumping the schema.

Because the output file contains real runtime values, call DumpSchema() only in development builds. Store the generated schema.json alongside the UI files in source control so designers can re-run the generator without compiling native code.

Generating the Mockup

Run gen-mock-data.py from the SDK root folder (the folder that holds tools/, not from inside tools/scripts), passing the schema file and the UI output folder:

Shell
python tools/scripts/gen-mock-data.py ui/schema.json -o ui/

The script writes several files into the UI folder beside the pages:

ui/
  hud.html            your page
  schema.json         saved by the app
  ul-mock.js          rewritten on every run
  ul-mock-runtime.js  rewritten on every run
  ul-schema.js        rewritten on every run
  mock-data.js        written once; your mock data

Files with the ul- prefix are generated and rewritten on every run— don't edit them. The ul-schema.js file holds the schema along with the values saved from native code.

The script creates mock-data.js only when it doesn't already exist. You can define mock data in this file, and re-running the generator preserves your edits.

Loading the Script in a Browser

Add a single script tag to the <head> of each HTML page:

HTML
<script src="ul-mock.js"></script>

Open the page directly from disk in a browser using a file:// URL without running a web server. Once the scripts load, the page renders using the values saved in the schema.

👍 Leaving the Script in Shipped Pages

You can keep the ul-mock.js tag in shipped pages. The script is a small loader that does nothing inside Ultralight, so native data takes over without loading mock files or the runtime.

Editing Mock Data

The generated mock-data.js contains a starter call to ul.mock() and commented action handlers:

JavaScript
// Mock data for the pages in this folder, loaded by ul-mock.js in a browser
// (never in Ultralight). A page can add its own layer in a
// <script type="ul-mock"> block. A scenario goes in scenarios/<name>.js,
// shown when the page is opened with ?ul-scenario=<name>:
// ul.scenario("<name>", { ... });
// The mockup starts from the app's values saved in ul-schema.js; values
// you set here go on top.

ul.mock({});

// An action without a handler logs to the console. Uncomment a handler to
// give it behavior.
// ul.onAction("player.items.drop", (item, player) => {});
// ul.onAction("player.usePotion", (player) => {});

Provide only the fields you want to override:

JavaScript
ul.mock({
  player: {
    name: "Kai",
    items: [
      { name: "Potion", count: 3 },
      { name: "Shield", count: 1 },
    ],
  },
});

Mock data applies in layers over the values saved in the schema. Objects merge field by field, while arrays and primitive values replace earlier values entirely. List rows in mock data do not require a key field.

Modifying Data in the Console

You can inspect and modify live models directly in the browser's developer console using ul.data:

JavaScript
ul.data.player.stats.health = 15;

Changing a property on ul.data updates the page immediately.

Simulating Native Code

Handling Actions

Clicking an element bound with ul-on logs a message to the browser console when no handler is registered:

[ul-mock] action: player.usePotion

Register a handler with ul.onAction(), combining the model name and action name:

JavaScript
ul.onAction("player.usePotion", (player) => {
  player.stats.health = 100;
});

The callback receives the model instance, allowing you to update its state in response to user input.

Handling Row Actions

For actions inside a list template, prefix the action path from Binding Lists with the model name:

JavaScript
ul.onAction("player.items.drop", (item, player) => {
  player.items.splice(player.items.indexOf(item), 1);
});

The handler receives the clicked item row first, followed by the parent model. One handler handles clicks across all rows in the list.

Handling Form Control Edits

Form controls bound with ul-value update mock data automatically, or you can validate changes with ul.onChange(), matching native handlers from Binding Form Controls:

JavaScript
ul.onChange("settings.volume", (volume, settings) => {
  if (volume <= 0.9)
    settings.volume = volume;  // louder than 0.9 snaps back
});

The handler receives the new value and the model. To accept the change, store the value on the model— if you don't store it, the input control reverts to its previous value.

Defining Formatters

Register formatting pipes used in markup with ul.defineFormat():

JavaScript
ul.defineFormat("comma", (value) => value.toLocaleString("en-US"));

Formatters use the same names as native DefineFormat() calls. If a page uses a format pipe without a registered formatter, the page displays the raw value and logs a warning to the console.

Adding Page-Specific Mock Data

To set mock data for a single page, add an inline script block with type="ul-mock":

HTML
<script src="ul-mock.js"></script>
<script type="ul-mock">
  ul.mock({ player: { gold: 50 } });  // only this page
</script>

Browsers ignore custom script types, but ul-mock.js evaluates the block after loading mock-data.js.

Loading Page Data from a File

You can also load page-specific mock files using the data-mock attribute:

HTML
<script src="ul-mock.js" data-mock="shop-data.js"></script>

Pass space-separated file paths relative to the HTML page. Mock layers apply in sequence: mock-data.js loads first, followed by any data-mock files, and finally inline type="ul-mock" blocks. Values set in later layers override earlier ones.

🚧 Isolating Mockup Code

Put mock setup only in mock-data.js, data-mock files, type="ul-mock" blocks, or scenario files. Code in a standard <script> tag runs inside the shipped application as well. Never edit generated ul-* files, as the generator overwrites them on every run.

Creating Scenarios

A scenario is a named set of mock data that configures the page for a specific situation (such as an empty inventory):

JavaScript
// scenarios/empty-bag.js
ul.scenario("empty-bag", { player: { items: [] } });

Save the file under scenarios/<name>.js beside ul-mock.js, then open the page with the ul-scenario query parameter:

hud.html?ul-scenario=empty-bag

The runtime loads the scenario file and applies its data on top of the page's mock data.

Switching Scenarios in Script

Switch scenarios dynamically from JavaScript or the console using ul.setScenario():

JavaScript
await ul.setScenario("empty-bag");
await ul.setScenario(null);

Calling setScenario() resets models back to the page's base mock data before applying the new scenario. Passing null removes the active scenario and restores the base mock data.

Adding Behavior to Scenarios

Pass a setup function to ul.scenario() to register custom actions and handlers alongside mock data:

JavaScript
ul.scenario("rich", () => {
  ul.mock({ player: { gold: 1000000 } });
  ul.onAction("player.usePotion", (player) => {
    player.gold -= 100;
  });
});

Handlers, formatters, and changes registered inside a scenario function remain active only while that scenario is displayed.

Capturing Scenarios from the App

You can capture live game situations by dumping a schema file from native code and converting it into a scenario:

Shell
python tools/scripts/gen-mock-data.py scenario capture.json --name boss-fight -o ui/scenarios/

The command writes a scenario file containing every value captured in the schema. The script won't overwrite an existing scenario file.

Updating After Schema Changes

When native types change, dump the schema again from native code and re-run the generator— the output lists the fields added and removed since the previous run:

Wrote ui/ul-mock.js
Wrote ui/ul-mock-runtime.js
Wrote ui/ul-schema.js
Added player.stats.defense (integer).
Removed player.stats.armor (integer).
Kept ui/mock-data.js (it already exists; delete it to start over).

Added fields take the values saved in the new schema.

If markup or mock data still names a removed field, the browser console reports an error when the page loads.

A scenario file that references a removed field reports the error only when that scenario is displayed.

Checking for Schema Errors

Checking in the Browser

When a page loads in the browser, the runtime validates mock data, handler names, and markup bindings against the schema:

[ul-mock] 2 errors (see ul.errors)
  mock-data.js: mock data for 'player': no field 'armor' in Stats
  hud.html: {{player.stats.armor}}: 'Stats' has no field 'armor'.

The console summary reports every mismatch found during load once the page finishes rendering, naming the file or page where each problem occurred.

Scenarios are validated only when displayed through the ul-scenario query parameter or ul.setScenario(). An unshown scenario reports nothing.

When the schema rejects a value because of an unknown field or the wrong data type, the runtime discards that value— the field keeps its placeholder or previous value. Valid fields in the same mock layer still apply.

Errors that happen after load, such as invalid property assignments in the console or inside action handlers, log to the console immediately.

Automated tests can inspect ul.errors, which records every error in order as an object with source and message properties.

Checking Pages in a Build Step

To validate markup against the schema without opening a browser, run gen-mock-data.py with the check command:

Shell
python tools/scripts/gen-mock-data.py check ui/hud.html ui/shop.html --schema ui/schema.json

The tool prints one line for each problem found, naming the file and line number:

ui/hud.html:10: error: {{player.stats.armor}}: 'Stats' has no field 'armor'.

The command exits with a non-zero status when it finds an error.

Adding --strict causes warnings to fail the command with a non-zero status as well.

The tool checks only the HTML files passed on the command line— it does not inspect mock-data.js.

The script does not expand wildcards, so list each file explicitly. Patterns like ui/*.html work only in shells that expand wildcards before running the command (such as macOS and Linux shells), not in Command Prompt or PowerShell.

To catch markup warnings when running the page in native code, see Developer Mode and Diagnostics.

Waiting for the First Update

Because mock data applies asynchronously after the HTML is parsed, use ul.ready before reading model data from page scripts:

JavaScript
ul.ready.then(() => startIntroAnimation());

The ul.ready promise resolves after the initial data update completes, both in a browser and inside Ultralight.

Mocking Before Native Types Exist

Run gen-mock-data.py without a schema file to generate the mockup files before native types exist:

Shell
python tools/scripts/gen-mock-data.py -o ui/

Without a schema, write model values by hand in mock-data.js— the runtime infers value types directly from the mock data.

Actions on the page require a handler registered with ul.onAction(), since no schema exists to declare them.

When native types are ready, dump the schema from native code and run the generator again with the schema file. The generator preserves mock-data.js, and the browser console reports any fields or actions that do not match the schema.

📘 Differences from the App

Handlers run in JavaScript on the page rather than native code, and schema validators never run in the mockup. A form control without a ul.onChange() handler modifies mock data directly, while native code without an OnChange() handler declines the change and snaps the control back. Rows in a keyed list match by object identity— replacing an item with an equal copy re-creates its elements.