hyperactiveform 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9bf33b760b731a44e34fd302e0d0d4049f9dd04cfeaeff043a32992f6a8c01d7
4
+ data.tar.gz: 556cb734a8ee3eabf099240c26a709b19214b30e65d0c731e233e58b2984565f
5
+ SHA512:
6
+ metadata.gz: 129e1e5a36f424767e7135339ade191302fd7b03cee66695abb9eaa4e274fb521a811d3bbd6b8c3e1fb8934e8a66c94ce75a7e99712ca3fe6bd40ee15949d670
7
+ data.tar.gz: 381d63d878a72e386c4b0156be4836d54095b503e6640ebacbd2a4b441d924422b11bc583affc291a493c0514b0fcd57f46f9fdea917a7b9403fd36c1847f5fb
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2024-11-30
4
+
5
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Adrien Siami
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,213 @@
1
+ # ✨ HyperActiveForm ✨
2
+
3
+ HyperActiveForm is a simple form object implementation for Rails.
4
+
5
+ Form objects are objects that encapsulate form logic and validations, they allow to extract the business logic out of the controller and models into specialized objects.
6
+
7
+ HyperActiveForm's form objects mimic the ActiveModel API, so they work out of the box with Rails' form helpers, and allow you to use the ActiveModel validations you already know.
8
+
9
+ This allows you to only keep strictly necessary validations in the model, and have business logic validations in the form object. This is especially useful when you want different validations to be applied depending on the context.
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ ```ruby
16
+ gem 'hyperactiveform'
17
+ ```
18
+
19
+ And then execute:
20
+
21
+ $ bundle install
22
+
23
+ Run the install generator:
24
+
25
+ $ rails generate hyperactiveform:install
26
+
27
+ this will create an `ApplicationForm` class in your app/forms directory. You can use it as a base class for your form objects.
28
+
29
+ ## Usage
30
+
31
+ Here is an example of an `HyperActiveForm` form object:
32
+
33
+ ```ruby
34
+ class ProfileForm < ApplicationForm
35
+ # proxy_for is used to delegate the model name to the class, and some methods to the object
36
+ # this helps use `form_with` in views without having to specify the url
37
+ proxy_for User, :@user
38
+
39
+ # Define the form fields, using ActiveModel::Attributes
40
+ attribute :first_name
41
+ attribute :last_name
42
+ attribute :birth_date, :date
43
+
44
+ # Define the validations, using ActiveModel::Validations
45
+ validates :first_name, presence: true
46
+ validates :last_name, presence: true
47
+ validates :birth_date, presence: true
48
+
49
+ # Pre-fill the form if needed
50
+ def setup(user)
51
+ @user = user
52
+ self.first_name = user.first_name
53
+ self.last_name = user.last_name
54
+ self.birth_date = user.birth_date
55
+ end
56
+
57
+ # Perform the form logic
58
+ def perform
59
+ @user.update!(
60
+ first_name: first_name,
61
+ last_name: last_name,
62
+ birth_date: birth_date
63
+ )
64
+ end
65
+ end
66
+ ```
67
+
68
+ The controller would look like this:
69
+
70
+ ```ruby
71
+ class UsersController < ApplicationController
72
+
73
+ def edit
74
+ @form = ProfileForm.new(user: current_user)
75
+ end
76
+
77
+ def update
78
+ @form = ProfileForm.new(user: current_user)
79
+ if @form.submit(params[:user])
80
+ redirect_to root_path, notice: "Profile updated"
81
+ else
82
+ render :edit, status: :unprocessable_entity
83
+ end
84
+ end
85
+ ```
86
+
87
+ And the view would look like this:
88
+
89
+ ```erb
90
+ <%= form_with(model: @form) do |f| %>
91
+ <%= f.text_field :first_name %>
92
+ <%= f.text_field :last_name %>
93
+ <%= f.date_field :birth_date %>
94
+
95
+ <%= f.submit %>
96
+ <% end %>
97
+ ```
98
+
99
+ ### Understanding `proxy_for`
100
+
101
+ `HyperActiveForm` mimics a model object, you can use `proxy_for` to tell it which class and object to delegate to.
102
+
103
+ When using `form_for` or `form_with`, Rails will choose the URL and method based on the object, according to the persisted state of the object and its model name.
104
+
105
+ The first argument of `proxy_for` is the class of the object, and the second argument is the name of the instance variable that holds the object.
106
+
107
+ ```ruby
108
+ class ProfileForm < ApplicationForm
109
+ proxy_for User, :@user # Will delegate to @user
110
+ end
111
+ ```
112
+
113
+ If you pass an url and method yourself, you don't need to use `proxy_for`.
114
+
115
+ ### Understanding `setup`
116
+
117
+ `setup` is called just after the form is initialized, and is used to pre-fill the form with data from the object.
118
+
119
+ `setup` will receive the same arguments as the initializer, so you can use it to pass any data you need to the form.
120
+
121
+ ```ruby
122
+ class ProfileForm < ApplicationForm
123
+ def setup(user)
124
+ @user = user
125
+ self.first_name = user.first_name
126
+ self.last_name = user.last_name
127
+ self.birth_date = user.birth_date
128
+ end
129
+ end
130
+ ```
131
+
132
+ ### Understanding `perform`
133
+
134
+ When using `submit` or `submit!`, `HyperActiveForm` will first assign the form attributes to the object, then perform the validations, then call `perform` on the object if the form is valid.
135
+
136
+ The `perform` method is where you should do the actual form logic, like updating the object or creating a new one.
137
+
138
+ If the return value of `perform` is not truthy, `HyperActiveForm` will consider the form encountered an error and `submit` will return `false`, or `submit!` will raise a `HyperActiveForm::FormDidNotSubmitError`.
139
+
140
+ At any point during the form processing, you can raise `HyperActiveForm::CancelForm` to cancel the form submission, this is the same as returning `false`.
141
+
142
+ ## Understanding add_errors_from
143
+
144
+ `HyperActiveForm` provides a method to add errors from a model and apply them fo the form.
145
+
146
+ This is useful when the underlying model has validations that are not set up in the form object, and you want them to be applied to the form.
147
+
148
+ ```ruby
149
+ class User < ApplicationRecord
150
+ validates :first_name, presence: true
151
+ end
152
+
153
+ class ProfileForm < ApplicationForm
154
+ proxy_for User, :@user
155
+ attribute :first_name
156
+
157
+ def setup(user)
158
+ @user = user
159
+ self.first_name = user.first_name
160
+ end
161
+
162
+ def perform
163
+ @user.update!(first_name: first_name) || add_errors_from(@user)
164
+ end
165
+ end
166
+ ```
167
+
168
+ ### Not all forms map to a single model
169
+
170
+ The power of `HyperActiveForm` is that you can use it to create forms that don't map to a single model.
171
+
172
+ Some forms can be used to create several models at once. Doing so without form objects can be tedious especially with nested attributes.
173
+
174
+ Some forms dont map to any model at all, like a simple contact form that only sends an email and saves nothing in the database, or a sign in form that would only validate the credentials and return the instance of the connected user.
175
+
176
+ One great example of such forms are search forms. You can use a form object to encapsulate the search logic :
177
+
178
+ ```ruby
179
+ class UserSearchForm < ApplicationForm
180
+ attribute :name
181
+ attribute :email
182
+ attribute :min_age, :integer
183
+
184
+ attr_reader :results # So the controller can access the results
185
+
186
+ def perform
187
+ @results = User.all
188
+
189
+ if name.present?
190
+ @results = @results.where(name: name)
191
+ end
192
+ if email.present?
193
+ @results = @results.where(email: email)
194
+ end
195
+ if age.present?
196
+ @results = @results.where("age >= ?", age)
197
+ end
198
+
199
+ true
200
+ end
201
+ end
202
+ ```
203
+
204
+ And in the controller:
205
+
206
+ ```ruby
207
+ class UsersController < ApplicationController
208
+ def index
209
+ @form = UserSearchForm.new
210
+ @form.submit!(params[:user])
211
+ @users = @form.results
212
+ end
213
+ end
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec :path => '..'
4
+
5
+ gem 'rails', '~> 7.1'
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec :path => '..'
4
+
5
+ gem 'rails', '~> 7.2'
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec :path => '..'
4
+
5
+ gem 'rails', '~> 8.0'
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+ require "action_controller"
5
+
6
+ module HyperActiveForm
7
+ class Base
8
+ include ActiveModel::Model
9
+ include ActiveModel::Attributes
10
+ include ActiveModel::Validations
11
+
12
+ def self.proxy_for(klass, object)
13
+ delegate :new_record?, :persisted?, :id, to: object
14
+ singleton_class.delegate :model_name, to: klass
15
+ end
16
+
17
+ def initialize(*, **)
18
+ super()
19
+ setup(*, **)
20
+ end
21
+
22
+ def setup; end
23
+
24
+ def perform
25
+ raise NotImplementedError
26
+ end
27
+
28
+ def assign_form_attributes(params)
29
+ params = ActionController::Parameters.new(params) unless params.is_a?(ActionController::Parameters)
30
+ attribute_names.each do |attribute|
31
+ public_send(:"#{attribute}=", params&.dig(attribute)) if params&.key?(attribute)
32
+ end
33
+ end
34
+
35
+ def submit(params)
36
+ assign_form_attributes(params)
37
+ !!(valid? && perform)
38
+ rescue HyperActiveForm::CancelFormSubmit
39
+ false
40
+ end
41
+
42
+ def submit!(params)
43
+ submit(params) || raise(HyperActiveForm::FormDidNotSubmitError)
44
+ end
45
+
46
+ def add_errors_from(model)
47
+ model.errors.each do |error|
48
+ Array.wrap(error.message).each { |e| errors.add(error.attribute, e) }
49
+ end
50
+
51
+ false
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,14 @@
1
+ require "rails/generators"
2
+
3
+ module HyperActiveForm
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ def create_application_form
7
+ create_file "app/forms/application_form.rb", <<~RUBY
8
+ class ApplicationForm < HyperActiveForm::Base
9
+ end
10
+ RUBY
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HyperActiveForm
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HyperActiveForm
4
+ class CancelFormSubmit < StandardError; end
5
+ class FormDidNotSubmitError < StandardError; end
6
+ end
7
+
8
+ require_relative "hyper_active_form/version"
9
+ require_relative "hyper_active_form/base"
10
+ require_relative "hyper_active_form/generators"
@@ -0,0 +1 @@
1
+ require_relative "hyper_active_form"
@@ -0,0 +1,4 @@
1
+ module Hyperactiveform
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hyperactiveform
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Adrien Siami
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-11-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '7.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '7.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.13'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.13'
41
+ description: Encapsulate form logic and validations in a simple object
42
+ email:
43
+ - adrien@siami.fr
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".rspec"
49
+ - CHANGELOG.md
50
+ - LICENSE.txt
51
+ - README.md
52
+ - Rakefile
53
+ - gemfiles/Gemfile.rails-7.1.x
54
+ - gemfiles/Gemfile.rails-7.2.x
55
+ - gemfiles/Gemfile.rails-8.x
56
+ - lib/hyper_active_form.rb
57
+ - lib/hyper_active_form/base.rb
58
+ - lib/hyper_active_form/generators.rb
59
+ - lib/hyper_active_form/version.rb
60
+ - lib/hyperactiveform.rb
61
+ - sig/hyperactiveform.rbs
62
+ homepage: https://github.com/Intrepidd/hyperactiveform
63
+ licenses:
64
+ - MIT
65
+ metadata:
66
+ homepage_uri: https://github.com/Intrepidd/hyperactiveform
67
+ source_code_uri: https://github.com/Intrepidd/hyperactiveform
68
+ changelog_uri: https://github.com/Intrepidd/hyperactiveform/blob/main/CHANGELOG.md
69
+ post_install_message:
70
+ rdoc_options: []
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '3.2'
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ requirements: []
84
+ rubygems_version: 3.5.10
85
+ signing_key:
86
+ specification_version: 4
87
+ summary: Simple form objects for Rails
88
+ test_files: []