Create a Form and Handle its Submission
Form handling in Brut involves three things:
- A form class that defines the fields on the form
- A route where the form's fields are submitted
- A handler class that will be used to handle the submission to that route
brut scaffold form can set up a lot of the infrastructure.
Create the Route, Form, and Handler
Decide on the name of your form/handler, which will be used to name the route. Remember, Brut doesn't care about resources, so name the form whatever the non-programmers on your team would call it. In this example, we'l call it the user preferences form.
The route would be /user-preferences so we can use that to run brut scaffold form
dx/exec brut scaffold form /user-preferencesThis will create a form class, a handler class, a test for both, and an entry in app.rb for the new route.
Define the Form's Fields
In Brut, you define form fields on the form class. They can have the same types and constraints as an HTML form. For this example, we'll define three fields: An optional account name, a default number of tasks for new projects, and a a checkbox for new projects to be public or not.
The file to edit is in app/src/front_end/forms/user_preferences_form.rb and should look like so after adding the fields:
class UserPreferencesForm < AppForm
input :account_name, type: :text, required: false
input :default_num_tasks, type: :number
input :default_public, type: :checkbox
endNote that fields other than checkboxes are required by default, thus why required: false is set for :account_name.
With these fields defined, the HTML can be generated.
Generate HTML
We'll assume a page called preferences page where the form will live. This page would be in app/src/front_end/pages/preferences_page.rb.
The form's HTML needs to:
- Submit to the form's route (
/user-preferences) - Use the names and types from our form class
- Render any default values we've provided
To do this, we'll use the form itself as a data source. The best practice is for a page that is to render a form accept it as an optional parameter to its initializer
# app/src/front_end/pages/preferences_page.rb
class PreferencesPage < AppPage
def initialize(form: nil)
@form = form || UserPreferencesForm.new
end
def page_template
# ...
end
endWe'll then use @form, along with Brut's API to generate the HTML. While not required, surrounding our form with the <brut-form> custom element is a good practice, which we'll see in another HOWTO.
To create the <form> tag, we'll use Brut::FrontEnd::Components::FormTag, which is available in Phlex views via FormTag
# app/src/front_end/pages/preferences_page.rb
class PreferencesPage < AppPage
def initialize(form: nil)
@form = form || UserPreferencesForm.new
end
def page_template
brut_form do
FormTag(for: @form) do
# ...
end
end
end
endThis ensures the form will be submitted to the route we configured. Now, we'll generate the HTML. This example will show only minimal HTML to get the form working, but will include the correct markup for validation errors (though we won't use them in this HOWTO).
To generate HTML for the form elements, we'll use included Brut components that live in Brut::FrontEnd::Components::Inputs. These can be accessed directly from Inputs::, e.g. Inputs::InputTag. We'll also use the Brut::FrontEnd::Components::ConstraintViolations component which will include markup useful for validation errors.
# app/src/front_end/pages/preferences_page.rb
class PreferencesPage < AppPage
def initialize(form: nil)
@form = form || UserPreferencesForm.new
end
def page_template
brut_form do
FormTag(for: @form) do
label do
Inputs::InputTag(form: @form, input_name: :account_name)
span { "Account Name" }
ConstraintViolations(form: @form, input_name: :account_name)
end
label do
Inputs::InputTag(form: @form, input_name: :default_num_tasks)
span { "Default # of Tasks" }
ConstraintViolations(form: @form, input_name: :default_num_tasks)
end
label do
Inputs::InputTag(form: @form, input_name: :default_public)
span { "Public by Default?" }
ConstraintViolations(form: @form, input_name: :default_public)
end
button { "Save Preferences" }
end
end
end
endHandle the Submission
When the form is submitted, it will be submitted to /user-preferences. The values will be placed into an instance of UserPreferencesForm, and that instance can be made available to UserPreferencesHandler for processing.
NOTE
Because UserPreferencesForm is a class with defined attributes, any values submitted with the form that aren't explicitly declared in UserPreferencesForm will be discarded. No need for "strong" attributes or an allowlist of inputs
brut scaffold created app/src/front_end/handlers/user_preferences_handler.rb where we can put our logic. For this HOWTO, we'll just save the data into a database table called USER_PREFERENCES
Handlers are initialized with whatever values they need to work. Handlers need the form instance, so in our case, we'll just accept that in the initializer.
class UserPreferencesHandler < AppHandler
class initialize(form:)
@form = form
end
def handle
# ..
end
endNext, we'll implement handle which is called by Brut to actual do the handling. It's return value controls what happens next. In this case, we'll redirect back to the page with the form, PreferencesPage.
class UserPreferencesHandler < AppHandler
class initialize(form:)
@form = form
end
def handle
DB::UserPreferences.create(
account_name: @form.account_name,
default_num_tasks: @form.default_num_tasks,
default_public: @form.default_public
)
redirect_to(PreferencesPage)
end
endShow Default Values in HTML
Not all forms are blank by default. In our running example, it would make sense for the PreferencesPage to show the values from the DB::UserPreferences entry instead of blank values.
All form classes have an initializer that accepts the key params:, which is a hash of values to use as defaults. We can do this in the initializer of PreferencesPage.
# app/src/front_end/pages/preferences_page.rb
class PreferencesPage < AppPage
def initialize(form: nil)
@form = if form
form
else
existing_preferences = DB::UserPreferences.first
UserPreferencesForm.new(params: {
account_name: existing_preferences&.account_name,
default_num_tasks: existing_preferences&.default_num_tasks,
default_public existing_preferences&.default_public:
})
end
end
def page_template
# ...
end
endThe page_template method doesn't need to change - the use of Inputs::InputTag will see that the form has a value for a given input and use that.