Vix UI forms help you build server-rendered HTML forms from C++.
They are useful when an application needs login pages, dashboards, admin panels, settings pages, product forms, profile forms, or any page where the server renders the form and sends normal HTML to the browser.
#include <vix/ui/forms/Form.hpp>
#include <vix/ui/forms/Field.hpp>
#include <vix/ui/forms/FieldOption.hpp>
#include <vix/ui/forms/FormData.hpp>
#include <vix/ui/forms/CsrfToken.hpp>
#include <vix/ui/forms/ValidationError.hpp>A form can be written manually as HTML.
<form method="post" action="/login">
<input name="email" type="email" />
<input name="password" type="password" />
<button type="submit">Submit</button>
</form>That works, but it becomes repetitive when the application needs labels, placeholders, selected options, old input values, validation errors, CSRF fields, file uploads, and reusable field rendering.
Vix UI forms let you describe the form from C++ and render the final HTML.
vix::ui::Form form = vix::ui::Form::post("/login");
form.add_field(
vix::ui::Field::email("email")
.set_label("Email")
.set_required(true));
form.add_field(
vix::ui::Field::password("password")
.set_label("Password")
.set_required(true));
std::string html = form.render();#include <iostream>
#include <vix/ui/forms/Form.hpp>
#include <vix/ui/forms/Field.hpp>
int main()
{
vix::ui::Form form = vix::ui::Form::post("/login");
form.set_attr("class", "login-form");
form.add_field(
vix::ui::Field::email("email")
.set_label("Email")
.set_placeholder("you@example.com")
.set_required(true));
form.add_field(
vix::ui::Field::password("password")
.set_label("Password")
.set_placeholder("Your password")
.set_required(true));
form.add_field(
vix::ui::Field::checkbox("remember")
.set_label("Remember me")
.set_value("1"));
std::cout << form.render() << "\n";
return 0;
}Run it:
vix run main.cppForm represents the whole HTML form.
Use it to set the method, action, attributes, CSRF field, fields, old input values, and errors.
vix::ui::Form form = vix::ui::Form::post("/profile/update");Common factories:
vix::ui::Form form = vix::ui::Form::post("/login");
vix::ui::Form form = vix::ui::Form::get("/search");Add HTML attributes to the form:
form.set_attr("class", "profile-form");
form.set_attr("id", "profile-form");Render the full form:
std::string html = form.render();Render only the opening tag:
std::string open = form.render_open();Render only the fields:
std::string fields = form.render_fields();Render only the errors:
std::string errors = form.render_errors();Field represents one input control.
Vix UI supports the common fields used by server-rendered applications:
- text
- password
- number
- hidden
- checkbox
- radio
- textarea
- select
- file
vix::ui::Field title =
vix::ui::Field::text("title")
.set_label("Product title")
.set_placeholder("Example: iPhone 13 Pro")
.set_required(true);vix::ui::Field email =
vix::ui::Field::email("email")
.set_label("Email")
.set_placeholder("you@example.com")
.set_required(true);vix::ui::Field password =
vix::ui::Field::password("password")
.set_label("Password")
.set_placeholder("Your password")
.set_required(true);vix::ui::Field price =
vix::ui::Field::number("price")
.set_label("Price")
.set_placeholder("Example: 350")
.set_attr("min", "0")
.set_attr("step", "0.01")
.set_required(true);vix::ui::Field description =
vix::ui::Field::textarea("description")
.set_label("Description")
.set_placeholder("Write a short description")
.set_attr("rows", "5");Hidden field
form.add_hidden("_token", "csrf-demo-token");Hidden fields are useful for tokens, IDs, or server-generated values that must be submitted with the form.
vix::ui::Field newsletter =
vix::ui::Field::checkbox("newsletter")
.set_label("Receive product updates")
.set_value("1");Set the checkbox as checked:
newsletter.set_checked(true);You can also use a boolean attribute:
newsletter.set_bool_attr("checked", true);A radio field can receive multiple options.
vix::ui::Field visibility =
vix::ui::Field::radio("visibility")
.set_label("Visibility")
.set_value("public")
.set_required(true);
visibility.add_option("public", "Public");
visibility.add_option("private", "Private");
visibility.add_option("draft", "Draft");When the field value matches an option value, that option is rendered as selected.
vix::ui::Field country =
vix::ui::Field::select("country")
.set_label("Country")
.set_required(true);
country.add_option("", "Choose country");
country.add_option("ug", "Uganda");
country.add_option("cd", "DRC");
country.add_option("rw", "Rwanda");
country.add_option("ke", "Kenya");Set the selected value:
country.set_value("ug");FieldOption represents one option in a select or radio field.
vix::ui::FieldOption option =
vix::ui::FieldOption::make("ug", "Uganda");
option.set_selected(true);
option.set_disabled(false);You can also attach attributes:
option.set_attr("data-code", "256");Use FieldOption directly when an option needs more control than a simple value and label.
vix::ui::Field images =
vix::ui::Field::file("images")
.set_label("Product images")
.set_accept("image/png,image/jpeg,image/webp")
.set_multiple(true);When a file field is present, the form renders the correct encoding type:
enctype="multipart/form-data"This prevents a common upload mistake where the browser submits the form without file data.
CsrfToken is a rendering helper.
It can render a hidden CSRF input:
vix::ui::CsrfToken token =
vix::ui::CsrfToken::named("_csrf", "secure-token-value");
std::string html = token.render();Output:
<input id="_csrf" name="_csrf" type="hidden" value="secure-token-value" />Attach it to a form:
form.set_csrf(
vix::ui::CsrfToken::named("_csrf", "secure-token-value"));Or use the short form:
form.set_csrf("secure-token-value");Render only the CSRF input:
std::string csrf = form.render_csrf();CsrfToken does not generate, sign, rotate, store, or validate tokens. The application security layer remains responsible for that.
Some applications also expose the token to JavaScript through a meta tag.
vix::ui::CsrfToken token("secure-token-value");
std::string meta = token.render_meta();Output:
<meta
content="secure-token-value"
data-header="X-CSRF-Token"
name="csrf-token"
/>Customize the meta name:
std::string meta = token.render_meta("vix-csrf");Forms can render validation errors.
form.add_error("email", "Email is required.");
form.add_error("password", "Password is required.");A field error is attached to a field name.
form.add_error("email", "This email address is already used.");Render only errors:
std::string errors = form.render_errors();Render the full form with errors:
std::string html = form.render();FormData stores submitted values or old input values.
It is useful after validation fails.
Instead of returning an empty form, the server can bind the previous values back to the form.
vix::ui::FormData old_input;
old_input.set("name", "Gaspard");
old_input.set("email", "gaspard@example.com");
old_input.set("country", "ug");
old_input.set("newsletter", "1");Bind the data to the form:
form.bind(old_input);After binding, matching fields receive their old values.
Some fields can have multiple values.
vix::ui::FormData data;
data.add("roles", "admin");
data.add("roles", "editor");Read the first value:
std::string role = data.get_or("roles");Read all values:
std::vector<std::string> roles = data.get_all_or_empty("roles");Check whether a value exists:
bool is_admin = data.contains("roles", "admin");#include <iostream>
#include <vix/ui/forms/Form.hpp>
#include <vix/ui/forms/Field.hpp>
#include <vix/ui/forms/FormData.hpp>
#include <vix/ui/forms/CsrfToken.hpp>
int main()
{
vix::ui::FormData old_input;
old_input.set("name", "Gaspard");
old_input.set("email", "gaspard@example.com");
old_input.set("country", "ug");
old_input.set("role", "admin");
old_input.set("newsletter", "1");
old_input.set("bio", "Building server-rendered UI with Vix.cpp.");
vix::ui::Form form = vix::ui::Form::post("/profile/update");
form.set_attr("class", "profile-form")
.set_csrf(vix::ui::CsrfToken::named("_csrf", "csrf-demo-token"));
form.add_field(
vix::ui::Field::text("name")
.set_label("Name")
.set_required(true));
form.add_field(
vix::ui::Field::email("email")
.set_label("Email")
.set_required(true));
form.add_field(
vix::ui::Field::password("password")
.set_label("Password")
.set_placeholder("Leave empty to keep current password"));
vix::ui::Field country =
vix::ui::Field::select("country")
.set_label("Country")
.set_required(true);
country.add_option("ug", "Uganda");
country.add_option("cd", "DRC");
country.add_option("rw", "Rwanda");
country.add_option("ke", "Kenya");
form.add_field(country);
vix::ui::Field role =
vix::ui::Field::radio("role")
.set_label("Role")
.set_required(true);
role.add_option("admin", "Admin");
role.add_option("editor", "Editor");
role.add_option("viewer", "Viewer");
form.add_field(role);
form.add_field(
vix::ui::Field::checkbox("newsletter")
.set_label("Receive product updates")
.set_value("1"));
form.add_field(
vix::ui::Field::textarea("bio")
.set_label("Bio")
.set_attr("rows", "4"));
form.bind(old_input);
form.add_error("email", "This email address is already used.");
std::cout << form.render() << "\n";
return 0;
}Run it:
vix run main.cpp#include <iostream>
#include <vix/ui/forms/Form.hpp>
#include <vix/ui/forms/Field.hpp>
int main()
{
vix::ui::Form form = vix::ui::Form::post("/products/create");
form.set_attr("class", "product-form")
.set_csrf("csrf-demo-token");
form.add_field(
vix::ui::Field::text("title")
.set_label("Product title")
.set_placeholder("Example: iPhone 13 Pro")
.set_required(true));
form.add_field(
vix::ui::Field::number("price")
.set_label("Price")
.set_placeholder("Example: 350")
.set_required(true)
.set_attr("min", "0")
.set_attr("step", "0.01"));
vix::ui::Field condition =
vix::ui::Field::select("condition")
.set_label("Condition")
.set_required(true);
condition.add_option("", "Choose condition");
condition.add_option("new", "New");
condition.add_option("used", "Used");
condition.add_option("refurbished", "Refurbished");
form.add_field(condition);
vix::ui::Field visibility =
vix::ui::Field::radio("visibility")
.set_label("Visibility")
.set_value("public")
.set_required(true);
visibility.add_option("public", "Public");
visibility.add_option("private", "Private");
visibility.add_option("draft", "Draft");
form.add_field(visibility);
form.add_field(
vix::ui::Field::checkbox("featured")
.set_label("Mark as featured")
.set_value("1")
.set_checked(true));
form.add_field(
vix::ui::Field::file("images")
.set_label("Product images")
.set_accept("image/png,image/jpeg,image/webp")
.set_multiple(true));
form.add_field(
vix::ui::Field::textarea("description")
.set_label("Description")
.set_placeholder("Write a short product description")
.set_attr("rows", "5"));
std::cout << form.render() << "\n";
return 0;
}#include <vix/core.hpp>
#include <vix/ui.hpp>
int main()
{
vix::App app;
app.get("/", [](vix::Request &req, vix::Response &res) {
(void)req;
vix::ui::Form form = vix::ui::Form::post("/login");
form.set_attr("class", "login-form")
.set_csrf("csrf-demo-token");
form.add_field(
vix::ui::Field::email("email")
.set_label("Email")
.set_placeholder("you@example.com")
.set_required(true));
form.add_field(
vix::ui::Field::password("password")
.set_label("Password")
.set_placeholder("Your password")
.set_required(true));
const std::string html =
"<!doctype html>"
"<html lang=\"en\">"
"<head>"
"<meta charset=\"utf-8\">"
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"
"<title>Login</title>"
"</head>"
"<body>"
"<main>"
"<h1>Login</h1>" +
form.render() +
"</main>"
"</body>"
"</html>";
res.ui(vix::ui::HtmlResponse::html(html));
});
app.run(8080);
return 0;
}Run it:
vix run main.cppCreate the form in C++:
vix::ui::Form form = vix::ui::Form::post("/login");
form.add_field(
vix::ui::Field::email("email")
.set_label("Email")
.set_required(true));
form.add_field(
vix::ui::Field::password("password")
.set_label("Password")
.set_required(true));
auto view =
vix::ui::View("login.html")
.set_title("Login")
.set("form", form.render());
res.ui(view);Use the rendered form in the template:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>{{ page_title }}</title>
</head>
<body>
<main>
<h1>{{ page_title }}</h1>
{{ form | safe }}
</main>
</body>
</html>Use safe because form.render() already returns HTML.
Most form objects support custom HTML attributes.
form.set_attr("class", "login-form");field.set_attr("autocomplete", "email");
field.set_attr("class", "form-control");Boolean attributes can be added or removed.
field.set_bool_attr("disabled", true);
field.set_bool_attr("disabled", false);Attribute values are escaped before rendering.
A common server-rendered flow looks like this:
GET /profile
render empty form or existing profile values
POST /profile
read request body
validate input
if invalid:
bind old input
attach validation errors
render form again
if valid:
save data
redirect or render success responseVix UI forms handle the rendering side of this flow.
The application remains responsible for reading the request body and validating the data.
With Vix UI, adding a file field lets the form render the upload encoding automatically.
form.add_field(
vix::ui::Field::file("images")
.set_multiple(true));Wrong:
{{ form }}This may escape the form HTML.
Correct:
{{ form | safe }}CsrfToken renders tokens.
It does not validate them.
Validation belongs in the application security layer.
Bind old input before rendering the form again.
form.bind(old_input);For a selected option, set the field value.
country.set_value("ug");Or bind old input:
form.bind(old_input);Use Form for the whole form.
Use Field for each input.
Use FieldOption when select or radio options need more control.
Use FormData to keep old input after validation fails.
Use CsrfToken to render CSRF fields and meta tags.
Form -> the full HTML form
Field -> one input, select, textarea, checkbox, radio, or file field
FieldOption -> one select or radio option
FormData -> old input or submitted values
CsrfToken -> hidden input or meta tag renderingContinue with live UI.