Skip to content

Commit

Permalink
Add lookbook Form pattern page
Browse files Browse the repository at this point in the history
  • Loading branch information
cbliard committed Sep 12, 2024
1 parent 61d2daa commit 8ef787b
Showing 1 changed file with 226 additions and 0 deletions.
226 changes: 226 additions & 0 deletions lookbook/docs/patterns/14-forms.md.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
Forms are used heavily in administration pages and throughout the application.
They rely on Primer forms with some OpenProject specific components and helpers.

## Overview

<%= embed Primer::FormsPreview, :custom_width_fields_form, log: true %>

## Usage

To create forms, you need 2 basic things:

- A form instance to render fields
- A `primer_form_with` call to get a form builder and render the form instance

```ruby
class TextFieldAndCheckboxForm < ApplicationForm
form do |my_form|
my_form.text_field(
name: :ultimate_answer,
label: "Ultimate answer",
required: true,
caption: "The answer to life, the universe, and everything"
)

my_form.check_box(
name: :enable_ipd,
label: "Enable the Infinite Improbability Drive",
caption: "Cross interstellar distances in a mere nothingth of a second."
)
end
end
```

```erb
<%%= primer_form_with(url: "/foo") do |f| %>
<%%= render(TextFieldAndCheckboxForm.new(f)) %>
<%% end %>
```

Multiple form instances can be rendered within the same `primer_form_with` call,
allowing to put some content in between:

```erb
<%%= primer_form_with(url: "/foo") do |f| %>
<%%= render(TextFieldAndCheckboxForm.new(f)) %>
<%%= render(MessageComponent.new(icon: :info, message: "This will be fine!")) %>
<%%= render(SubmitButtonForm.new(f)) %>
<%% end %>
```

This is the regular way of using Primer forms.

## OpenProject helpers

OpenProject provides some helpers to make building and rendering forms easier.

### `render_inline_form` to avoid creating form classes

This helper allows to render an anymous form instance, avoiding the need to
create a dedicated form class. This can be useful for simple forms or when you
don't want to pollute the form class namespace.

```ruby
class TextFieldWithWarningForm < ApplicationForm
def initialize(warning: nil, **args)
super(**args)
@warning = warning
end

form do |my_form|
my_form.text_field(
name: :answer,
label: "answer",
required: true
)

if @warning
my_form.html_content do
tag.div(class: "flash flash-warn") { @warning }
end
end

my_form.submit(name: :submit, label: "Save")
end
end
```

### `FormObject#html_content` to mix form fields and html content

This helper allows to render non-form content in a form. For instance it can be
used to render a description box inside a form, an image, or whatever makes
sense for the form being built.

```erb
<%%= primer_form_with(url: "/foo") do |f| %>
<%%= render(TextFieldAndCheckboxForm.new(f)) %>
<%%= f.html_content do %>
<%%= render(MessageComponent.new(icon: :info, message: "This will be fine!")) %>
<%% end %>
<%% end %>
```

## Forms for administration pages

Administration pages forms are used to change the values of `Settings`. The name
and labels being used are standardized making them very repetitive.

Here is how the form of the General tab of the system administration page could
look like:

```ruby
class Admin::Settings::GeneralSettingsForm < ApplicationForm
attr_reader :guessed_host

def initialize(guessed_host:)
super()
@guessed_host = guessed_host
end

form do |general_form|
general_form.text_field(
name: :app_title,
label: I18n.t("setting_app_title"),
value: Setting[:app_title],
disabled: !Setting.app_title_writable?
)
general_form.text_field(
name: :per_page_options,
label: I18n.t("setting_per_page_options"),
value: Setting[:per_page_options],
caption: "#{I18n.t(:text_comma_separated)}<br/>" \
"#{I18n.t(:text_notice_too_many_values_are_inperformant)}".html_safe)
disabled: !Setting.per_page_options_writable?
)
general_form.text_field(
name: :activity_days_default,
label: I18n.t("setting_activity_days_default"),
value: Setting[:activity_days_default],
type: :number,
disabled: !Setting.activity_days_default_writable?
)
general_form.text_field(
name: :host_name,
label: I18n.t("setting_host_name"),
value: Setting[:host_name],
caption: "#{I18n.t(:label_example)}: #{guessed_host}"),
disabled: !Setting.host_name_writable?
)
#
# and so on...
#
general_form.submit(
name: 'submit',
label: I18n.t('button_save'),
scheme: :primary
)
end
end
```

There is a lot of repetition in the form above: the field can be disabled for
read-only settings (which happens for settings set through environment variables
or configuration files), the field name has to be translated and the value must
be read from `Settings`. Entering all this information manually is tedious and
error prone.

In this case, `settings_form` can be usedinstead of `form` to get a form
instance with knowledge about to render fields for settings.

The above example then becomes:

```ruby
class Admin::Settings::GeneralSettingsForm < ApplicationForm
attr_reader :guessed_host

def initialize(guessed_host:)
super()
@guessed_host = guessed_host
end

settings_form do |general_form|
general_form.text_field(name: :app_title)
general_form.text_field(name: :per_page_options,
caption: "#{I18n.t(:text_comma_separated)}<br/>" \
"#{I18n.t(:text_notice_too_many_values_are_inperformant)}".html_safe)
general_form.text_field(name: :activity_days_default,
type: :number)
general_form.text_field(name: :host_name,
caption: "#{I18n.t(:label_example)}: #{guessed_host}")
#
# and so on...
#
general_form.submit
end
end
```

It is easier to write and read.

Under the hood, the form object is decorated with `SettingsFormDecorator`.
That's where all the helper methods are defined. There are not much for now, but
this is intended to grow to support more advanced form features for
administration pages.

So far, the following helpers are available:

* `text_field(name:, **options)`: renders a text field for the setting called
`name`, automatically setting the label, value, and disabled state from the
setting's attributes.

* `check_box(name:, **options)`: renders a checkbox for the setting called
`name`, automatically setting the label, checked state, and disabled state
from the setting's attributes.

* `radio_button_group(name:, values:, button_options: {}, **options)`: renders
a radio button group for the setting called `name` and radio button for each
element of `values`, automatically setting the label, checked state, html
caption, and disabled state from the setting's attributes.

* `submit`: renders a submit button with the label "Save" and the primary
scheme.

* `form`: the form builder instance if you need to render some form elements
normally handled by the settings form decorator in another way than intended.
Any call to a method that is not defined on the settings form decorator will
be forwarded to this form builder instance so its usage is transparent.

0 comments on commit 8ef787b

Please sign in to comment.