From 8ef787b24c74487022a1f6d0e70a67c00c91d088 Mon Sep 17 00:00:00 2001 From: Christophe Bliard Date: Thu, 12 Sep 2024 11:33:59 +0200 Subject: [PATCH] Add lookbook Form pattern page --- lookbook/docs/patterns/14-forms.md.erb | 226 +++++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 lookbook/docs/patterns/14-forms.md.erb diff --git a/lookbook/docs/patterns/14-forms.md.erb b/lookbook/docs/patterns/14-forms.md.erb new file mode 100644 index 000000000000..0f9e09c2ff8a --- /dev/null +++ b/lookbook/docs/patterns/14-forms.md.erb @@ -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)}
" \ + "#{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)}
" \ + "#{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.