From e66c074beed568c4f719d4c4834a025e64b39b63 Mon Sep 17 00:00:00 2001 From: Weston Ganger Date: Fri, 24 Mar 2023 18:50:39 -0700 Subject: [PATCH] Add bootstrap 5 themes --- CHANGELOG.md | 1 + README.md | 18 +++-- lib/sexy_form.rb | 7 +- lib/sexy_form/themes/bootstrap_5_base.rb | 56 ++++++++++++++ .../themes/bootstrap_5_horizontal.rb | 57 ++++++++++++++ lib/sexy_form/themes/bootstrap_5_inline.rb | 39 ++++++++++ lib/sexy_form/themes/bootstrap_5_vertical.rb | 36 +++++++++ .../sexy_form/themes/bootstrap_5_base_spec.rb | 74 +++++++++++++++++++ .../themes/bootstrap_5_horizontal_spec.rb | 65 ++++++++++++++++ .../themes/bootstrap_5_inline_spec.rb | 51 +++++++++++++ .../themes/bootstrap_5_vertical_spec.rb | 46 ++++++++++++ spec/sexy_form/themes_spec.rb | 4 +- spec/sexy_form_spec.rb | 26 ++++++- 13 files changed, 469 insertions(+), 11 deletions(-) create mode 100644 lib/sexy_form/themes/bootstrap_5_base.rb create mode 100644 lib/sexy_form/themes/bootstrap_5_horizontal.rb create mode 100644 lib/sexy_form/themes/bootstrap_5_inline.rb create mode 100644 lib/sexy_form/themes/bootstrap_5_vertical.rb create mode 100644 spec/sexy_form/themes/bootstrap_5_base_spec.rb create mode 100644 spec/sexy_form/themes/bootstrap_5_horizontal_spec.rb create mode 100644 spec/sexy_form/themes/bootstrap_5_inline_spec.rb create mode 100644 spec/sexy_form/themes/bootstrap_5_vertical_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fc1ae6..fe6dc3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG - [View Diff](https://github.com/westonganger/sexy_form.rb/compare/v0.9.0...master) - [#2](https://github.com/westonganger/sexy_form.rb/pulls/2) - Remove validations on field `:type` option - Fix bug when field `:errors` is provided but is empty + - [#3](https://github.com/westonganger/sexy_form.rb/pulls/3) - Add bootstrap 5 themes - **0.9.0** - February 15, 2019 - Gem Initial Release diff --git a/README.md b/README.md index 3ef2958..1d09e34 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,11 @@ Dead simple HTML form builder for Ruby with built-in support for many popular UI Out of the box Form Builder can generate HTML markup for the following UI libraries: -- Bootstrap 4 +- Bootstrap 5 + * `theme: :bootstrap_5_vertical` + * `theme: :bootstrap_5_inline` + * `theme: :bootstrap_5_horizontal` or `theme: SexyForm::Themes::Bootstrap4Horizontal.new(column_classes: ["col-sm-3","col-sm-9"])` +- Bootstrap 4 * `theme: :bootstrap_4_vertical` * `theme: :bootstrap_4_inline` * `theme: :bootstrap_4_horizontal` or `theme: SexyForm::Themes::Bootstrap4Horizontal.new(column_classes: ["col-sm-3","col-sm-9"])` @@ -91,8 +95,8 @@ The following field types are supported: ### label_html : (Optional) Hash ### contains attributes to be added to the label ### wrapper_html : (Optional) Hash ### contains attributes to be added to the outer wrapper for the label and input ### help_text_html : (Optional) Hash ### contains attributes to be added to the help text container - ### error_html : (Optional) Hash ### contains attributes to be added to the error container(s) - + ### error_html : (Optional) Hash ### contains attributes to be added to the error container(s) + = f.field name: "product[name]", label: "Name", type: :text, errors: product_errors["name"] = f.field name: "product[description]", label: "Description", type: :textarea, input_html: {class: "foobar"}, wrapper_html: {style: "margin-top: 10px"}, label_html: {style: "color: red;"} @@ -200,9 +204,9 @@ module SexyForm s << "#{html_label}" s << "#{html_field}" end - + s << "#{html_help_text}" - + if html_errors s << html_errors.join end @@ -215,11 +219,11 @@ module SexyForm def input_html_attributes(field_type: , has_errors: , html_attrs:) html_attrs["class"] = "form-field other-class #{html_attrs["class"]}".strip html_attrs["style"] = "color: blue; #{html_attrs["style"]}".strip - + unless html_attrs.has_key?("data-foo") html_attrs["data-foo"] = "bar" end - + html_attrs end diff --git a/lib/sexy_form.rb b/lib/sexy_form.rb index b95aae2..e573fd5 100644 --- a/lib/sexy_form.rb +++ b/lib/sexy_form.rb @@ -51,7 +51,12 @@ def self.form(action: nil, method: "post", theme: nil, form_html: {}) protected def self.build_html_attr_string(hash) - hash.map{|k, v| "#{k}=\"#{v}\""}.join(" ") + hash.delete_if{|_,v| v.nil? || v.to_s.strip.empty? }.map{|k, v| "#{k}=\"#{v.to_s.strip}\""}.join(" ") + end + + def self.build_html_element(type, hash) + attr_str = build_html_attr_string(hash) + attr_str.empty? ? "<#{type}>" : "<#{type} #{attr_str}>" end def self.safe_string_hash(h) diff --git a/lib/sexy_form/themes/bootstrap_5_base.rb b/lib/sexy_form/themes/bootstrap_5_base.rb new file mode 100644 index 0000000..e13c3a2 --- /dev/null +++ b/lib/sexy_form/themes/bootstrap_5_base.rb @@ -0,0 +1,56 @@ +module SexyForm + module Themes + module Bootstrap5Base + + def input_html_attributes(html_attrs:, field_type:, has_errors:) + html_attrs["class"] ||= "" + html_attrs["class"].concat(" is-invalid").strip! if has_errors + + case field_type + when "checkbox", "radio" + html_attrs["class"].concat(" form-check-input").strip! + when "select" + html_attrs["class"].concat(" form-select").strip! + else + html_attrs["class"].concat(" form-control").strip! + end + + html_attrs + end + + def label_html_attributes(html_attrs:, field_type:, has_errors:) + if ["checkbox", "radio"].include?(field_type) + html_attrs["class"] = "form-check-label #{html_attrs['class']}".strip + end + + html_attrs + end + + def form_html_attributes(html_attrs:) + html_attrs + end + + def build_html_help_text(help_text:, html_attrs:, field_type:) + html_attrs["class"] = "form-text #{html_attrs['class']}".strip + html_attrs["style"] = "display:block; #{html_attrs['style']}".strip + + s = "" + s << SexyForm.build_html_element(:small, html_attrs) + s << "#{help_text}" + s << "" + s + end + + def build_html_error(error:, html_attrs:, field_type:) + html_attrs["class"] = "invalid-feedback #{html_attrs['class']}".strip + + s = "" + s << "
" + s << "#{error}" + s << "
" + s + end + + end + end +end diff --git a/lib/sexy_form/themes/bootstrap_5_horizontal.rb b/lib/sexy_form/themes/bootstrap_5_horizontal.rb new file mode 100644 index 0000000..0936a04 --- /dev/null +++ b/lib/sexy_form/themes/bootstrap_5_horizontal.rb @@ -0,0 +1,57 @@ +module SexyForm + module Themes + class Bootstrap5Horizontal < BaseTheme + include Bootstrap5Base + + def self.theme_name + "bootstrap_5_horizontal" + end + + def initialize(column_classes: ["col-sm-3", "col-sm-9"]) + @column_classes = column_classes.first(2) + + s = "#{@column_classes[0]}" + @offset_class = (i = s.index(/-\d/)) ? s.insert(i+1, "offset-") : "" + end + + def wrap_field(field_type:, html_field:, html_label:, html_help_text: nil, html_errors: nil, wrapper_html_attributes:) + s = "" + + s << SexyForm.build_html_element(:div, wrapper_html_attributes) + + if ["checkbox", "radio"].include?(field_type) + s << %Q(
) + + s << %Q(
) + + s << %Q(
) + s << %Q(
) + s << "#{html_field}" + s << "#{html_label}" + s << "#{html_help_text}" + s << html_errors.join if html_errors + s << "
" + s << "
" + + s << "
" + else + s << %Q(
) + + s << %Q(
) + s << "#{html_label}" + s << "
" + + s << %Q(
) + s << "#{html_field}" + s << "#{html_help_text}" + s << html_errors.join if html_errors + s << "
" + + s << "
" + end + + s << "" + end + end + end +end diff --git a/lib/sexy_form/themes/bootstrap_5_inline.rb b/lib/sexy_form/themes/bootstrap_5_inline.rb new file mode 100644 index 0000000..6d1274c --- /dev/null +++ b/lib/sexy_form/themes/bootstrap_5_inline.rb @@ -0,0 +1,39 @@ +module SexyForm + module Themes + class Bootstrap5Inline < BaseTheme + include Bootstrap5Base + + def self.theme_name + "bootstrap_5_inline" + end + + def wrap_field(field_type:, html_field:, html_label:, html_help_text: nil, html_errors: nil, wrapper_html_attributes:) + s = "" + + if ["checkbox", "radio"].include?(field_type) + wrapper_html_attributes["class"] = "col-auto form-check #{wrapper_html_attributes['class']}".strip + + s << SexyForm.build_html_element(:div, wrapper_html_attributes) + s << html_field + s << html_label + else + s << %Q(
#{html_label}
) + s << %Q(
) + s << html_field + end + + s << "#{html_help_text}" + s << html_errors.join if html_errors + s << "
" + + s + end + + def form_html_attributes(html_attrs:) + html_attrs["class"] = "row #{html_attrs['class']}".strip + super(html_attrs: html_attrs) + end + + end + end +end diff --git a/lib/sexy_form/themes/bootstrap_5_vertical.rb b/lib/sexy_form/themes/bootstrap_5_vertical.rb new file mode 100644 index 0000000..bc09cba --- /dev/null +++ b/lib/sexy_form/themes/bootstrap_5_vertical.rb @@ -0,0 +1,36 @@ +module SexyForm + module Themes + class Bootstrap5Vertical < BaseTheme + include Bootstrap5Base + + def self.theme_name + "bootstrap_5_vertical" + end + + def wrap_field(field_type:, html_field:, html_label:, html_help_text: nil, html_errors: nil, wrapper_html_attributes:) + s = "" + + if ["checkbox", "radio"].include?(field_type) + wrapper_html_attributes["class"] = "form-check #{wrapper_html_attributes['class']}".strip + end + + s << SexyForm.build_html_element(:div, wrapper_html_attributes) + + if ["checkbox", "radio"].include?(field_type) + s << "#{html_field}" + s << "#{html_label}" + else + s << "#{html_label}" + s << "#{html_field}" + end + s << "#{html_help_text}" + s << html_errors.join if html_errors + + s << "" + + s + end + + end + end +end diff --git a/spec/sexy_form/themes/bootstrap_5_base_spec.rb b/spec/sexy_form/themes/bootstrap_5_base_spec.rb new file mode 100644 index 0000000..77912af --- /dev/null +++ b/spec/sexy_form/themes/bootstrap_5_base_spec.rb @@ -0,0 +1,74 @@ +require_relative "../../spec_helper" +require_relative "theme_spec_helper" + +base_klass = SexyForm::Themes::Bootstrap5Base +theme_klass = SexyForm::Themes::Bootstrap5Horizontal +theme = theme_klass.new + +describe theme do + + it "is not a valid theme" do + expect(base_klass < SexyForm::Themes::BaseTheme).to eq(nil) + expect{ SexyForm.form(theme: base_klass) }.to raise_error(ArgumentError) + end + + describe "form_html_attributes" do + it "returns correct attributes" do + attrs = {} + + theme.form_html_attributes(html_attrs: {}).should eq(attrs) + end + end + + TESTED_FIELD_TYPES.each do |field_type| + describe "input_html_attributes" do + it "returns correct #{field_type} attributes" do + attrs = {} + + case field_type + when "checkbox", "radio" + attrs["class"] = "form-check-input" + when "select" + attrs["class"] = "form-select" + else + attrs["class"] = "form-control" + end + + theme.input_html_attributes(html_attrs: {}, field_type: field_type, has_errors: false).should eq(attrs) + end + end + + describe "label_html_attributes" do + it "returns correct #{field_type} attributes" do + attrs = {} + + if ["checkbox", "radio"].include?(field_type) + attrs["class"] = "form-check-label" + end + + theme.label_html_attributes(html_attrs: {}, field_type: field_type, has_errors: false).should eq(attrs) + end + end + + describe "build_html_help_text" do + it "returns correct #{field_type} attributes" do + expected = %Q(foobar) + + attrs = {} + + theme.build_html_help_text(html_attrs: attrs, field_type: field_type, help_text: "foobar").should eq(expected) + end + end + + describe "build_html_error" do + it "returns correct #{field_type} attributes" do + expected = "
foobar
" + + attrs = {} + + theme.build_html_error(html_attrs: attrs, field_type: field_type, error: "foobar").should eq(expected) + end + end + end + +end diff --git a/spec/sexy_form/themes/bootstrap_5_horizontal_spec.rb b/spec/sexy_form/themes/bootstrap_5_horizontal_spec.rb new file mode 100644 index 0000000..c80d9d9 --- /dev/null +++ b/spec/sexy_form/themes/bootstrap_5_horizontal_spec.rb @@ -0,0 +1,65 @@ +require_relative "../../spec_helper" +require_relative "theme_spec_helper" + +theme_klass = SexyForm::Themes::Bootstrap5Horizontal +theme = theme_klass.new + +describe theme_klass do + + describe "theme_name" do + it "is correct" do + theme_klass.theme_name.should eq("bootstrap_5_horizontal") + end + end + + describe "SexyForm.form" do + it "matches docs example" do + expected = build_string do |str| + str << %Q(
) + str << %Q(
) + str << %Q(
) + str << %Q(
) + str << %Q() + str << %Q(
) + str << %Q(
) + str << %Q() + str << %Q(
) + str << %Q(
) + str << %Q(
) + + str << %Q(
) + str << %Q(
) + str << %Q(
) + str << %Q() + str << %Q(
) + str << %Q(
) + str << %Q() + str << %Q(
) + str << %Q(
) + str << %Q(
) + + str << %Q(
) + str << %Q(
) + str << %Q(
) + str << %Q(
) + str << %Q(
) + str << %Q() + str << %Q() + str << %Q(
) + str << %Q(
) + str << %Q(
) + str << %Q(
) + str << %Q(
) + end + + actual = SexyForm.form(theme: theme_klass.new(column_classes: ["col-sm-3", "col-sm-9"])) do |f| + f << f.field(type: :text, name: :email) + f << f.field(type: :password, name: :password) + f << f.field(type: :checkbox, name: :remember_me) + end + + actual.should eq(expected) + end + end + +end diff --git a/spec/sexy_form/themes/bootstrap_5_inline_spec.rb b/spec/sexy_form/themes/bootstrap_5_inline_spec.rb new file mode 100644 index 0000000..6b1ef98 --- /dev/null +++ b/spec/sexy_form/themes/bootstrap_5_inline_spec.rb @@ -0,0 +1,51 @@ +require_relative "../../spec_helper" +require_relative "theme_spec_helper" + +theme_klass = SexyForm::Themes::Bootstrap5Inline +theme = theme_klass.new + +describe theme_klass do + + describe "theme_name" do + it "is correct" do + theme_klass.theme_name.should eq("bootstrap_5_inline") + end + end + + describe "SexyForm.form" do + it "matches docs example" do + expected = build_string do |str| + str << %Q(
) + str << %Q(
) + str << %Q() + str << %Q(
) + str << %Q(
) + str << %Q() + str << %Q(
) + + str << %Q(
) + str << %Q() + str << %Q(
) + str << %Q(
) + str << %Q() + str << %Q(
) + + str << %Q(
) + str << %Q() + str << %Q() + str << %Q(
) + + str << %Q(
) + end + + actual = SexyForm.form(theme: theme_klass.theme_name) do |f| + f << f.field(type: :text, name: :email) + f << f.field(type: :password, name: :password) + f << f.field(type: :checkbox, name: :remember_me) + end + + actual.should eq(expected) + end + end + +end diff --git a/spec/sexy_form/themes/bootstrap_5_vertical_spec.rb b/spec/sexy_form/themes/bootstrap_5_vertical_spec.rb new file mode 100644 index 0000000..b14a9d3 --- /dev/null +++ b/spec/sexy_form/themes/bootstrap_5_vertical_spec.rb @@ -0,0 +1,46 @@ +require_relative "../../spec_helper" +require_relative "theme_spec_helper" + +theme_klass = SexyForm::Themes::Bootstrap5Vertical +theme = theme_klass.new + +describe theme_klass do + + describe "theme_name" do + it "is correct" do + theme_klass.theme_name.should eq("bootstrap_5_vertical") + end + end + + describe "SexyForm.form" do + it "matches docs example" do + expected = build_string do |str| + str << %Q(
) + str << %Q(
) + str << %Q() + str << %Q() + str << %Q(
) + + str << %Q(
) + str << %Q() + str << %Q() + str << %Q(
) + + str << %Q(
) + str << %Q() + str << %Q() + str << %Q(
) + str << %Q(
) + end + + actual = SexyForm.form(theme: theme_klass.theme_name) do |f| + f << f.field(type: :text, name: :email) + f << f.field(type: :password, name: :password) + f << f.field(type: :checkbox, name: :remember_me) + end + + actual.should eq(expected) + end + end + +end diff --git a/spec/sexy_form/themes_spec.rb b/spec/sexy_form/themes_spec.rb index 1d219f5..7d9629c 100644 --- a/spec/sexy_form/themes_spec.rb +++ b/spec/sexy_form/themes_spec.rb @@ -6,11 +6,11 @@ describe ".classes" do it "Comes with default themes" do - classes = [SexyForm::Themes::Bootstrap2Horizontal, SexyForm::Themes::Bootstrap2Inline, SexyForm::Themes::Bootstrap2Vertical, SexyForm::Themes::Bootstrap3Horizontal, SexyForm::Themes::Bootstrap3Inline, SexyForm::Themes::Bootstrap3Vertical, SexyForm::Themes::Bootstrap4Horizontal, SexyForm::Themes::Bootstrap4Inline, SexyForm::Themes::Bootstrap4Vertical, SexyForm::Themes::BulmaHorizontal, SexyForm::Themes::BulmaVertical, SexyForm::Themes::Default, SexyForm::Themes::Foundation, SexyForm::Themes::Materialize, SexyForm::Themes::Milligram, SexyForm::Themes::SemanticUIInline, SexyForm::Themes::SemanticUIVertical] + classes = [SexyForm::Themes::Bootstrap2Horizontal, SexyForm::Themes::Bootstrap2Inline, SexyForm::Themes::Bootstrap2Vertical, SexyForm::Themes::Bootstrap3Horizontal, SexyForm::Themes::Bootstrap3Inline, SexyForm::Themes::Bootstrap3Vertical, SexyForm::Themes::Bootstrap4Horizontal, SexyForm::Themes::Bootstrap4Inline, SexyForm::Themes::Bootstrap4Vertical, SexyForm::Themes::Bootstrap5Horizontal, SexyForm::Themes::Bootstrap5Inline, SexyForm::Themes::Bootstrap5Vertical, SexyForm::Themes::BulmaHorizontal, SexyForm::Themes::BulmaVertical, SexyForm::Themes::Default, SexyForm::Themes::Foundation, SexyForm::Themes::Materialize, SexyForm::Themes::Milligram, SexyForm::Themes::SemanticUIInline, SexyForm::Themes::SemanticUIVertical] SexyForm::Themes.classes.should eq(classes) - expected = ["bootstrap_2_horizontal", "bootstrap_2_inline", "bootstrap_2_vertical", "bootstrap_3_horizontal", "bootstrap_3_inline", "bootstrap_3_vertical", "bootstrap_4_horizontal", "bootstrap_4_inline", "bootstrap_4_vertical", "bulma_horizontal", "bulma_vertical", "default", "foundation", "materialize", "milligram", "semantic_ui_inline", "semantic_ui_vertical"] + expected = ["bootstrap_2_horizontal", "bootstrap_2_inline", "bootstrap_2_vertical", "bootstrap_3_horizontal", "bootstrap_3_inline", "bootstrap_3_vertical", "bootstrap_4_horizontal", "bootstrap_4_inline", "bootstrap_4_vertical", "bootstrap_5_horizontal", "bootstrap_5_inline", "bootstrap_5_vertical", "bulma_horizontal", "bulma_vertical", "default", "foundation", "materialize", "milligram", "semantic_ui_inline", "semantic_ui_vertical"] classes.map{|x| x.theme_name}.should eq(expected) end diff --git a/spec/sexy_form_spec.rb b/spec/sexy_form_spec.rb index 19347ee..80c8c21 100644 --- a/spec/sexy_form_spec.rb +++ b/spec/sexy_form_spec.rb @@ -6,7 +6,7 @@ SexyForm::VERSION.should be_a(String) end - describe ".form" do + describe "form" do it "allows no block" do result = SexyForm.form(action: "/products", method: :post, form_html: {style: "margin-top: 20px;", "data-foo": "bar"}) @@ -51,4 +51,28 @@ end end + describe "build_html_attr_string" do + it "removes nil and blank values" do + expect(SexyForm.build_html_attr_string({foo: nil, bar: " "})).to eq("") + end + + it "works with symbol values" do + expect(SexyForm.build_html_attr_string({foo: :bar})).to eq(%Q(foo="bar")) + end + + it "strips present values" do + expect(SexyForm.build_html_attr_string({foo: "bar "})).to eq(%Q(foo="bar")) + end + end + + describe "build_html_element" do + it "works without attrs" do + expect(SexyForm.build_html_element(:span, {})).to eq("") + end + + it "works with attrs" do + expect(SexyForm.build_html_element(:span, {foo: :bar})).to eq(%Q()) + end + end + end