superform 0.5.1 → 0.6.1

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.
@@ -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,108 @@ 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
+ # A helper, borrowed from Phlex, that makes it easy to "grab" values
46
+ # passed into a method that are reserved keywords. For example, this
47
+ # would throw a syntax error because `class` and `end` are reserved:
48
+ #
49
+ # def foo(end:, class:)
50
+ # puts class
51
+ # puts end
52
+ # end
53
+ #
54
+ # So you "grab" them like this:
55
+ # def foo(end:, class:)
56
+ # puts grab(end:)
57
+ # puts grab(class:)
58
+ # end
59
+ private def grab(**bindings)
60
+ if bindings.size > 1
61
+ bindings.values
62
+ else
63
+ bindings.values.first
64
+ end
65
+ end
66
+
67
+ # High-performance Kit proxy that wraps field methods with form.render calls.
68
+ # Uses Ruby class hooks to define methods at the class level for maximum speed:
69
+ # - Methods are defined once per Field class, not per Kit instance
70
+ # - True Ruby methods with full VM optimization (no method_missing overhead)
71
+ # - ~125x faster Kit instantiation compared to instance-level dynamic methods
72
+ # - Each Field subclass gets its own isolated Kit class with true isolation:
73
+ # * Methods are copied at subclass creation time, not inherited dynamically
74
+ # * Adding methods to a parent Field class won't affect existing subclass Kits
75
+ # * Perfect for library design where you don't want parent changes affecting subclasses
76
+ class Kit
77
+ def initialize(field:, form:)
78
+ @field = field
79
+ @form = form
80
+ end
81
+ end
82
+
83
+ def self.inherited(subclass)
84
+ super
85
+ # Create a new Kit class for each Field subclass with true isolation
86
+ # Copy methods from parent Field classes at creation time, not through inheritance
87
+ subclass.const_set(:Kit, Class.new(Field::Kit))
88
+
89
+ # Copy all existing methods from the inheritance chain
90
+ field_class = self
91
+ while field_class != Field
92
+ copy_field_methods_to_kit(field_class, subclass::Kit)
93
+ field_class = field_class.superclass
94
+ end
95
+ end
96
+
97
+ def self.method_added(method_name)
98
+ super
99
+ # Skip if this is the base Field class or if we don't have a Kit class yet
100
+ return if self == Field
101
+ return unless const_defined?(:Kit, false)
102
+
103
+ # Only add method to THIS class's Kit, not subclasses (isolation)
104
+ add_method_to_kit(method_name, self::Kit)
105
+ end
106
+
107
+ def kit(form)
108
+ self.class::Kit.new(field: self, form: form)
109
+ end
110
+
111
+ private
112
+
113
+ def self.copy_field_methods_to_kit(field_class, kit_class)
114
+ base_methods = (Object.instance_methods + Node.instance_methods +
115
+ [:dom, :value, :serialize, :assign, :collection, :field, :kit]).to_set
116
+
117
+ field_class.instance_methods(false).each do |method_name|
118
+ next if method_name.to_s.end_with?('=')
119
+ next if base_methods.include?(method_name)
120
+ next if kit_class.method_defined?(method_name)
121
+
122
+ kit_class.define_method(method_name) do |*args, **kwargs, &block|
123
+ result = @field.send(method_name, *args, **kwargs, &block)
124
+ @form.render result
125
+ end
126
+ end
127
+ end
128
+
129
+ def self.add_method_to_kit(method_name, kit_class)
130
+ return if method_name.to_s.end_with?('=')
131
+
132
+ base_methods = (Object.instance_methods + Node.instance_methods +
133
+ [:dom, :value, :serialize, :assign, :collection, :field, :kit]).to_set
134
+ return if base_methods.include?(method_name)
135
+ return if kit_class.method_defined?(method_name)
136
+
137
+ kit_class.define_method(method_name) do |*args, **kwargs, &block|
138
+ result = @field.send(method_name, *args, **kwargs, &block)
139
+ @form.render result
140
+ end
141
+ end
38
142
  end
39
143
  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
@@ -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, field_class: Field)
15
- super(key, parent: parent)
14
+ def initialize(key, parent:, object: nil, form: Superform::Form.new)
15
+ super(key, parent:)
16
16
  @object = object
