diff --git a/.changeset/lovely-boats-hang.md b/.changeset/lovely-boats-hang.md new file mode 100644 index 0000000000..3e47be7966 --- /dev/null +++ b/.changeset/lovely-boats-hang.md @@ -0,0 +1,5 @@ +--- +'@primer/view-components': patch +--- + +Add Primer::Forms::ToggleSwitchForm diff --git a/app/forms/example_toggle_switch_form.rb b/app/forms/example_toggle_switch_form.rb new file mode 100644 index 0000000000..0636457d71 --- /dev/null +++ b/app/forms/example_toggle_switch_form.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# :nodoc: +class ExampleToggleSwitchForm < Primer::Forms::ToggleSwitchForm + def initialize(**system_arguments) + super(name: :example_field, label: "Example", **system_arguments) + end +end diff --git a/app/forms/example_toggle_switch_form/example_field_caption.html.erb b/app/forms/example_toggle_switch_form/example_field_caption.html.erb new file mode 100644 index 0000000000..3dd8a8a9fa --- /dev/null +++ b/app/forms/example_toggle_switch_form/example_field_caption.html.erb @@ -0,0 +1 @@ +My favorite caption. diff --git a/lib/primer/forms/dsl/toggle_switch_input.rb b/lib/primer/forms/dsl/toggle_switch_input.rb new file mode 100644 index 0000000000..a574bbfd3e --- /dev/null +++ b/lib/primer/forms/dsl/toggle_switch_input.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Primer + module Forms + module Dsl + # :nodoc: + class ToggleSwitchInput < Input + attr_reader :name, :label, :src, :csrf + + def initialize( + name:, + label:, + src:, + csrf: nil, + **system_arguments + ) + @name = name + @label = label + @src = src + @csrf = csrf + + super(**system_arguments) + end + + def to_component + ToggleSwitch.new(input: self) + end + + def type + :toggle_switch + end + end + end + end +end diff --git a/lib/primer/forms/toggle_switch.html.erb b/lib/primer/forms/toggle_switch.html.erb new file mode 100644 index 0000000000..1b49692a94 --- /dev/null +++ b/lib/primer/forms/toggle_switch.html.erb @@ -0,0 +1,17 @@ +<%= content_tag(:div, **@form_group_arguments) do %> + + <%= builder.label(@input.name, **@input.label_arguments) do %> + <%= @input.label %> + <% end %> + <%= render(Caption.new(input: @input)) %> + + <% + csrf = @input.csrf || @view_context.form_authenticity_token( + form_options: { + method: :post, + action: @input.src + } + ) + %> + <%= render(Primer::Alpha::ToggleSwitch.new(src: @input.src, csrf: csrf)) %> +<% end %> diff --git a/lib/primer/forms/toggle_switch.rb b/lib/primer/forms/toggle_switch.rb new file mode 100644 index 0000000000..6ff843dba1 --- /dev/null +++ b/lib/primer/forms/toggle_switch.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Primer + module Forms + # :nodoc: + class ToggleSwitch < BaseComponent + delegate :builder, :form, to: :@input + + def initialize(input:) + @input = input + @input.add_label_classes("FormControl-label") + + @form_group_arguments = { class: "d-flex" } + + @form_group_arguments[:hidden] = "hidden" if @input.hidden? + end + end + end +end diff --git a/lib/primer/forms/toggle_switch_form.rb b/lib/primer/forms/toggle_switch_form.rb new file mode 100644 index 0000000000..ab703a94c2 --- /dev/null +++ b/lib/primer/forms/toggle_switch_form.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +module Primer + module Forms + # Toggle switches are designed to submit an on/off value to the server immediately + # upon click. For that reason they are not designed to be used in "regular" forms + # that have other fields, etc. Instead they should be used independently via this + # class. + # + # ToggleSwitchForm can be used directly or via inheritance. + # + # Via inheritance: + # + # # app/forms/my_toggle_form.rb + # class MyToggleForm < Primer::Forms::ToggleSwitchForm + # def initialize(**system_arguments) + # super(name: :foo, label: "Foo", src: "/foo", **system_arguments) + # end + # end + # + # # app/views/some_view.html.erb + # <%= render(MyToggleForm.new) %> + # + # Directly: + # + # # app/views/some_view.html.erb + # <%= render( + # Primer::Forms::ToggleSwitchForm.new( + # name: :foo, label: "Foo", src: "/foo" + # ) + # ) %> + # + class ToggleSwitchForm < Primer::Forms::Base + # Define the form on subclasses so render(Subclass.new) works as expected. + def self.inherited(base) + base.form do |toggle_switch_form| + input = Dsl::ToggleSwitchInput.new( + builder: toggle_switch_form.builder, form: self, **@system_arguments + ) + + toggle_switch_form.send(:add_input, input) + end + end + + # Define the form on self so render(ToggleSwitchForm.new) works as expected. + inherited(self) + + # Override to avoid accepting a builder argument. We create our own builder + # on render. See the implementation of render_in below. + def self.new(**options) + allocate.tap { |obj| obj.send(:initialize, **options) } + end + + def initialize(**system_arguments) + @system_arguments = system_arguments + end + + # Unlike other instances of Base, ToggleSwitchForm defines its own form and + # is not given a Rails form builder on instantiation. We do this mostly for + # ergonomic reasons; it's much less verbose than if you were required to + # call form_with/form_for, etc. That said, the rest of the forms framework + # assumes the presence of a builder so we create our own here. A builder + # cannot be constructed without a corresponding view context, which is why + # we have to override render_in and can't create it in the initializer. + def render_in(view_context, &block) + @builder = Primer::Forms::Builder.new( + nil, nil, view_context, {} + ) + + super + end + end + end +end diff --git a/previews/primer/forms/forms_preview.rb b/previews/primer/forms/forms_preview.rb index 004c0cf02d..c5f4e1822e 100644 --- a/previews/primer/forms/forms_preview.rb +++ b/previews/primer/forms/forms_preview.rb @@ -39,6 +39,8 @@ def multi_input_form; end def name_with_question_mark_form; end def immediate_validation_form; end + + def example_toggle_switch_form; end end end end diff --git a/previews/primer/forms/forms_preview/example_toggle_switch_form.html.erb b/previews/primer/forms/forms_preview/example_toggle_switch_form.html.erb new file mode 100644 index 0000000000..f9a1aab130 --- /dev/null +++ b/previews/primer/forms/forms_preview/example_toggle_switch_form.html.erb @@ -0,0 +1 @@ +<%= render(ExampleToggleSwitchForm.new(csrf: "let_me_in", src: toggle_switch_index_path)) %> diff --git a/test/lib/primer/forms/toggle_switch_form_test.rb b/test/lib/primer/forms/toggle_switch_form_test.rb new file mode 100644 index 0000000000..da8afd403a --- /dev/null +++ b/test/lib/primer/forms/toggle_switch_form_test.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require "lib/test_helper" + +class Primer::Forms::ToggleSwitchFormTest < Minitest::Test + include Primer::ComponentTestHelpers + + def test_it_renders_with_a_name + bogus_csrf = "let me in" + render_inline(ExampleToggleSwitchForm.new(csrf: bogus_csrf, src: "/toggle_switch")) + + assert_selector "toggle-switch[src='/toggle_switch'][csrf='#{bogus_csrf}']" + end + + def test_can_render_without_subclass + render_inline( + Primer::Forms::ToggleSwitchForm.new( + name: :example_field, + label: "Example", + src: "/toggle_switch" + ) + ) + + assert_selector "toggle-switch[src='/toggle_switch']" + end +end