Skip to content

Commit

Permalink
form select
Browse files Browse the repository at this point in the history
  • Loading branch information
lovasoa committed Feb 5, 2025
1 parent 69b3207 commit 8ba15fd
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 9 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
- Update ApexCharts to [v4.4.0](https://github.com/apexcharts/apexcharts.js/releases/tag/v4.4.0): fixes multiple small bugs in the chart component.
- Add a new `auto_submit` parameter to the form component. When set to true, the form will be automatically submitted when the user changes any of its fields, and the page will be reloaded with the new value. The validation button is removed.
- This is useful to quickly create filters at the top of a dashboard or report page, that will be automatically applied when the user changes them.
- New `options_source` parameter in the form component. This allows to dynamically load options for dropdowns from a different SQL file.
- This allows easily implementing autocomplete for form fields with a large number of possible options.

## 0.32.1 (2025-01-03)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
select 'json' as component;

select name as value, name as label
from component
where name like '%' || $search || '%';
45 changes: 37 additions & 8 deletions examples/official-site/sqlpage/migrations/01_documentation.sql
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ FROM fruits
('form', '### Multi-select
You can authorize the user to select multiple options by setting the `multiple` property to `true`.
This creates a more compact (but arguably less user-friendly) alternative to a series of checkboxes.
In this case, you should add square brackets to the name of the field.
In this case, you should add square brackets to the name of the field (e.g. `''my_field[]'' as name`).
The target page will then receive the value as a JSON array of strings, which you can iterate over using
- the `json_each` function [in SQLite](https://www.sqlite.org/json1.html) and [Postgres](https://www.postgresql.org/docs/9.3/functions-json.html),
- the [`OPENJSON`](https://learn.microsoft.com/fr-fr/sql/t-sql/functions/openjson-transact-sql?view=sql-server-ver16) function in Microsoft SQL Server.
Expand All @@ -388,14 +388,20 @@ The target page could then look like this:
```sql
insert into best_fruits(id) -- INSERT INTO ... SELECT ... runs the SELECT query and inserts the results into the table
select CAST(value AS integer) as id -- all values are transmitted by the browser as strings
from json_each($preferred_fruits); -- json_each returns a table with a "value" column for each element in the JSON array
from json_each($my_field); -- in SQLite, json_each returns a table with a "value" column for each element in the JSON array
```
### Example multiselect generated from a database table
As an example, if you have a table of all possible options (`my_options(id int, label text)`),
and another table that contains the selected options per user (`my_user_options(user_id int, option_id int)`),
you can use a query like this to generate the multi-select field:
If you have a table of all possible options (`my_options(id int, label text)`),
and want to generate a multi-select field from it, you have two options:
- if the number of options is not too large, you can use the `options` parameter to return them all as a JSON array in the SQL query
- if the number of options is large (e.g. more than 1000), you can use `options_source` to load options dynamically from a different SQL query as the user types
#### Embedding all options in the SQL query
Let''s say you have a table that contains the selected options per user (`my_user_options(user_id int, option_id int)`).
You can use a query like this to generate the multi-select field:
```sql
select ''select'' as type, true as multiple, json_group_array(json_object(
Expand All @@ -408,10 +414,33 @@ left join my_user_options
on my_options.id = my_user_options.option_id
and my_user_options.user_id = $user_id
```
This will generate a json array of objects, each containing the label, value and selected status of each option.
#### Loading options dynamically from a different SQL query with `options_source`
If the `my_options` table has a large number of rows, you can use the `options_source` parameter to load options dynamically from a different SQL query as the user types.
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.
##### `options_source.sql`
```sql
select ''json'' as component;
select id as value, label as label
from my_options
where label like $search || ''%'';
```
##### `form`
', json('[{"component":"form", "action":"examples/show_variables.sql", "reset": "Reset"},
{"label": "Fruits", "name": "fruits[]", "type": "select", "multiple": true, "create_new":true, "placeholder": "Good fruits...", "searchable": true, "description": "press ctrl to select multiple values", "options":
"[{\"label\": \"Orange\", \"value\": 0, \"selected\": true}, {\"label\": \"Apple\", \"value\": 1}, {\"label\": \"Banana\", \"value\": 3, \"selected\": true}]"}
]')),
{"name": "component", "type": "select",
"options_source": "examples/from_component_options_source.sql",
"description": "Start typing the name of a component like ''map'' or ''form''..."
}]')),
('form', 'This example illustrates the use of the `radio` type.
The `name` parameter is used to group the radio buttons together.
The `value` parameter is used to set the value that will be submitted when the user selects the radio button.
Expand Down
3 changes: 2 additions & 1 deletion sqlpage/templates/form.handlebars
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,11 @@
{{~#if autofocus}} autofocus {{/if~}}
{{~#if disabled}}disabled {{/if~}}
{{~#if multiple}} multiple {{/if~}}
{{~#if (or dropdown searchable)}}
{{~#if (or dropdown searchable options_source)}}
data-pre-init="select-dropdown"
data-sqlpage-js="{{static_path 'tomselect.js'}}"
{{/if~}}
{{~#if options_source}} data-options_source="{{options_source}}" {{/if~}}
{{~#if placeholder}} placeholder="{{placeholder}}" {{/if~}}
{{~#if create_new}} data-create_new={{create_new}} {{/if~}}
>
Expand Down
44 changes: 44 additions & 0 deletions sqlpage/tomselect.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ function sqlpage_select_dropdown() {
// By default, TomSelect will not retain the focus if s is already focused
// This is a workaround to fix that
const is_focused = s === document.activeElement;

const tom = new TomSelect(s, {
load: sqlpage_load_options_source(s.dataset.options_source),
valueField: "value",
labelField: "label",
searchField: "label",
create: s.dataset.create_new,
maxOptions: null,
onItemAdd: function () {
Expand All @@ -32,4 +37,43 @@ function sqlpage_select_dropdown() {
}
}

function sqlpage_load_options_source(options_source) {
if (!options_source) return;
return async (query, callback) => {
const err = (label) =>
callback([{ label, value: "" }]);
const resp = await fetch(
`${options_source}?search=${encodeURIComponent(query)}`,
);
if (!resp.ok) {
return err(
`Error loading options from "${options_source}": ${resp.status} ${resp.statusText}`,
);
}
const resp_type = resp.headers.get("content-type");
if (resp_type !== "application/json") {
return err(
`Invalid response type: ${resp_type} from "${options_source}". Make sure to use the 'json' component in the SQL file that generates the options.`,
);
}
const results = await resp.json();
if (!Array.isArray(results)) {
return err(
`Invalid response from "${options_source}". The response must be an array of objects with a 'label' and a 'value' property.`,
);
}
if (results.length === 1 && results[0].error) {
return err(results[0].error);
}
if (results.length > 0) {
const keys = Object.keys(results[0]);
if (keys.length !== 2 || !keys.includes("label") || !keys.includes("value")) {
return err(
`Invalid response from "${options_source}". The response must be an array of objects with a 'label' and a 'value' property. Got: ${JSON.stringify(results[0])} in the first object instead.`,
);
}
}
callback(results);
};
}
add_init_fn(sqlpage_select_dropdown);

0 comments on commit 8ba15fd

Please sign in to comment.