superform 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +131 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +128 -27
- data/README.md +183 -54
- data/SPEC_STYLE_GUIDE.md +146 -0
- data/lib/generators/superform/install/USAGE +6 -2
- data/lib/generators/superform/install/install_generator.rb +11 -22
- data/lib/generators/superform/install/templates/base.rb +33 -0
- data/lib/superform/field.rb +82 -0
- data/lib/superform/form.rb +34 -0
- data/lib/superform/namespace.rb +25 -15
- data/lib/superform/namespace_collection.rb +7 -4
- data/lib/superform/rails/components/base.rb +31 -0
- data/lib/superform/rails/components/button.rb +20 -0
- data/lib/superform/rails/components/checkbox.rb +19 -0
- data/lib/superform/rails/components/field.rb +11 -0
- data/lib/superform/rails/components/input.rb +59 -0
- data/lib/superform/rails/components/label.rb +20 -0
- data/lib/superform/rails/components/select.rb +43 -0
- data/lib/superform/rails/components/textarea.rb +12 -0
- data/lib/superform/rails/form.rb +240 -0
- data/lib/superform/rails/option_mapper.rb +36 -0
- data/lib/superform/rails/strong_parameters.rb +73 -0
- data/lib/superform/rails.rb +17 -368
- data/lib/superform/version.rb +1 -1
- metadata +21 -14
- data/lib/generators/superform/install/templates/application_form.rb +0 -31
data/lib/superform/field.rb
CHANGED
@@ -10,6 +10,7 @@ module Superform
|
|
10
10
|
@object = object
|
11
11
|
@value = value
|
12
12
|
@dom = Superform::DOM.new(field: self)
|
13
|
+
yield self if block_given?
|
13
14
|
end
|
14
15
|
|
15
16
|
def value
|
@@ -35,5 +36,86 @@ module Superform
|
|
35
36
|
def collection(&)
|
36
37
|
@collection ||= FieldCollection.new(field: self, &)
|
37
38
|
end
|
39
|
+
|
40
|
+
# Make the name more obvious for extending or writing docs.
|
41
|
+
def field
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
# High-performance Kit proxy that wraps field methods with form.render calls.
|
46
|
+
# Uses Ruby class hooks to define methods at the class level for maximum speed:
|
47
|
+
# - Methods are defined once per Field class, not per Kit instance
|
48
|
+
# - True Ruby methods with full VM optimization (no method_missing overhead)
|
49
|
+
# - ~125x faster Kit instantiation compared to instance-level dynamic methods
|
50
|
+
# - Each Field subclass gets its own isolated Kit class with true isolation:
|
51
|
+
# * Methods are copied at subclass creation time, not inherited dynamically
|
52
|
+
# * Adding methods to a parent Field class won't affect existing subclass Kits
|
53
|
+
# * Perfect for library design where you don't want parent changes affecting subclasses
|
54
|
+
class Kit
|
55
|
+
def initialize(field:, form:)
|
56
|
+
@field = field
|
57
|
+
@form = form
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.inherited(subclass)
|
62
|
+
super
|
63
|
+
# Create a new Kit class for each Field subclass with true isolation
|
64
|
+
# Copy methods from parent Field classes at creation time, not through inheritance
|
65
|
+
subclass.const_set(:Kit, Class.new(Field::Kit))
|
66
|
+
|
67
|
+
# Copy all existing methods from the inheritance chain
|
68
|
+
field_class = self
|
69
|
+
while field_class != Field
|
70
|
+
copy_field_methods_to_kit(field_class, subclass::Kit)
|
71
|
+
field_class = field_class.superclass
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.method_added(method_name)
|
76
|
+
super
|
77
|
+
# Skip if this is the base Field class or if we don't have a Kit class yet
|
78
|
+
return if self == Field
|
79
|
+
return unless const_defined?(:Kit, false)
|
80
|
+
|
81
|
+
# Only add method to THIS class's Kit, not subclasses (isolation)
|
82
|
+
add_method_to_kit(method_name, self::Kit)
|
83
|
+
end
|
84
|
+
|
85
|
+
def kit(form)
|
86
|
+
self.class::Kit.new(field: self, form: form)
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def self.copy_field_methods_to_kit(field_class, kit_class)
|
92
|
+
base_methods = (Object.instance_methods + Node.instance_methods +
|
93
|
+
[:dom, :value, :serialize, :assign, :collection, :field, :kit]).to_set
|
94
|
+
|
95
|
+
field_class.instance_methods(false).each do |method_name|
|
96
|
+
next if method_name.to_s.end_with?('=')
|
97
|
+
next if base_methods.include?(method_name)
|
98
|
+
next if kit_class.method_defined?(method_name)
|
99
|
+
|
100
|
+
kit_class.define_method(method_name) do |*args, **kwargs, &block|
|
101
|
+
result = @field.send(method_name, *args, **kwargs, &block)
|
102
|
+
@form.render result
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.add_method_to_kit(method_name, kit_class)
|
108
|
+
return if method_name.to_s.end_with?('=')
|
109
|
+
|
110
|
+
base_methods = (Object.instance_methods + Node.instance_methods +
|
111
|
+
[:dom, :value, :serialize, :assign, :collection, :field, :kit]).to_set
|
112
|
+
return if base_methods.include?(method_name)
|
113
|
+
return if kit_class.method_defined?(method_name)
|
114
|
+
|
115
|
+
kit_class.define_method(method_name) do |*args, **kwargs, &block|
|
116
|
+
result = @field.send(method_name, *args, **kwargs, &block)
|
117
|
+
@form.render result
|
118
|
+
end
|
119
|
+
end
|
38
120
|
end
|
39
121
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "phlex"
|
4
|
+
|
5
|
+
module Superform
|
6
|
+
# A basic form component that inherits from Phlex::HTML and wraps content in a form tag.
|
7
|
+
# This provides a simple foundation for building forms without Rails dependencies.
|
8
|
+
#
|
9
|
+
# Example usage:
|
10
|
+
# class MyForm < Superform::Form
|
11
|
+
# def view_template
|
12
|
+
# div { "Form content goes here" }
|
13
|
+
# end
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# form = MyForm.new(action: "/users", method: :post)
|
17
|
+
# form.call # renders <form action="/users" method="post">...</form>
|
18
|
+
class Form < Phlex::HTML
|
19
|
+
def initialize(action: nil, method: :post, **attributes)
|
20
|
+
@action = action
|
21
|
+
@method = method
|
22
|
+
@attributes = attributes
|
23
|
+
super()
|
24
|
+
end
|
25
|
+
|
26
|
+
def around_template(&block)
|
27
|
+
form(action: @action, method: @method, **@attributes, &block)
|
28
|
+
end
|
29
|
+
|
30
|
+
def build_field(key, parent:, object: nil, &block)
|
31
|
+
Field.new(key, parent: parent, object: object, &block)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/superform/namespace.rb
CHANGED
@@ -9,12 +9,12 @@ module Superform
|
|
9
9
|
class Namespace < Node
|
10
10
|
include Enumerable
|
11
11
|
|
12
|
-
attr_reader :object
|
12
|
+
attr_reader :object, :form
|
13
13
|
|
14
|
-
def initialize(key, parent:, object: nil,
|
15
|
-
super(key, parent:
|
14
|
+
def initialize(key, parent:, object: nil, form: Superform::Form.new)
|
15
|
+
super(key, parent:)
|
16
16
|
@object = object
|
17
|
-
@
|
17
|
+
@form = form
|
18
18
|
@children = Hash.new
|
19
19
|
yield self if block_given?
|
20
20
|
end
|
@@ -33,8 +33,8 @@ module Superform
|
|
33
33
|
# end
|
34
34
|
# end
|
35
35
|
# ```
|
36
|
-
def namespace(key, &
|
37
|
-
|
36
|
+
def namespace(key, &)
|
37
|
+
@children[key] ||= build_namespace(key, &)
|
38
38
|
end
|
39
39
|
|
40
40
|
# Maps the `Object#proprety` and `Object#property=` to a field in a web form that can be
|
@@ -46,12 +46,13 @@ module Superform
|
|
46
46
|
# form.field :name
|
47
47
|
# end
|
48
48
|
# ```
|
49
|
-
def field(key)
|
50
|
-
|
51
|
-
yield field if block_given?
|
52
|
-
end
|
49
|
+
def field(key, &)
|
50
|
+
@children[key] ||= build_field(key, &)
|
53
51
|
end
|
54
52
|
|
53
|
+
def Field(...)
|
54
|
+
field(...).kit(@form)
|
55
|
+
end
|
55
56
|
# Wraps an array of objects in Namespace classes. For example, if `User#addresses` returns
|
56
57
|
# an enumerable or array of `Address` classes:
|
57
58
|
#
|
@@ -69,7 +70,7 @@ module Superform
|
|
69
70
|
# The object within the block is a `Namespace` object that maps each object within the enumerable
|
70
71
|
# to another `Namespace` or `Field`.
|
71
72
|
def collection(key, &)
|
72
|
-
|
73
|
+
@children[key] ||= build_collection(key, &)
|
73
74
|
end
|
74
75
|
|
75
76
|
# Creates a Hash of Hashes and Arrays that represent the fields and collections of the Superform.
|
@@ -115,10 +116,19 @@ module Superform
|
|
115
116
|
|
116
117
|
private
|
117
118
|
|
118
|
-
#
|
119
|
-
|
120
|
-
|
121
|
-
|
119
|
+
# Builds a new field child
|
120
|
+
def build_field(key, &)
|
121
|
+
@form.build_field(key, parent: self, object:, &)
|
122
|
+
end
|
123
|
+
|
124
|
+
# Builds a new namespace child
|
125
|
+
def build_namespace(key, &)
|
126
|
+
self.class.new(key, parent: self, object: object_for(key:), form:, &)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Builds a new collection child
|
130
|
+
def build_collection(key, &)
|
131
|
+
NamespaceCollection.new(key, parent: self, form:, &)
|
122
132
|
end
|
123
133
|
end
|
124
134
|
end
|
@@ -5,9 +5,12 @@ module Superform
|
|
5
5
|
class NamespaceCollection < Node
|
6
6
|
include Enumerable
|
7
7
|
|
8
|
-
|
9
|
-
|
8
|
+
attr_reader :form
|
9
|
+
|
10
|
+
def initialize(key, parent:, form: parent.form, &template)
|
11
|
+
super(key, parent:)
|
10
12
|
@template = template
|
13
|
+
@form = form
|
11
14
|
@namespaces = enumerate(parent_collection)
|
12
15
|
end
|
13
16
|
|
@@ -32,13 +35,13 @@ module Superform
|
|
32
35
|
def enumerate(enumerator)
|
33
36
|
Enumerator.new do |y|
|
34
37
|
enumerator.each.with_index do |object, key|
|
35
|
-
y << build_namespace(key, object:
|
38
|
+
y << build_namespace(key, object:)
|
36
39
|
end
|
37
40
|
end
|
38
41
|
end
|
39
42
|
|
40
43
|
def build_namespace(index, **)
|
41
|
-
parent.class.new(index, parent: self, **, &@template)
|
44
|
+
parent.class.new(index, parent: self, form:, **, &@template)
|
42
45
|
end
|
43
46
|
|
44
47
|
def parent_collection
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Superform
|
2
|
+
module Rails
|
3
|
+
module Components
|
4
|
+
class Base < Component
|
5
|
+
attr_reader :field, :dom
|
6
|
+
|
7
|
+
delegate :dom, to: :field
|
8
|
+
|
9
|
+
def initialize(field, attributes: {})
|
10
|
+
@field = field
|
11
|
+
@attributes = attributes
|
12
|
+
end
|
13
|
+
|
14
|
+
def field_attributes
|
15
|
+
{}
|
16
|
+
end
|
17
|
+
|
18
|
+
def focus(value = true)
|
19
|
+
@attributes[:autofocus] = value
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def attributes
|
26
|
+
field_attributes.merge(@attributes)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Superform
|
2
|
+
module Rails
|
3
|
+
module Components
|
4
|
+
class Button < Field
|
5
|
+
def view_template(&content)
|
6
|
+
content ||= Proc.new { button_text }
|
7
|
+
button(**attributes, &content)
|
8
|
+
end
|
9
|
+
|
10
|
+
def button_text
|
11
|
+
@attributes.fetch(:value, dom.value).titleize
|
12
|
+
end
|
13
|
+
|
14
|
+
def field_attributes
|
15
|
+
{ id: dom.id, name: dom.name, value: dom.value }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Superform
|
2
|
+
module Rails
|
3
|
+
module Components
|
4
|
+
class Checkbox < Field
|
5
|
+
def view_template(&)
|
6
|
+
# Rails has a hidden and checkbox input to deal with sending back a value
|
7
|
+
# to the server regardless of if the input is checked or not.
|
8
|
+
input(name: dom.name, type: :hidden, value: "0")
|
9
|
+
# The hard coded keys need to be in here so the user can't overrite them.
|
10
|
+
input(type: :checkbox, value: "1", **attributes)
|
11
|
+
end
|
12
|
+
|
13
|
+
def field_attributes
|
14
|
+
{ id: dom.id, name: dom.name, checked: field.value }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Superform
|
2
|
+
module Rails
|
3
|
+
module Components
|
4
|
+
class Input < Field
|
5
|
+
def view_template(&)
|
6
|
+
input(**attributes)
|
7
|
+
end
|
8
|
+
|
9
|
+
def field_attributes
|
10
|
+
{
|
11
|
+
id: dom.id,
|
12
|
+
name: dom.name,
|
13
|
+
type: type,
|
14
|
+
value: value
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
def has_client_provided_value?
|
19
|
+
case type.to_s
|
20
|
+
when "file", "image"
|
21
|
+
true
|
22
|
+
else
|
23
|
+
false
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def value
|
28
|
+
dom.value unless has_client_provided_value?
|
29
|
+
end
|
30
|
+
|
31
|
+
def type
|
32
|
+
@type ||= ActiveSupport::StringInquirer.new(attribute_type || value_type)
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
def value_type
|
37
|
+
case field.value
|
38
|
+
when URI
|
39
|
+
"url"
|
40
|
+
when Integer, Float
|
41
|
+
"number"
|
42
|
+
when Date, DateTime
|
43
|
+
"date"
|
44
|
+
when Time
|
45
|
+
"time"
|
46
|
+
else
|
47
|
+
"text"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def attribute_type
|
52
|
+
if type = @attributes[:type] || @attributes["type"]
|
53
|
+
type.to_s
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Superform
|
2
|
+
module Rails
|
3
|
+
module Components
|
4
|
+
class Label < Base
|
5
|
+
def view_template(&content)
|
6
|
+
content ||= Proc.new { label_text }
|
7
|
+
label(**attributes, &content)
|
8
|
+
end
|
9
|
+
|
10
|
+
def field_attributes
|
11
|
+
{ for: dom.id }
|
12
|
+
end
|
13
|
+
|
14
|
+
def label_text
|
15
|
+
field.key.to_s.titleize
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Superform
|
2
|
+
module Rails
|
3
|
+
module Components
|
4
|
+
class Select < Field
|
5
|
+
def initialize(*, collection: [], **, &)
|
6
|
+
super(*, **, &)
|
7
|
+
@collection = collection
|
8
|
+
end
|
9
|
+
|
10
|
+
def view_template(&options)
|
11
|
+
if block_given?
|
12
|
+
select(**attributes, &options)
|
13
|
+
else
|
14
|
+
select(**attributes) { options(*@collection) }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def options(*collection)
|
19
|
+
map_options(collection).each do |key, value|
|
20
|
+
option(selected: field.value == key, value: key) { value }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def blank_option(&)
|
25
|
+
option(selected: field.value.nil?, &)
|
26
|
+
end
|
27
|
+
|
28
|
+
def true_option(&)
|
29
|
+
option(selected: field.value == true, value: true.to_s, &)
|
30
|
+
end
|
31
|
+
|
32
|
+
def false_option(&)
|
33
|
+
option(selected: field.value == false, value: false.to_s, &)
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
def map_options(collection)
|
38
|
+
OptionMapper.new(collection)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,240 @@
|
|
1
|
+
module Superform
|
2
|
+
module Rails
|
3
|
+
# A Phlex::HTML view module that accepts a model and sets a `Superform::Namespace`
|
4
|
+
# with the `Object#model_name` as the key and maps the object to form fields
|
5
|
+
# and namespaces.
|
6
|
+
#
|
7
|
+
# The `Form::Field` is a class that's meant to be extended so you can customize the `Form` inputs
|
8
|
+
# to your applications needs. Defaults for the `input`, `button`, `label`, and `textarea` tags
|
9
|
+
# are provided.
|
10
|
+
#
|
11
|
+
# The `Form` component also handles Rails authenticity tokens via the `authenticity_toklen_field`
|
12
|
+
# method and the HTTP verb via the `_method_field`.
|
13
|
+
class Form < Component
|
14
|
+
include Phlex::Rails::Helpers::FormAuthenticityToken
|
15
|
+
include Phlex::Rails::Helpers::URLFor
|
16
|
+
|
17
|
+
attr_accessor :model
|
18
|
+
|
19
|
+
delegate \
|
20
|
+
:Field,
|
21
|
+
:field,
|
22
|
+
:collection,
|
23
|
+
:namespace,
|
24
|
+
:assign,
|
25
|
+
:serialize,
|
26
|
+
to: :@namespace
|
27
|
+
|
28
|
+
# The Field class is designed to be extended to create custom forms. To override,
|
29
|
+
# in your subclass you may have something like this:
|
30
|
+
#
|
31
|
+
# ```ruby
|
32
|
+
# class MyForm < Superform::Rails::Form
|
33
|
+
# class MyLabel < Superform::Rails::Components::Label
|
34
|
+
# def view_template(&content)
|
35
|
+
# label(form: @field.dom.name, class: "text-bold", &content)
|
36
|
+
# end
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# class Field < Field
|
40
|
+
# def label(**attributes)
|
41
|
+
# MyLabel.new(self, **attributes)
|
42
|
+
# end
|
43
|
+
# end
|
44
|
+
# end
|
45
|
+
# ```
|
46
|
+
#
|
47
|
+
# Now all calls to `label` will have the `text-bold` class applied to it.
|
48
|
+
class Field < Superform::Field
|
49
|
+
def button(**attributes)
|
50
|
+
Components::Button.new(self, attributes:)
|
51
|
+
end
|
52
|
+
|
53
|
+
def input(**attributes)
|
54
|
+
Components::Input.new(self, attributes:)
|
55
|
+
end
|
56
|
+
|
57
|
+
def text(*, **, &)
|
58
|
+
input(*, **, type: :text, &)
|
59
|
+
end
|
60
|
+
|
61
|
+
def checkbox(**attributes)
|
62
|
+
Components::Checkbox.new(self, attributes:)
|
63
|
+
end
|
64
|
+
|
65
|
+
def label(**attributes, &)
|
66
|
+
Components::Label.new(self, attributes:, &)
|
67
|
+
end
|
68
|
+
|
69
|
+
def textarea(**attributes)
|
70
|
+
Components::Textarea.new(self, attributes:)
|
71
|
+
end
|
72
|
+
|
73
|
+
def select(*collection, **attributes, &)
|
74
|
+
Components::Select.new(self, attributes:, collection:, &)
|
75
|
+
end
|
76
|
+
|
77
|
+
# HTML5 input type convenience methods - clean API without _field suffix
|
78
|
+
# Examples:
|
79
|
+
# field(:email).email(class: "form-input")
|
80
|
+
# field(:age).number(min: 18, max: 99)
|
81
|
+
# field(:birthday).date
|
82
|
+
# field(:secret).hidden(value: "token123")
|
83
|
+
# field(:gender).radio("male", id: "user_gender_male")
|
84
|
+
def hidden(*, **, &)
|
85
|
+
input(*, **, type: :hidden, &)
|
86
|
+
end
|
87
|
+
|
88
|
+
def password(*, **, &)
|
89
|
+
input(*, **, type: :password, &)
|
90
|
+
end
|
91
|
+
|
92
|
+
def email(*, **, &)
|
93
|
+
input(*, **, type: :email, &)
|
94
|
+
end
|
95
|
+
|
96
|
+
def url(*, **, &)
|
97
|
+
input(*, **, type: :url, &)
|
98
|
+
end
|
99
|
+
|
100
|
+
def tel(*, **, &)
|
101
|
+
input(*, **, type: :tel, &)
|
102
|
+
end
|
103
|
+
alias_method :phone, :tel
|
104
|
+
|
105
|
+
def number(*, **, &)
|
106
|
+
input(*, **, type: :number, &)
|
107
|
+
end
|
108
|
+
|
109
|
+
def range(*, **, &)
|
110
|
+
input(*, **, type: :range, &)
|
111
|
+
end
|
112
|
+
|
113
|
+
def date(*, **, &)
|
114
|
+
input(*, **, type: :date, &)
|
115
|
+
end
|
116
|
+
|
117
|
+
def time(*, **, &)
|
118
|
+
input(*, **, type: :time, &)
|
119
|
+
end
|
120
|
+
|
121
|
+
def datetime(*, **, &)
|
122
|
+
input(*, **, type: :"datetime-local", &)
|
123
|
+
end
|
124
|
+
|
125
|
+
def month(*, **, &)
|
126
|
+
input(*, **, type: :month, &)
|
127
|
+
end
|
128
|
+
|
129
|
+
def week(*, **, &)
|
130
|
+
input(*, **, type: :week, &)
|
131
|
+
end
|
132
|
+
|
133
|
+
def color(*, **, &)
|
134
|
+
input(*, **, type: :color, &)
|
135
|
+
end
|
136
|
+
|
137
|
+
def search(*, **, &)
|
138
|
+
input(*, **, type: :search, &)
|
139
|
+
end
|
140
|
+
|
141
|
+
def file(*, **, &)
|
142
|
+
input(*, **, type: :file, &)
|
143
|
+
end
|
144
|
+
|
145
|
+
def radio(value, *, **, &)
|
146
|
+
input(*, **, type: :radio, value: value, &)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Rails compatibility aliases
|
150
|
+
alias_method :check_box, :checkbox
|
151
|
+
alias_method :text_area, :textarea
|
152
|
+
|
153
|
+
def title
|
154
|
+
key.to_s.titleize
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def build_field(...)
|
159
|
+
self.class::Field.new(...)
|
160
|
+
end
|
161
|
+
|
162
|
+
def initialize(model, action: nil, method: nil, **attributes)
|
163
|
+
@model = model
|
164
|
+
@action = action
|
165
|
+
@method = method
|
166
|
+
@attributes = attributes
|
167
|
+
@namespace = Namespace.root(key, object: model, form: self)
|
168
|
+
end
|
169
|
+
|
170
|
+
def around_template(&)
|
171
|
+
form_tag do
|
172
|
+
authenticity_token_field
|
173
|
+
_method_field
|
174
|
+
super
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def form_tag(&)
|
179
|
+
form action: form_action, method: form_method, **@attributes, &
|
180
|
+
end
|
181
|
+
|
182
|
+
def view_template(&block)
|
183
|
+
yield self if block_given?
|
184
|
+
end
|
185
|
+
|
186
|
+
def submit(value = submit_value, **attributes)
|
187
|
+
input **attributes.merge(
|
188
|
+
name: "commit",
|
189
|
+
type: "submit",
|
190
|
+
value: value
|
191
|
+
)
|
192
|
+
end
|
193
|
+
|
194
|
+
def key
|
195
|
+
@model.model_name.param_key
|
196
|
+
end
|
197
|
+
|
198
|
+
protected
|
199
|
+
def authenticity_token_field
|
200
|
+
input(
|
201
|
+
name: "authenticity_token",
|
202
|
+
type: "hidden",
|
203
|
+
value: form_authenticity_token
|
204
|
+
)
|
205
|
+
end
|
206
|
+
|
207
|
+
def _method_field
|
208
|
+
input(
|
209
|
+
name: "_method",
|
210
|
+
type: "hidden",
|
211
|
+
value: _method_field_value
|
212
|
+
)
|
213
|
+
end
|
214
|
+
|
215
|
+
def _method_field_value
|
216
|
+
@method || resource_method_field_value
|
217
|
+
end
|
218
|
+
|
219
|
+
def resource_method_field_value
|
220
|
+
@model.persisted? ? "patch" : "post"
|
221
|
+
end
|
222
|
+
|
223
|
+
def submit_value
|
224
|
+
"#{resource_action.to_s.capitalize} #{@model.model_name}"
|
225
|
+
end
|
226
|
+
|
227
|
+
def resource_action
|
228
|
+
@model.persisted? ? :update : :create
|
229
|
+
end
|
230
|
+
|
231
|
+
def form_action
|
232
|
+
@action ||= url_for(action: resource_action)
|
233
|
+
end
|
234
|
+
|
235
|
+
def form_method
|
236
|
+
@method.to_s.downcase == "get" ? "get" : "post"
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|