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
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:
@@ -0,0 +1,64 @@
|
||||
# Forms with multiple steps
|
||||
|
||||
Multi-steps forms are forms where the user has to go through multiple pages
|
||||
to fill in all the information.
|
||||
They are a good practice to improve the user experience
|
||||
on complex forms by removing the cognitive load of filling in a long form at once.
|
||||
Additionally, they allow you to validate the input at each step,
|
||||
and create dynamic forms, where the next step depends on the user's input.
|
||||
|
||||
There are multiple ways to create forms with multiple steps in SQLPage,
|
||||
which vary in the way the state of the partially filled form
|
||||
is persisted between steps.
|
||||
|
||||
This example illustrates the main ones.
|
||||
All the examples will implement the same simple form:
|
||||
a form that asks for a person's name, email, and age.
|
||||
|
||||
## [Storing the state in hidden fields](./hidden/)
|
||||
|
||||

|
||||
|
||||
You can store the state of the partially filled form in hidden fields,
|
||||
using `'hidden' as type` in the [form component](https://sql-page.com/component.sql?component=form#component).
|
||||
|
||||
- **advantages**
|
||||
- simple to implement
|
||||
- the form state is not sent to the server when the user navigates to other pages
|
||||
- **disadvantages**
|
||||
- the entire state is re-sent to the server on each step
|
||||
- you need to reference all the previous answers in each step
|
||||
- no *backwards navigation*: the user has to fill in the steps in order. If they go back to a previous step, you cannot prefill the form with the previous answers, or save the data they have already entered.
|
||||
|
||||
## [Storing the state in the database](./database/)
|
||||
|
||||
You can store the state of the partially filled form in the database,
|
||||
either in the final table where you want to store the data,
|
||||
or in a dedicated table that will be used to store only partial data,
|
||||
allowing you to have more relaxed column constraints in the partially filled data.
|
||||
|
||||
- **advantages**
|
||||
- the website administrator can access user inputs before they submit the final form
|
||||
- the user can start filling the form on one device, and continue on another one.
|
||||
- the user can have multiple partially filled forms in flight at the same time.
|
||||
- **disadvantages**
|
||||
- the website administrator needs to manage a dedicated table for the form state
|
||||
- old partially filled forms may pile up in the database
|
||||
|
||||
## [Storing the state in cookies](./cookies/)
|
||||
|
||||
You can store each answer of the user in a cookie,
|
||||
using the
|
||||
[`cookie` component](https://sql-page.com/component.sql?component=cookie#component).
|
||||
and retrieve it on the next step using the
|
||||
[`sqlpage.cookie` function](https://sql-page.com/functions.sql?function=cookie#function).
|
||||
|
||||
- **advantages**
|
||||
- simple to implement
|
||||
- if the user leaves the form before submitting it, and returns to it later,
|
||||
the state will be persisted.
|
||||
- works even if some of the steps do not use the form component.
|
||||
- **disadvantages**
|
||||
- the entire state is re-sent to the server on each step
|
||||
- the user needs to have cookies enabled to fill in the form
|
||||
- if the user leaves the form before submitting it, the form state will keep being sent to all the pages he visits until he submits the form.
|
||||
@@ -0,0 +1,20 @@
|
||||
insert into users (
|
||||
name, email, age
|
||||
) values (
|
||||
sqlpage.cookie('name'),
|
||||
sqlpage.cookie('email'),
|
||||
:age -- This is the age that was submitted from the form in step_3.sql
|
||||
);
|
||||
|
||||
-- remove cookies
|
||||
with t(name) as (values ('name'), ('email'), ('age'))
|
||||
select 'cookie' as component, name, '/cookies/' as path, true as remove from t;
|
||||
|
||||
select
|
||||
'alert' as component,
|
||||
'Welcome, ' || name || '!' as title,
|
||||
'You are user #' || id || '. [Create a new user](step_1.sql)' as description_md
|
||||
from users where id = last_insert_rowid();
|
||||
|
||||
select 'list' as component, 'Existing users' as title, 'users' as value;
|
||||
select name as title, email as description from users;
|
||||
@@ -0,0 +1 @@
|
||||
select 'redirect' as component, 'step_1.sql' as link;
|
||||
@@ -0,0 +1,8 @@
|
||||
select
|
||||
'form' as component,
|
||||
'step_2.sql' as action;
|
||||
|
||||
select
|
||||
'name' as name,
|
||||
true as required,
|
||||
sqlpage.cookie ('name') as value;
|
||||
@@ -0,0 +1,17 @@
|
||||
select
|
||||
'cookie' as component,
|
||||
'name' as name,
|
||||
:name as value,
|
||||
'/cookies/' as path; -- Only send the cookie for pages in the /cookies/ directory
|
||||
|
||||
select
|
||||
'form' as component,
|
||||
'step_3.sql' as action;
|
||||
|
||||
select
|
||||
'email' as name,
|
||||
'email' as type,
|
||||
true as required,
|
||||
sqlpage.cookie ('email') as value,
|
||||
'you@example.com' as placeholder,
|
||||
'Hey ' || coalesce(:name, sqlpage.cookie('name')) || '! what is your email?' as description;
|
||||
@@ -0,0 +1,15 @@
|
||||
select
|
||||
'cookie' as component,
|
||||
'email' as name,
|
||||
:email as value,
|
||||
'/cookies/' as path;
|
||||
|
||||
select
|
||||
'form' as component,
|
||||
'finish.sql' as action;
|
||||
|
||||
select
|
||||
'age' as name,
|
||||
'number' as type,
|
||||
true as required,
|
||||
'How old are you, ' || sqlpage.cookie('name') || '?' as description;
|
||||
@@ -0,0 +1,16 @@
|
||||
update partially_filled_users set age = :age
|
||||
where :age is not null and id = $id;
|
||||
|
||||
insert into users (name, email, age)
|
||||
select name, email, age from partially_filled_users where id = $id;
|
||||
|
||||
delete from partially_filled_users where id = $id;
|
||||
|
||||
select
|
||||
'alert' as component,
|
||||
'Welcome, ' || name || '!' as title,
|
||||
'You are user #' || id || '. [Create a new user](index.sql)' as description_md
|
||||
from users where id = last_insert_rowid();
|
||||
|
||||
select 'list' as component, 'Existing users' as title, 'users' as value;
|
||||
select name as title, email as description from users;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- create a new empty partially_filled_users row, returning its id
|
||||
insert into partially_filled_users default values
|
||||
returning
|
||||
'redirect' as component,
|
||||
'step_1.sql?id=' || id as link;
|
||||
@@ -0,0 +1,4 @@
|
||||
select 'form' as component, 'step_2.sql?id=' || $id as action;
|
||||
|
||||
select 'name' as name, true as required, name as value
|
||||
from partially_filled_users where id = $id;
|
||||
@@ -0,0 +1,9 @@
|
||||
update partially_filled_users set name = :name
|
||||
where :name is not null and id = $id;
|
||||
|
||||
select 'form' as component, 'step_3.sql?id=' || $id as action;
|
||||
|
||||
select 'email' as name, 'email' as type, true as required, email as value,
|
||||
'you@example.com' as placeholder,
|
||||
'Hey ' || name || '! what is your email?' as description
|
||||
from partially_filled_users where id = $id;
|
||||
@@ -0,0 +1,8 @@
|
||||
update partially_filled_users set email = :email
|
||||
where :email is not null and id = $id;
|
||||
|
||||
select 'form' as component, 'finish.sql?id=' || $id as action;
|
||||
|
||||
select 'age' as name, 'number' as type, true as required, age as value,
|
||||
'How old are you, ' || name || '?' as description
|
||||
from partially_filled_users where id = $id;
|
||||
@@ -0,0 +1,8 @@
|
||||
services:
|
||||
web:
|
||||
image: lovasoa/sqlpage:main
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- .:/var/www
|
||||
- ./sqlpage:/etc/sqlpage
|
||||
@@ -0,0 +1,8 @@
|
||||
insert into users (name, email, age) values (:name, :email, :age)
|
||||
returning
|
||||
'alert' as component,
|
||||
'Welcome, ' || name || '!' as title,
|
||||
'You are user #' || id || '. [Create a new user](step_1.sql)' as description_md;
|
||||
|
||||
select 'list' as component, 'Existing users' as title, 'users' as value;
|
||||
select name as title, email as description from users;
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 486 KiB |
@@ -0,0 +1 @@
|
||||
select 'redirect' as component, 'step_1.sql' as link;
|
||||
@@ -0,0 +1,7 @@
|
||||
select
|
||||
'form' as component,
|
||||
'step_2.sql' as action;
|
||||
|
||||
select
|
||||
'name' as name,
|
||||
true as required;
|
||||
@@ -0,0 +1,13 @@
|
||||
select
|
||||
'form' as component,
|
||||
'step_3.sql' as action;
|
||||
|
||||
select
|
||||
'email' as name,
|
||||
'email' as type,
|
||||
true as required,
|
||||
'you@example.com' as placeholder,
|
||||
'Hey ' || :name || '! what is your email?' as description;
|
||||
|
||||
with previous_answers(name, value) as (values ('name', :name))
|
||||
select 'hidden' as type, name, value from previous_answers;
|
||||
@@ -0,0 +1,12 @@
|
||||
select
|
||||
'form' as component,
|
||||
'finish.sql' as action;
|
||||
|
||||
select
|
||||
'age' as name,
|
||||
'number' as type,
|
||||
true as required,
|
||||
'How old are you, ' || :name || '?' as description;
|
||||
|
||||
with previous_answers(name, value) as (values ('name', :name), ('email', :email))
|
||||
select 'hidden' as type, name, value from previous_answers;
|
||||
@@ -0,0 +1,7 @@
|
||||
select 'list' as component, 'Forms with multiple steps' as title;
|
||||
|
||||
select 'Database persistence' as title, 'database' as link;
|
||||
select 'Cookies' as title, 'cookies' as link;
|
||||
select 'Hidden fields' as title, 'hidden' as link;
|
||||
|
||||
select 'text' as component, sqlpage.read_file_as_text('README.md') as contents_md;
|
||||
@@ -0,0 +1,7 @@
|
||||
-- Simple SQLite users table
|
||||
create table users (
|
||||
id integer primary key autoincrement,
|
||||
name text not null,
|
||||
email text not null,
|
||||
age integer not null check(age > 0)
|
||||
);
|
||||
@@ -0,0 +1,7 @@
|
||||
-- this table will store partially filled user forms
|
||||
create table partially_filled_users (
|
||||
id integer primary key autoincrement,
|
||||
name text null, -- all fields are nullable, because the user may not have filled them yet
|
||||
email text null,
|
||||
age integer null check(age > 0)
|
||||
);
|
||||
@@ -0,0 +1,33 @@
|
||||
GET http://localhost:8080/hidden/step_1.sql
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body contains "name"
|
||||
body contains "step_2.sql"
|
||||
|
||||
POST http://localhost:8080/hidden/step_2.sql
|
||||
[FormParams]
|
||||
name: Hurl User
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body contains "Hey Hurl User! what is your email?"
|
||||
body contains "step_3.sql"
|
||||
|
||||
POST http://localhost:8080/hidden/step_3.sql
|
||||
[FormParams]
|
||||
name: Hurl User
|
||||
email: hurl@example.com
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body contains "How old are you, Hurl User?"
|
||||
body contains "finish.sql"
|
||||
|
||||
POST http://localhost:8080/hidden/finish.sql
|
||||
[FormParams]
|
||||
name: Hurl User
|
||||
email: hurl@example.com
|
||||
age: 42
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body contains "Welcome, Hurl User!"
|
||||
body contains "Existing users"
|
||||
body contains "hurl@example.com"
|
||||
Reference in New Issue
Block a user