Skip to content

Commit 243c1a5

Browse files
committed
add a detailed example using MySQL syntax illustrating the usage of custom components
1 parent 552ef26 commit 243c1a5

File tree

9 files changed

+283
-0
lines changed

9 files changed

+283
-0
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Custom form component
2+
3+
This example shows how to create a simple custom component in handlebars, and use from SQL.
4+
5+
It uses MySQL, but does not use any MySQL-specific features, so it should be easy to adapt to other databases.
6+
7+
![screenshot](screenshot.png)
8+
9+
10+
## Key features illustrated in this example
11+
12+
- How to create a custom component in handlebars, with dynamic behavior implemented in JavaScript
13+
- How to manage multiple-option select boxes, with pre-selected items, and multiple choices
14+
- Including a common menu between different pages using a `shell.sql` file, the dynamic component, and the `sqlpage.run_sql` function.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
select
2+
'dynamic' as component,
3+
sqlpage.run_sql ('shell.sql') as properties;
4+
5+
-- this does the same thing as index.sql, but uses the normal form component instead of our fancy dual-list component
6+
select
7+
'form' as component,
8+
'form_action.sql' as action;
9+
10+
select
11+
'select' as type,
12+
true as searchable,
13+
true as multiple,
14+
'selected_items[]' as name,
15+
'Users in this group' as label,
16+
-- JSON_MERGE combines two JSON documents:
17+
-- 1. A JSON object with an empty label
18+
-- 2. An array of user objects created by JSON_ARRAYAGG
19+
JSON_MERGE (
20+
-- Creates a simple JSON object with a single empty property {"label": ""}
21+
JSON_OBJECT ('label', ''),
22+
-- JSON_ARRAYAGG takes multiple rows and combines them into a JSON array
23+
-- Each element in the array is a JSON object created by json_object()
24+
JSON_ARRAYAGG (
25+
-- Creates a JSON object for each user with:
26+
-- - {"label": "the user's name", "value": "the user's ID", "selected": true } (if the user is in the group)
27+
json_object (
28+
'label',
29+
users.name,
30+
'value',
31+
users.id,
32+
'selected',
33+
group_members.group_id is not null -- the left join creates NULLs for users not in the group
34+
)
35+
)
36+
) as options
37+
from
38+
users
39+
left join group_members on users.id = group_members.user_id
40+
and group_members.group_id = 1;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
services:
2+
web:
3+
image: lovasoa/sqlpage:main # main is cutting edge, use lovasoa/sqlpage:latest for the latest stable version
4+
ports:
5+
- "8080:8080"
6+
volumes:
7+
- .:/var/www
8+
- ./sqlpage:/etc/sqlpage
9+
depends_on:
10+
- db
11+
environment:
12+
DATABASE_URL: mysql://root:secret@db/sqlpage
13+
db: # The DB environment variable can be set to "mariadb" or "postgres" to test the code with different databases
14+
ports:
15+
- "3306:3306"
16+
image: mysql:9 # support for json_table was added in mariadb 10.6
17+
environment:
18+
MYSQL_ROOT_PASSWORD: secret
19+
MYSQL_DATABASE: sqlpage
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
-- remove all existing members from this group
2+
delete from group_members where group_id = 1;
3+
4+
-- add the selected members to this group
5+
-- This query takes a JSON array and converts it to rows
6+
-- :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
7+
-- json_table breaks down the JSON array into individual rows
8+
-- '$[*]' means "look at each element in the root array"
9+
-- columns (id int path '$') extracts each array element as an integer into a column named 'id'
10+
-- The result is a temporary table with one integer column (id) and one row per array element
11+
insert into group_members (group_id, user_id)
12+
select 1, id
13+
from json_table(
14+
:selected_items,
15+
'$[*]' columns (id int path '$')
16+
) as submitted_items;
17+
18+
select 'alert' as component, 'Group members successfully updated !' as title, 'success' as color;
19+
20+
select 'list' as component, 'Users in this group' as title;
21+
22+
select name as title, email as description
23+
from users
24+
join group_members on users.id = group_members.user_id
25+
where group_members.group_id = 1;
26+
27+
select 'button' as component;
28+
select 'Go back' as title, 'index.sql' as link;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
-- include the common menu
2+
select 'dynamic' as component, sqlpage.run_sql('shell.sql') as properties;
3+
4+
-- Call our custom component from ./sqlpage/templates/dual-list.handlebars
5+
select
6+
'dual-list' as component,
7+
'form_action.sql' as action;
8+
9+
-- This SQL query returns the list of users, with a boolean indicating if they are in the group
10+
select
11+
id,
12+
name as label,
13+
group_members.group_id is not null as selected
14+
from
15+
users
16+
left join group_members on users.id = group_members.user_id
17+
and group_members.group_id = 1;
34 KB
Loading
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
select
2+
'shell' as component,
3+
'Custom form component' as title,
4+
'index' as menu_item,
5+
'basic' as menu_item;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
create table users (
2+
id int primary key auto_increment,
3+
name varchar(255) not null,
4+
email varchar(255) not null
5+
);
6+
7+
create table `groups` (
8+
id int primary key auto_increment,
9+
name varchar(255) not null
10+
);
11+
12+
create table group_members (
13+
group_id int not null,
14+
user_id int not null,
15+
primary key (group_id, user_id),
16+
foreign key (group_id) references `groups` (id),
17+
foreign key (user_id) references users (id)
18+
);
19+
20+
INSERT INTO users (id, name, email) VALUES
21+
(1, 'John Smith', '[email protected]'),
22+
(2, 'Jane Doe', '[email protected]'),
23+
(3, 'Bob Wilson', '[email protected]'),
24+
(4, 'Mary Johnson', '[email protected]'),
25+
(5, 'James Brown', '[email protected]'),
26+
(6, 'Sarah Davis', '[email protected]'),
27+
(7, 'Michael Lee', '[email protected]'),
28+
(8, 'Lisa Anderson', '[email protected]'),
29+
(9, 'David Miller', '[email protected]'),
30+
(10, 'Emma Wilson', '[email protected]');
31+
32+
INSERT INTO `groups` (id, name) VALUES
33+
(1, 'Team Alpha');
34+
35+
INSERT INTO group_members (group_id, user_id) VALUES
36+
(1, 1),
37+
(1, 2),
38+
(1, 3),
39+
(1, 4),
40+
(1, 5);
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
{{!-- This is a form that will submit data using POST method to the URL specified in the top-level 'action' property coming from the SQL query--}}
2+
<form method="POST" action="{{action}}">
3+
{{!-- Create a row with centered content and spacing between items --}}
4+
<div class="row justify-content-center align-items-center g-4">
5+
{{!-- Left List Box (5 columns wide) --}}
6+
<div class="col-5">
7+
{{!-- Card with no border and subtle shadow --}}
8+
<div class="card border-0 shadow-sm">
9+
<div class="card-header bg-white border-bottom fw-semibold text-secondary py-3">
10+
Available Items
11+
</div>
12+
<div class="card-body p-3">
13+
{{!-- Multiple-select dropdown list, 300px tall --}}
14+
<select class="form-select" id="leftList" multiple style="height: 300px">
15+
{{!-- Loop through each row of data returned by the second SQL query (row-level properties are available as variables) --}}
16+
{{#each_row}}
17+
{{!-- Create an option for each item, marking it selected if the 'selected' property is true --}}
18+
<option class="py-2 px-3 rounded-1 my-1" value="{{id}}" {{#if selected}}selected{{/if}}>{{label}}</option>
19+
{{/each_row}}
20+
</select>
21+
</div>
22+
</div>
23+
</div>
24+
25+
{{!-- Middle section with transfer buttons (auto-sized column) --}}
26+
<div class="col-auto d-flex flex-column gap-2">
27+
{{!-- Right arrow button (→) to move items to selected list --}}
28+
<button type="button" class="btn btn-outline-primary rounded-circle p-0 d-flex align-items-center justify-content-center"
29+
id="moveRight"
30+
title="Move to selected"
31+
style="width: 40px; height: 40px">
32+
33+
</button>
34+
{{!-- Left arrow button (←) to remove items from selected list --}}
35+
<button type="button" class="btn btn-outline-primary rounded-circle p-0 d-flex align-items-center justify-content-center"
36+
id="moveLeft"
37+
title="Remove from selected"
38+
style="width: 40px; height: 40px">
39+
40+
</button>
41+
</div>
42+
43+
{{!-- Right List Box (5 columns wide) --}}
44+
<div class="col-5">
45+
<div class="card border-0 shadow-sm">
46+
<div class="card-header bg-white border-bottom fw-semibold text-secondary py-3">
47+
Selected Items
48+
</div>
49+
<div class="card-body p-3">
50+
{{!-- Multiple-select dropdown that will contain selected items. The name attribute makes it submit as an array --}}
51+
<select class="form-select" id="rightList" name="selected_items[]" multiple style="height: 300px"></select>
52+
</div>
53+
</div>
54+
</div>
55+
56+
{{!-- Submit Button Section (full width) --}}
57+
<div class="col-12 text-center mt-4">
58+
<input type="submit" class="btn btn-primary px-4 py-2 fw-semibold shadow-sm"
59+
id="submitBtn"
60+
disabled
61+
value="Submit Selection">
62+
</div>
63+
</div>
64+
</form>
65+
66+
{{!-- JavaScript code with CSP (Content Security Policy) nonce for security --}}
67+
<script nonce="{{@csp_nonce}}">
68+
// Get references to both list elements
69+
const rightList = document.getElementById('rightList');
70+
const leftList = document.getElementById('leftList');
71+
72+
/**
73+
* Moves selected items from one list to another while maintaining alphabetical order
74+
* @param {HTMLSelectElement} fromList - The list to take items from
75+
* @param {HTMLSelectElement} toList - The list to add items to
76+
*/
77+
function transferItems(fromList, toList) {
78+
// Deselect all items in the destination list
79+
for (const option of toList.options) option.selected = false;
80+
81+
// Combine existing options with newly selected ones
82+
const newOptions = [...toList.options, ...fromList.selectedOptions];
83+
84+
// Sort the combined options alphabetically
85+
newOptions.sort((a, b) => a.text.localeCompare(b.text));
86+
87+
// Add all options to the destination list
88+
toList.append(...newOptions);
89+
90+
// Focus the destination list and update submit button state
91+
toList.focus();
92+
updateSubmitButton();
93+
}
94+
95+
/**
96+
* Enable/disable submit button based on whether there are items in the right list
97+
*/
98+
function updateSubmitButton() {
99+
submitBtn.disabled = rightList.options.length === 0;
100+
}
101+
102+
/**
103+
* Ensure all items in right list are selected before form submission
104+
* This is necessary because unselected options aren't included in form data
105+
*/
106+
function handleSubmit(e) {
107+
for (const option of rightList.options) option.selected = true;
108+
}
109+
110+
// Set initial state of submit button
111+
updateSubmitButton();
112+
113+
// Move any pre-selected items to the right list when page loads
114+
transferItems(leftList, rightList);
115+
116+
// Set up click handlers for the transfer buttons and form submission
117+
document.getElementById('moveRight').addEventListener('click', transferItems.bind(null, leftList, rightList));
118+
document.getElementById('moveLeft').addEventListener('click', transferItems.bind(null, rightList, leftList));
119+
document.querySelector('form').addEventListener('submit', handleSubmit);
120+
</script>

0 commit comments

Comments
 (0)