17
- @field_class = field_class
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, &block)
37
- create_child(key, self.class, object: object_for(key: key), &block)
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
- create_child(key, @field_class, object: object).tap do |field|
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
- create_child(key, NamespaceCollection, &)
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
- # Checks if the child exists. If it does then it returns that. If it doesn't, it will
119
- # build the child.
120
- def create_child(key, child_class, **kwargs, &block)
121
- @children.fetch(key) { @children[key] = child_class.new(key, parent: self, **kwargs, &block) }
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
- def initialize(key, parent:, &template)
9
- super(key, parent: parent)
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: 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,11 @@
1
+ module Superform
2
+ module Rails
3
+ module Components
4
+ class Field < Base
5
+ def field_attributes
6
+ { id: dom.id, name: dom.name }
7
+ end
8
+ end
9
+ end
10
+ end
11
+ 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,12 @@
1
+ module Superform
2
+ module Rails
3
+ module Components
4
+ class Textarea < Field
5
+ def view_template(&content)
6
+ content ||= Proc.new { dom.value }
7
+ textarea(**attributes, &content)
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,137 @@
1
+ module Superform
2
+ module Rails
3
+ # The Field class is designed to be extended to create custom forms. To override,
4
+ # in your subclass you may have something like this:
5
+ #
6
+ # ```ruby
7
+ # class MyForm < Superform::Rails::Form
8
+ # class MyLabel < Superform::Rails::Components::Label
9
+ # def view_template(&content)
10
+ # label(form: @field.dom.name, class: "text-bold", &content)
11
+ # end
12
+ # end
13
+ #
14
+ # class Field < Field
15
+ # def label(**, &)
16
+ # MyLabel.new(self, **, &)
17
+ # end
18
+ #
19
+ # def input(class: nil, **)
20
+ # super(class: ["input input-outline", grab(class:)])
21
+ # end
22
+ # end
23
+ # end
24
+ # ```
25
+ #
26
+ # Now all calls to `label` will have the `text-bold` class applied to it.
27
+ class Field < Superform::Field
28
+ def button(**attributes)
29
+ Components::Button.new(field, attributes:)
30
+ end
31
+
32
+ def input(**attributes)
33
+ Components::Input.new(field, attributes:)
34
+ end
35
+
36
+ def checkbox(**attributes)
37
+ Components::Checkbox.new(field, attributes:)
38
+ end
39
+
40
+ def label(**attributes, &)
41
+ Components::Label.new(field, attributes:, &)
42
+ end
43
+
44
+ def textarea(**attributes)
45
+ Components::Textarea.new(field, attributes:)
46
+ end
47
+
48
+ def select(*collection, **attributes, &)
49
+ Components::Select.new(field, attributes:, collection:, &)
50
+ end
51
+
52
+ # HTML5 input type convenience methods - clean API without _field suffix
53
+ # Examples:
54
+ # field(:email).email(class: "form-input")
55
+ # field(:age).number(min: 18, max: 99)
56
+ # field(:birthday).date
57
+ # field(:secret).hidden(value: "token123")
58
+ # field(:gender).radio("male", id: "user_gender_male")
59
+ def text(*, **, &)
60
+ input(*, **, type: :text, &)
61
+ end
62
+
63
+ def hidden(*, **, &)
64
+ input(*, **, type: :hidden, &)
65
+ end
66
+
67
+ def password(*, **, &)
68
+ input(*, **, type: :password, &)
69
+ end
70
+
71
+ def email(*, **, &)
72
+ input(*, **, type: :email, &)
73
+ end
74
+
75
+ def url(*, **, &)
76
+ input(*, **, type: :url, &)
77
+ end
78
+
79
+ def tel(*, **, &)
80
+ input(*, **, type: :tel, &)
81
+ end
82
+ alias_method :phone, :tel
83
+
84
+ def number(*, **, &)
85
+ input(*, **, type: :number, &)
86
+ end
87
+
88
+ def range(*, **, &)
89
+ input(*, **, type: :range, &)
90
+ end
91
+
92
+ def date(*, **, &)
93
+ input(*, **, type: :date, &)
94
+ end
95
+
96
+ def time(*, **, &)
97
+ input(*, **, type: :time, &)
98
+ end
99
+
100
+ def datetime(*, **, &)
101
+ input(*, **, type: :"datetime-local", &)
102
+ end
103
+
104
+ def month(*, **, &)
105
+ input(*, **, type: :month, &)
106
+ end
107
+
108
+ def week(*, **, &)
109
+ input(*, **, type: :week, &)
110
+ end
111
+
112
+ def color(*, **, &)
113
+ input(*, **, type: :color, &)
114
+ end
115
+
116
+ def search(*, **, &)
117
+ input(*, **, type: :search, &)
118
+ end
119
+
120
+ def file(*, **, &)
121
+ input(*, **, type: :file, &)
122
+ end
123
+
124
+ def radio(value, *, **, &)
125
+ input(*, **, type: :radio, value: value, &)
126
+ end
127
+
128
+ # Rails compatibility aliases
129
+ alias_method :check_box, :checkbox
130
+ alias_method :text_area, :textarea
131
+
132
+ def title
133
+ key.to_s.titleize
134
+ end
135
+ end
136
+ end
137
+ end