Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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
Expand All @@ -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''..."
}]')),
Expand Down
76 changes: 76 additions & 0 deletions tests/end-to-end/official-site.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLSelectElement>(
'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
Expand Down
Loading