Skip to content

Latest commit

 

History

History
862 lines (616 loc) · 17 KB

File metadata and controls

862 lines (616 loc) · 17 KB

Forms

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>

What forms solve

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();

Basic form

#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.cpp

Form

Form 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

Field represents one input control.

Vix UI supports the common fields used by server-rendered applications:

  • text
  • email
  • password
  • number
  • hidden
  • checkbox
  • radio
  • textarea
  • select
  • file

Text field

vix::ui::Field title =
    vix::ui::Field::text("title")
        .set_label("Product title")
        .set_placeholder("Example: iPhone 13 Pro")
        .set_required(true);

Email field

vix::ui::Field email =
    vix::ui::Field::email("email")
        .set_label("Email")
        .set_placeholder("you@example.com")
        .set_required(true);

Password field

vix::ui::Field password =
    vix::ui::Field::password("password")
        .set_label("Password")
        .set_placeholder("Your password")
        .set_required(true);

Number field

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);

Textarea

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.

Checkbox

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);

Radio group

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.

Select field

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

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.

File field

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.

CSRF token

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.

CSRF meta tag

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");

Validation errors

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();

Old input with FormData

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.

Multiple 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");

Profile form with old input

#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

Product form example

#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;
}

Render a form inside a Vix route

#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.cpp

Render a form inside a template

Create 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.

Attributes

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.

Practical validation flow

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 response

Vix UI forms handle the rendering side of this flow.

The application remains responsible for reading the request body and validating the data.

Common mistakes

Forgetting multipart/form-data for uploads

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));

Rendering trusted form HTML without safe

Wrong:

{{ form }}

This may escape the form HTML.

Correct:

{{ form | safe }}

Expecting CSRF helper to validate tokens

CsrfToken renders tokens.

It does not validate them.

Validation belongs in the application security layer.

Losing user input after validation fails

Bind old input before rendering the form again.

form.bind(old_input);

Setting select options but not the current value

For a selected option, set the field value.

country.set_value("ug");

Or bind old input:

form.bind(old_input);

What to remember

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 rendering

Next step

Continue with live UI.

Open the live UI guide