chore: import upstream snapshot with attribution
CI / compile_and_lint (push) Failing after 0s
CI / docker_build (linux/amd64, -linux-amd64-duckdb, duckdb) (push) Failing after 0s
CI / docker_build (linux/arm64, -linux-arm64, minimal) (push) Failing after 2s
CI / docker_build (linux/arm64, -linux-arm64-duckdb, duckdb) (push) Failing after 1s
CI / docker_build (linux/amd64, minimal) (push) Failing after 1s
CI / test (, sqlite, sqlite::memory:) (push) Has been skipped
CI / test (mssql, mssql, mssql://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / test (mysql, mysql, mysql://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / test (oracle, oracle, Driver=Oracle 21 ODBC driver;Dbq=//127.0.0.1:1521/FREEPDB1;Uid=root;Pwd=Password123!) (push) Has been skipped
CI / test (postgres, odbc, Driver=PostgreSQL Unicode;Server=127.0.0.1;Port=5432;Database=sqlpage;UID=root;PWD=Password123!, true) (push) Has been skipped
CI / test (postgres, postgres, postgres://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / playwright (push) Has been skipped
CI / docker_build (linux/arm/v7, -linux-arm-v7, minimal) (push) Failing after 0s
CI / hurl_examples (push) Failing after 8s
deploy website / deploy_official_site (push) Failing after 1s
CI / hurl (${{ matrix.example }}) (push) Has been skipped
CI / docker_push (duckdb) (push) Has been cancelled
CI / docker_push (minimal) (push) Has been cancelled
CI / windows_test (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:31:57 +08:00
commit d718c5a372
986 changed files with 74597 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
# Custom form component
This example shows how to create a simple custom component in handlebars, and call it from SQL.
It uses MySQL, but it should be easy to adapt to other databases.
The only MySQL-specific features used here are:
- `json_table`, which is supported by MariaDB and MySQL 8.0 and later,
- MySQL's `json_merge` function.
Both [have analogs in other databases](https://sql-page.com/blog.sql?post=JSON%20in%20SQL%3A%20A%20Comprehensive%20Guide).
![screenshot](screenshot.png)
## Key features illustrated in this example
- How to create a custom component in handlebars, with dynamic behavior implemented in JavaScript
- How to manage multiple-option select boxes, with pre-selected items, and multiple choices
- Including a common menu between different pages using a `shell.sql` file, the dynamic component, and the `sqlpage.run_sql` function.
+40
View File
@@ -0,0 +1,40 @@
select
'dynamic' as component,
sqlpage.run_sql ('shell.sql') as properties;
-- this does the same thing as index.sql, but uses the normal form component instead of our fancy dual-list component
select
'form' as component,
'form_action.sql' as action;
select
'select' as type,
true as searchable,
true as multiple,
'selected_items[]' as name,
'Users in this group' as label,
-- JSON_MERGE combines two JSON documents:
-- 1. A JSON object with an empty label
-- 2. An array of user objects created by JSON_ARRAYAGG
JSON_MERGE (
-- Creates a simple JSON object with a single empty property {"label": ""}
JSON_OBJECT ('label', ''),
-- JSON_ARRAYAGG takes multiple rows and combines them into a JSON array
-- Each element in the array is a JSON object created by json_object()
JSON_ARRAYAGG (
-- Creates a JSON object for each user with:
-- - {"label": "the user's name", "value": "the user's ID", "selected": true } (if the user is in the group)
json_object (
'label',
users.name,
'value',
users.id,
'selected',
group_members.group_id is not null -- the left join creates NULLs for users not in the group
)
)
) as options
from
users
left join group_members on users.id = group_members.user_id
and group_members.group_id = 1;
@@ -0,0 +1,17 @@
services:
web:
image: lovasoa/sqlpage:main # main is cutting edge, use sqlpage/SQLPage:latest for the latest stable version
ports:
- "8080:8080"
volumes:
- .:/var/www
- ./sqlpage:/etc/sqlpage
depends_on:
- db
environment:
DATABASE_URL: mysql://root:secret@db/sqlpage
db: # The DB environment variable can be set to "mariadb" or "postgres" to test the code with different databases
image: mysql:9 # support for json_table was added in mariadb 10.6
environment:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: sqlpage
@@ -0,0 +1,28 @@
-- remove all existing members from this group
delete from group_members where group_id = 1;
-- add the selected members to this group
-- This query takes a JSON array and converts it to rows
-- :selected_items contains a JSON array of user IDs, e.g. ["1", "2", "3"], generated by SQLPage from the multiple-select box answers posted by the browser
-- json_table breaks down the JSON array into individual rows
-- '$[*]' means "look at each element in the root array"
-- columns (id int path '$') extracts each array element as an integer into a column named 'id'
-- The result is a temporary table with one integer column (id) and one row per array element
insert into group_members (group_id, user_id)
select 1, id
from json_table(
:selected_items,
'$[*]' columns (id int path '$')
) as submitted_items;
select 'alert' as component, 'Group members successfully updated !' as title, 'success' as color;
select 'list' as component, 'Users in this group' as title;
select name as title, email as description
from users
join group_members on users.id = group_members.user_id
where group_members.group_id = 1;
select 'button' as component;
select 'Go back' as title, 'index.sql' as link;
+17
View File
@@ -0,0 +1,17 @@
-- include the common menu
select 'dynamic' as component, sqlpage.run_sql('shell.sql') as properties;
-- Call our custom component from ./sqlpage/templates/dual-list.handlebars
select
'dual-list' as component,
'form_action.sql' as action;
-- This SQL query returns the list of users, with a boolean indicating if they are in the group
select
id,
name as label,
group_members.group_id is not null as selected
from
users
left join group_members on users.id = group_members.user_id
and group_members.group_id = 1;
Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

+5
View File
@@ -0,0 +1,5 @@
select
'shell' as component,
'Custom form component' as title,
'index' as menu_item,
'basic' as menu_item;
@@ -0,0 +1,40 @@
create table users (
id int primary key auto_increment,
name varchar(255) not null,
email varchar(255) not null
);
create table `groups` (
id int primary key auto_increment,
name varchar(255) not null
);
create table group_members (
group_id int not null,
user_id int not null,
primary key (group_id, user_id),
foreign key (group_id) references `groups` (id),
foreign key (user_id) references users (id)
);
INSERT INTO users (id, name, email) VALUES
(1, 'John Smith', 'john@email.com'),
(2, 'Jane Doe', 'jane@email.com'),
(3, 'Bob Wilson', 'bob@email.com'),
(4, 'Mary Johnson', 'mary@email.com'),
(5, 'James Brown', 'james@email.com'),
(6, 'Sarah Davis', 'sarah@email.com'),
(7, 'Michael Lee', 'michael@email.com'),
(8, 'Lisa Anderson', 'lisa@email.com'),
(9, 'David Miller', 'david@email.com'),
(10, 'Emma Wilson', 'emma@email.com');
INSERT INTO `groups` (id, name) VALUES
(1, 'Team Alpha');
INSERT INTO group_members (group_id, user_id) VALUES
(1, 1),
(1, 2),
(1, 3),
(1, 4),
(1, 5);
@@ -0,0 +1,126 @@
{{!-- This is a form that will post data to the URL specified in the top-level 'action' property coming from the SQL query --}}
<form method="POST" action="{{action}}">
{{!-- Create a row with centered content and spacing between items --}}
<div class="row justify-content-center align-items-center g-4">
{{!-- Left List Box: 5 columns wide (out of the 12 made available by bootstrap) --}}
<div class="col-5">
{{!-- Card with no border and subtle shadow --}}
<div class="card border-0 shadow-sm">
{{!-- Card header with white background, no border, semibold font, and secondary text color --}}
<div class="card-header bg-white border-bottom fw-semibold text-secondary py-3">
Available Items
</div>
<div class="card-body p-3">
{{!-- Multiple-select dropdown list, 300px tall --}}
<select class="form-select" id="leftList" multiple style="height: 300px">
{{!-- Loop through each row of data returned by the second SQL query (row-level properties are available as variables) --}}
{{#each_row}}
{{!-- Create an option for each item, marking it selected if the 'selected' property is true --}}
<option class="py-2 px-3 rounded-1 my-1" value="{{id}}" {{#if selected}}selected{{/if}}>{{label}}</option>
{{/each_row}}
</select>
</div>
</div>
</div>
{{!-- Middle section with transfer buttons (auto-sized column) --}}
<div class="col-auto d-flex flex-column gap-2">
{{!-- Right arrow button (→) to move items to selected list --}}
<button type="button" class="btn btn-outline-primary rounded-circle p-0 d-flex align-items-center justify-content-center"
id="moveRight"
title="Move to selected"
style="width: 40px; height: 40px">
{{!-- icon_img is a helper that renders an icon image from tabler.io/icons --}}
{{~icon_img 'arrow-narrow-right' 20~}}
</button>
{{!-- Left arrow button (←) to remove items from selected list --}}
<button type="button" class="btn btn-outline-primary rounded-circle p-0 d-flex align-items-center justify-content-center"
id="moveLeft"
title="Remove from selected"
style="width: 40px; height: 40px">
{{~icon_img 'arrow-narrow-left' 20~}}
</button>
</div>
{{!-- Right List Box (5 columns wide) --}}
<div class="col-5">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-bottom fw-semibold text-secondary py-3">
Selected Items
</div>
<div class="card-body p-3">
{{!-- Multiple-select dropdown that will contain selected items. The name attribute makes it submit as an array --}}
<select class="form-select" id="rightList" name="selected_items[]" multiple style="height: 300px"></select>
</div>
</div>
</div>
{{!-- Submit Button Section (full width) --}}
<div class="col-12 text-center mt-4">
<input type="submit" class="btn btn-primary px-4 py-2 fw-semibold shadow-sm"
id="submitBtn"
disabled
value="Submit Selection">
</div>
</div>
</form>
{{!-- JavaScript code with CSP (Content Security Policy) nonce for security --}}
<script nonce="{{@csp_nonce}}">
// Get references to both list elements
// If we wanted to be able to include this component multiple times on the same page,
// we would need to generate IDs like "rightList1", "leftList1", etc. using a template string.like "rightList{{@component_index}}"
const rightList = document.getElementById('rightList');
const leftList = document.getElementById('leftList');
/**
* Moves selected items from one list to another while maintaining alphabetical order
* @param {HTMLSelectElement} fromList - The list to take items from
* @param {HTMLSelectElement} toList - The list to add items to
*/
function transferItems(fromList, toList) {
// Deselect all items in the destination list
for (const option of toList.options) option.selected = false;
// Combine existing options with newly selected ones
const newOptions = [...toList.options, ...fromList.selectedOptions];
// Sort the combined options alphabetically
newOptions.sort((a, b) => a.text.localeCompare(b.text));
// Add all options to the destination list
// Since an element can only exist once in the page,
// this will automatically remove the options from the source list
toList.append(...newOptions);
// Focus the destination list and update submit button state
toList.focus();
updateSubmitButton();
}
/**
* Enable/disable submit button based on whether there are items in the right list
*/
function updateSubmitButton() {
submitBtn.disabled = rightList.options.length === 0;
}
/**
* Ensure all items in right list are selected before form submission
* This is necessary because unselected options aren't included in form data
*/
function handleSubmit() {
for (const option of rightList.options) option.selected = true;
}
// Set initial state of submit button
updateSubmitButton();
// Move any pre-selected items to the right list when page loads
transferItems(leftList, rightList);
// Set up click handlers for the transfer buttons and form submission
document.getElementById('moveRight').addEventListener('click', transferItems.bind(null, leftList, rightList));
document.getElementById('moveLeft').addEventListener('click', transferItems.bind(null, rightList, leftList));
document.querySelector('form').addEventListener('submit', handleSubmit);
</script>
+23
View File
@@ -0,0 +1,23 @@
GET http://localhost:8080/index.sql
HTTP 200
[Asserts]
body contains "Available Items"
body contains "Selected Items"
body contains "John Smith"
body contains "Jane Doe"
body not contains "An error occurred"
POST http://localhost:8080/form_action.sql
[FormParams]
selected_items[]: 6
selected_items[]: 8
HTTP 200
[Asserts]
body contains "Group members successfully updated"
body contains "Users in this group"
body contains "Sarah Davis"
body contains "sarah@email.com"
body contains "Lisa Anderson"
body contains "lisa@email.com"
body not contains "John Smith"
body not contains "An error occurred"