From 5e11a4e16545ac36cee7facc113d49c0b00e487a Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Mon, 8 Jun 2026 12:17:44 +0200 Subject: [PATCH] document select options_source behavior --- .../sqlpage/migrations/01_documentation.sql | 7 +- tests/end-to-end/official-site.spec.ts | 76 +++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/examples/official-site/sqlpage/migrations/01_documentation.sql b/examples/official-site/sqlpage/migrations/01_documentation.sql index ef09b9a8..6294ebed 100644 --- a/examples/official-site/sqlpage/migrations/01_documentation.sql +++ b/examples/official-site/sqlpage/migrations/01_documentation.sql @@ -276,7 +276,8 @@ INSERT INTO parameter(component, name, description, type, top_level, optional) S ('label', 'A friendly name for the text field to show to the user.', 'TEXT', FALSE, TRUE), ('placeholder', 'A placeholder text that will be shown in the field when is is empty.', 'TEXT', FALSE, TRUE), ('value', 'A default value that will already be present in the field when the user loads the page.', 'TEXT', FALSE, TRUE), - ('options', 'A json array of objects containing the label and value of all possible options of a select field. Used only when type=select. JSON objects in the array can contain the properties "label", "value" and "selected".', 'JSON', FALSE, TRUE), + ('options', 'A json array of objects containing the label and value of initial options of a select field. Used only when type=select. JSON objects in the array can contain the properties "label", "value" and "selected".', 'JSON', FALSE, TRUE), + ('options_source', 'Only for inputs of type `select`. URL of a SQL file that returns JSON search results for the dropdown. The SQL file receives the search text as `$search` and must return an array of objects with exactly `label` and `value`. Use `options` for options that should be available before the user searches.', 'URL', FALSE, TRUE), ('required', 'Set this to true to prevent the form contents from being sent if this field is left empty by the user.', 'BOOLEAN', FALSE, TRUE), ('min', 'The minimum value to accept for an input of type number', 'REAL', FALSE, TRUE), ('max', 'The maximum value to accept for an input of type number', 'REAL', FALSE, TRUE), @@ -439,6 +440,8 @@ If the `my_options` table has a large number of rows, you can use the `options_s We''ll write a second SQL file, `options_source.sql`, that will receive the user''s search string as a parameter named `$search`, and return a json array of objects, each containing the label and value of each option. +When both `options` and `options_source` are set, the local `options` are loaded first. Search results from `options_source` are loaded into the same option list; a result with the same `value` updates the existing option. This is useful to set a default preselected value with `options_source`. + ##### `options_source.sql` ```sql @@ -453,6 +456,8 @@ where label like $search || ''%''; ', json('[{"component":"form", "action":"examples/show_variables.sql", "reset": "Reset"}, {"name": "component", "type": "select", + "value": "form", + "options": [{"label": "Form", "value": "form"}], "options_source": "examples/from_component_options_source.sql", "description": "Start typing the name of a component like ''map'' or ''form''..." }]')), diff --git a/tests/end-to-end/official-site.spec.ts b/tests/end-to-end/official-site.spec.ts index 8b2c6b3f..f4f5618d 100644 --- a/tests/end-to-end/official-site.spec.ts +++ b/tests/end-to-end/official-site.spec.ts @@ -206,6 +206,82 @@ test("form component documentation", async ({ page }) => { ).toBeVisible(); }); +test("form select combines initial options with remote search results", async ({ + page, +}) => { + await page.goto(`${BASE}/component.sql?component=form`); + + const select = page + .locator( + 'select[data-options_source="examples/from_component_options_source.sql"]', + ) + .first(); + await expect(select).toBeAttached(); + await page.waitForFunction( + () => + !!document.querySelector( + 'select[data-options_source="examples/from_component_options_source.sql"]', + )?.tomselect, + ); + + const initialState = await select.evaluate((element) => { + const tomselect = element.tomselect; + return { + value: tomselect.getValue(), + labels: Object.fromEntries( + Object.entries(tomselect.options).map(([value, option]) => [ + value, + option.label, + ]), + ), + }; + }); + expect(initialState).toEqual({ + value: "form", + labels: { form: "Form" }, + }); + + await select.evaluate((element) => element.tomselect.focus()); + await page.keyboard.type("form"); + await page.waitForResponse((response) => + response + .url() + .includes("examples/from_component_options_source.sql?search=form"), + ); + await expect + .poll(async () => + select.evaluate((element) => ({ + value: element.tomselect.getValue(), + formLabel: element.tomselect.options.form?.label, + })), + ) + .toEqual({ + value: "form", + formLabel: "form", + }); + + await select.evaluate((element) => element.tomselect.setTextboxValue("")); + await page.keyboard.type("map"); + await page.waitForResponse((response) => + response + .url() + .includes("examples/from_component_options_source.sql?search=map"), + ); + await expect + .poll(async () => + select.evaluate((element) => ({ + value: element.tomselect.getValue(), + formLabel: element.tomselect.options.form?.label, + mapLabel: element.tomselect.options.map?.label, + })), + ) + .toEqual({ + value: "form", + formLabel: "form", + mapLabel: "map", + }); +}); + test("modal", async ({ page }) => { await page.goto(`${BASE}/documentation.sql?component=modal#component`); // get the button that opens the modal