diff --git a/lookbook/docs/patterns/02-forms.md.erb b/lookbook/docs/patterns/02-forms.md.erb index 629e7afe0fc3..81a134ef375c 100644 --- a/lookbook/docs/patterns/02-forms.md.erb +++ b/lookbook/docs/patterns/02-forms.md.erb @@ -1,5 +1,9 @@ A form is a series of components that require user input that will then be submitted. Forms are notably common in settings pages but are also present elsewhere like in individual modals (edit project attribute section, edit meeting details) and the filter panel. +## Overview + +<%= embed Patterns::FormsPreview, :custom_width_fields_form %> + ## Form elements A form may be composed of these elements: @@ -48,3 +52,242 @@ By default, form elements do not have proper vertical spacing in Primer. We reco Until this is fixed at a component level, please do not manually apply form padding. An alternative approach is to wrap individual form elements using `Form group`. Please only use this sparingly and only when it is absolutely necessarily. + +## Technical notes + +### 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. + +The above example which was needing a dedicated `TextFieldAndCheckboxForm` form +class can be rewritten like this: + +```erb +<%%= +primer_form_with(url: "/foo") do |f| + render_inline_form(f) 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 +%> +``` + +#### `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. + +```ruby +class TextFieldWithWarningForm < ApplicationForm + attr_reader :warning + + def initialize(warning: nil) + super() + @warning = warning + end + + form do |my_form| + my_form.text_field( + name: :full_name, + label: "Full name", + 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 +``` + +### 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 used instead of `form` to get a form +instance with knowledge about how 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 aren't many 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. diff --git a/lookbook/docs/patterns/14-forms.md.erb b/lookbook/docs/patterns/14-forms.md.erb deleted file mode 100644 index 1c1598b39cba..000000000000 --- a/lookbook/docs/patterns/14-forms.md.erb +++ /dev/null @@ -1,243 +0,0 @@ -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. - -The above example which was needing a dedicated `TextFieldAndCheckboxForm` form -class can be rewritten like this: - -```erb -<%%= -primer_form_with(url: "/foo") do |f| - render_inline_form(f) 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 -%> -``` - -### `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. - -```ruby -class TextFieldWithWarningForm < ApplicationForm - attr_reader :warning - - def initialize(warning: nil) - super() - @warning = warning - end - - form do |my_form| - my_form.text_field( - name: :full_name, - label: "Full name", - 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 -``` - -## 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 used instead of `form` to get a form -instance with knowledge about how 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 aren't many 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. diff --git a/lookbook/previews/patterns/forms_preview.rb b/lookbook/previews/patterns/forms_preview.rb index 8529546f40c7..0929b50f4e61 100644 --- a/lookbook/previews/patterns/forms_preview.rb +++ b/lookbook/previews/patterns/forms_preview.rb @@ -4,8 +4,10 @@ module Patterns # @hidden class FormsPreview < ViewComponent::Preview # @display min_height 500px - def default - render_with_template - end + def default; end + + # @display min_height 300px + # @label Overview + def custom_width_fields_form; end end end diff --git a/lookbook/previews/patterns/forms_preview/custom_width_fields_form.html.erb b/lookbook/previews/patterns/forms_preview/custom_width_fields_form.html.erb new file mode 100644 index 000000000000..5a38e4e175bb --- /dev/null +++ b/lookbook/previews/patterns/forms_preview/custom_width_fields_form.html.erb @@ -0,0 +1,37 @@ +<% + custom_width_form = Class.new(ApplicationForm) do + form do |f| + f.text_field( + name: :ultimate_answer, + label: "Ultimate answer", + required: true, + caption: "The answer to life, the universe, and everything", + input_width: :medium + ) + + f.select_list( + name: "cities", + label: "Cool cities", + caption: "Select your favorite!", + include_blank: true, + input_width: :small + ) do |city_list| + city_list.option(label: "Lopez Island", value: "lopez_island") + city_list.option(label: "Bellevue", value: "bellevue") + city_list.option(label: "Seattle", value: "seattle") + end + + f.text_field( + name: :lots_of_text, + label: "Lots of text", + required: true, + caption: "What else do you need?", + input_width: :small + ) + end + end +%> + +<%= primer_form_with(url: "/foo") do |f| %> + <%= render(custom_width_form.new(f)) %> +<% end %>