Skip to content

Commit

Permalink
Add bootstrap 5 themes
Browse files Browse the repository at this point in the history
  • Loading branch information
westonganger committed Mar 25, 2023
1 parent 3bddc69 commit 2769861
Show file tree
Hide file tree
Showing 12 changed files with 451 additions and 4 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ CHANGELOG
- **UNRELEASED**
- [View Diff](https://github.com/westonganger/form_builder.cr/compare/v1.0.1...master)
- [#4](https://github.com/westonganger/form_builder.cr/pulls/4) - Remove validations on field `:type` option
- [#6](https://github.com/westonganger/form_builder.cr/pulls/6) - Add bootstrap 5 themes

- **1.0.1** - Apr 24, 2021
- [View Diff](https://github.com/westonganger/form_builder.cr/compare/v1.0.0...v1.0.1)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ crystal spec

# Ruby Alternative

This library has been ported to the Ruby language as [SexyForm.rb](https://github.com/westonganger/sexy_form.rb)
This library has been ported to the Ruby language as [FormBuilder.rb](https://github.com/westonganger/sexy_form.rb)

The pattern/implementation of this form builder library turned out so beautifully that I felt the desire to have the same syntax available in the Ruby language. Many Crystal developers also write Ruby and vice versa so this only made sense. What was awesome is that, the Crystal and Ruby syntax is so similar that converting Crystal code to Ruby was straight forward and quite simple.

Expand Down
76 changes: 76 additions & 0 deletions spec/form_builder/themes/bootstrap_5_base_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
require "../../spec_helper"
require "./theme_spec_helper"

base_klass = FormBuilder::Themes::Bootstrap5Base
theme_klass = FormBuilder::Themes::Bootstrap5Horizontal
theme = theme_klass.new

describe theme_klass do

describe ".theme_name" do
it "is not a valid theme" do
(base_klass < FormBuilder::Themes::BaseTheme).should eq(false)
expect_raises(ArgumentError){ FormBuilder.form(theme: base_klass.name.underscore) }
end
end

describe "form_html_attributes" do
it "returns correct attributes" do
attrs = StringHash.new

theme.form_html_attributes(html_attrs: StringHash.new).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 = StringHash.new

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: StringHash.new, 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 = StringHash.new

if {"checkbox", "radio"}.includes?(field_type)
attrs["class"] = "form-check-label"
end

theme.label_html_attributes(html_attrs: StringHash.new, 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 = %(<small class="form-text" style="display:block;">foobar</small>)

attrs = StringHash.new

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 = "<div class=\"invalid-feedback\">foobar</div>"

attrs = StringHash.new

theme.build_html_error(html_attrs: attrs, field_type: field_type, error: "foobar").should eq(expected)
end
end
end

end
67 changes: 67 additions & 0 deletions spec/form_builder/themes/bootstrap_5_horizontal_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
require "../../spec_helper"
require "./theme_spec_helper"

theme_klass = FormBuilder::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 "FormBuilder.form" do
it "matches docs example" do
expected = String.build do |str|
str << %Q(<form method="post">)

str << %Q(<div>)
str << %Q(<div class="row">)
str << %Q(<div class="col-sm-3">)
str << %Q(<label for="email">Email</label>)
str << %Q(</div>)
str << %Q(<div class="col-sm-9">)
str << %Q(<input type="text" class="form-control" name="email" id="email">)
str << %Q(</div>)
str << %Q(</div>)
str << %Q(</div>)

str << %Q(<div>)
str << %Q(<div class="row">)
str << %Q(<div class="col-sm-3">)
str << %Q(<label for="password">Password</label>)
str << %Q(</div>)
str << %Q(<div class="col-sm-9">)
str << %Q(<input type="password" class="form-control" name="password" id="password">)
str << %Q(</div>)
str << %Q(</div>)
str << %Q(</div>)

str << %Q(<div>)
str << %Q(<div class="row">)
str << %Q(<div class="col-sm-3"></div>)
str << %Q(<div class="col-sm-9">)
str << %Q(<div class="form-check">)
str << %Q(<input type="checkbox" class="form-check-input" name="remember_me" id="remember_me">)
str << %Q(<label class="form-check-label" for="remember_me">Remember Me</label>)
str << %Q(</div>)
str << %Q(</div>)
str << %Q(</div>)
str << %Q(</div>)

str << %Q(</form>)
end

actual = FormBuilder.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
51 changes: 51 additions & 0 deletions spec/form_builder/themes/bootstrap_5_inline_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
require "../../spec_helper"
require "./theme_spec_helper"

theme_klass = FormBuilder::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 "FormBuilder.form" do
it "matches docs example" do
expected = String.build do |str|
str << %Q(<form class="row" method="post">)
str << %Q(<div class="col-auto">)
str << %Q(<label for="email">Email</label>)
str << %Q(</div>)
str << %Q(<div class="col-auto">)
str << %Q(<input type="text" class="form-control" name="email" id="email">)
str << %Q(</div>)

str << %Q(<div class="col-auto">)
str << %Q(<label for="password">Password</label>)
str << %Q(</div>)
str << %Q(<div class="col-auto">)
str << %Q(<input type="password" class="form-control" name="password" id="password">)
str << %Q(</div>)

str << %Q(<div class="col-auto form-check">)
str << %Q(<input type="checkbox" class="form-check-input" name="remember_me" id="remember_me">)
str << %Q(<label class="form-check-label" for="remember_me">Remember Me</label>)
str << %Q(</div>)

str << %Q(</form>)
end

actual = FormBuilder.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
46 changes: 46 additions & 0 deletions spec/form_builder/themes/bootstrap_5_vertical_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
require "../../spec_helper"
require "./theme_spec_helper"

theme_klass = FormBuilder::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 "FormBuilder.form" do
it "matches docs example" do
expected = String.build do |str|
str << %Q(<form method="post">)
str << %Q(<div>)
str << %Q(<label for="email">Email</label>)
str << %Q(<input type="text" class="form-control" name="email" id="email">)
str << %Q(</div>)

str << %Q(<div>)
str << %Q(<label for="password">Password</label>)
str << %Q(<input type="password" class="form-control" name="password" id="password">)
str << %Q(</div>)

str << %Q(<div class="form-check">)
str << %Q(<input type="checkbox" class="form-check-input" name="remember_me" id="remember_me">)
str << %Q(<label class="form-check-label" for="remember_me">Remember Me</label>)
str << %Q(</div>)
str << %Q(</form>)
end

actual = FormBuilder.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
4 changes: 2 additions & 2 deletions spec/form_builder/themes_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ describe FormBuilder::Themes do

describe ".classes" do
it "Comes with default themes" do
classes = [FormBuilder::Themes::Bootstrap2Horizontal, FormBuilder::Themes::Bootstrap2Inline, FormBuilder::Themes::Bootstrap2Vertical, FormBuilder::Themes::Bootstrap3Horizontal, FormBuilder::Themes::Bootstrap3Inline, FormBuilder::Themes::Bootstrap3Vertical, FormBuilder::Themes::Bootstrap4Horizontal, FormBuilder::Themes::Bootstrap4Inline, FormBuilder::Themes::Bootstrap4Vertical, FormBuilder::Themes::BulmaHorizontal, FormBuilder::Themes::BulmaVertical, FormBuilder::Themes::Default, FormBuilder::Themes::Foundation, FormBuilder::Themes::Materialize, FormBuilder::Themes::Milligram, FormBuilder::Themes::SemanticUIInline, FormBuilder::Themes::SemanticUIVertical]
classes = [FormBuilder::Themes::Bootstrap2Horizontal, FormBuilder::Themes::Bootstrap2Inline, FormBuilder::Themes::Bootstrap2Vertical, FormBuilder::Themes::Bootstrap3Horizontal, FormBuilder::Themes::Bootstrap3Inline, FormBuilder::Themes::Bootstrap3Vertical, FormBuilder::Themes::Bootstrap4Horizontal, FormBuilder::Themes::Bootstrap4Inline, FormBuilder::Themes::Bootstrap4Vertical, FormBuilder::Themes::Bootstrap5Horizontal, FormBuilder::Themes::Bootstrap5Inline, FormBuilder::Themes::Bootstrap5Vertical, FormBuilder::Themes::BulmaHorizontal, FormBuilder::Themes::BulmaVertical, FormBuilder::Themes::Default, FormBuilder::Themes::Foundation, FormBuilder::Themes::Materialize, FormBuilder::Themes::Milligram, FormBuilder::Themes::SemanticUIInline, FormBuilder::Themes::SemanticUIVertical]

FormBuilder::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
Expand Down
7 changes: 6 additions & 1 deletion src/form_builder.cr
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,12 @@ module FormBuilder
end

protected def self.build_html_attr_string(h : Hash)
h.map{|k, v| "#{k}=\"#{v}\""}.join(" ")
h.reject{|_,v| v.nil? || v.to_s.strip.empty? }.map{|k, v| "#{k}=\"#{v.to_s.strip}\""}.join(" ")
end

def self.build_html_element(type : (String|Symbol), h : Hash)
attr_str = build_html_attr_string(h)
attr_str.empty? ? "<#{type}>" : "<#{type} #{attr_str}>"
end

protected def self.safe_string_hash(h : Hash)
Expand Down
74 changes: 74 additions & 0 deletions src/form_builder/themes/bootstrap_5_base.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
module FormBuilder
module Themes
module Bootstrap5Base

def wrap_field(field_type : String, html_field : String, html_label : String?, html_help_text : String?, html_errors : Array(String)?, wrapper_html_attributes : StringHash)
String.build do |s|
wrapper_html_attributes["class"] = "form-group #{"form-check" if {"checkbox", "radio"}.includes?(field_type)} #{wrapper_html_attributes["class"]?}".strip

attr_str = FormBuilder.build_html_attr_string(wrapper_html_attributes)
s << "#{attr_str.empty? ? "<div>" : %(<div #{attr_str}>)}"

if {"checkbox", "radio"}.includes?(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 << "</div>"
end
end

def input_html_attributes(html_attrs : StringHash, field_type : String, has_errors? : Bool)
case field_type
when "checkbox", "radio"
html_attrs["class"] = "form-check-input#{" is-invalid" if has_errors?} #{html_attrs["class"]?}".strip
when "select"
html_attrs["class"] = "form-select#{" is-invalid" if has_errors?} #{html_attrs["class"]?}".strip
else
html_attrs["class"] = "form-control#{" is-invalid" if has_errors?} #{html_attrs["class"]?}".strip
end

html_attrs
end

def label_html_attributes(html_attrs : StringHash, field_type : String, has_errors? : Bool)
if {"checkbox", "radio"}.includes?(field_type)
html_attrs["class"] = "form-check-label #{html_attrs["class"]?}".strip
end

html_attrs
end

def form_html_attributes(html_attrs : StringHash)
html_attrs
end

def build_html_help_text(help_text : String, html_attrs : StringHash, field_type : String)
html_attrs["class"] = "form-text #{html_attrs["class"]?}".strip
html_attrs["style"] = "display:block; #{html_attrs["style"]?}".strip

String.build do |s|
s << FormBuilder.build_html_element(:small, html_attrs)
s << help_text
s << "</small>"
end
end

def build_html_error(error : String, html_attrs : StringHash, field_type : String)
html_attrs["class"] = "invalid-feedback #{html_attrs["class"]?}".strip

String.build do |s|
s << FormBuilder.build_html_element(:div, html_attrs)
s << error
s << "</div>"
end
end

end
end
end
Loading

0 comments on commit 2769861

Please sign in to comment.