-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
226 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |