schoolgirl_uniform 0.2.0 → 1.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 58dbc55cc5843ea0980f2438a97a6cfbb85bd6fc37e503bdd507f95cd2a00b75
4
- data.tar.gz: 7105a08c67034e2b76dc48a6e8204c752bdd23dc6fa3c8e0ec25c85b897a19d8
3
+ metadata.gz: 51f5035a1c610591e3097740c7528de13b0d07ed3c453b3003a4756827686e1e
4
+ data.tar.gz: 6c1c4de2b74e41abf5559a34368c212f79a12ca19b58c6beaa4c6c152fd4e1d7
5
5
  SHA512:
6
- metadata.gz: 7818389aa702ad6a083dd10ae352d3b51baef8ed93525edd80a56baae6461883b86e5cfe2a24c8013ee698e5d21b9cc4a681347dcf688ba63dafe27b241575c6
7
- data.tar.gz: 72ff48308ea6985940542ae978834ec5cebb7a7850babceb93f647e34436dab8beed50ff2e824026f625a483c191c8aa930fa4480102e1d78ca761d698958203
6
+ metadata.gz: 65dfd5a3c743a4a7a95761fb5eb31b6eb96ad3161f52d648aa5c14b2a66cff6b34ed9a94d92c27ff80f7bc707b7ee04ff5af8cef315d1b879b6fc0afd83137ef
7
+ data.tar.gz: e6c0eb8ece6a9c54649c3f85f2663c539d4aac3692db42d3e8837584e0ae02ae4a7743d755f3d9907b02f82dabd75561110ad25cc0d4d18be2b1a94c80fd849a
data/README.md CHANGED
@@ -1,5 +1,6 @@
1
- <br>
2
- <br>
1
+ <p align="right">
2
+ <a href="https://rubygems.org/gems/schoolgirl_uniform"><img align="right" src="https://user-images.githubusercontent.com/2478436/51829691-c55cc000-22f6-11e9-99a5-42f88a8f2a55.png" width="56" height="56" /></a>
3
+ </p>
3
4
  <p align="center">
4
5
  <a href="#">
5
6
  <img align="center" width="75%" src="https://user-images.githubusercontent.com/2478436/210048098-9d09b442-f057-42e1-b77b-94277928e452.png"/>
@@ -16,10 +17,11 @@
16
17
  </p>
17
18
 
18
19
  :feet: Multistep form concept for Rails projects. Allows to create complex forms for a few models simultaneously. Supports selectable per step validations without data persistence into db.
20
+ > Currently uses session to store data before actual save. If your sessions are stored in cookies then it has a 4 KB limit.
19
21
 
20
22
  <br>
21
23
 
22
- ## Installation
24
+ ### Installation
23
25
 
24
26
  To start using it just add this line to your application's Gemfile:
25
27
 
@@ -36,8 +38,9 @@ $ rails generate schoolgirl_uniform:install CatgirlsSurvey
36
38
 
37
39
  > You can also use snake case, so `catgirls_survey` would be identical to `CatgirlsSurvey` and will generate the same output during scaffolding.
38
40
 
41
+ <br>
39
42
 
40
- ## Usage and Config
43
+ ### Usage and Config
41
44
 
42
45
  To achieve working multistep form you need to configure FVC:
43
46
 
@@ -48,41 +51,92 @@ To achieve working multistep form you need to configure FVC:
48
51
  - :school_satchel: [**Controller**](#school_satchel-controller)
49
52
  <hr>
50
53
 
54
+ <p align="center">
55
+ <a href="#">
56
+ <img align="center" width="75%" src="https://user-images.githubusercontent.com/2478436/210439098-6230592e-4e94-4236-88e6-70d162d5369a.png"/>
57
+ </a>
58
+ </p>
59
+
51
60
  ### :womans_clothes: Form
52
61
  e.g. CatgirlsSurveyForm - app/forms/catgirls_survey_form.rb
53
62
 
54
- 1. Declare the steps:
63
+ <br>
64
+
65
+ #### 1. Declare the steps and details if needed in your form:
66
+
55
67
  ```ruby
56
- def self.steps
57
- %w[first second third]
68
+ steps %w[first second third]
69
+
70
+ def self.steps_details
71
+ {
72
+ first: 'Credentials',
73
+ second: 'Personal Details',
74
+ third: 'Contact Information'
75
+ }
58
76
  end
59
77
  ```
60
- 2. Define form fields
78
+ :blue_book: How to [customize steps titles and descriptions](https://github.com/vergilet/schoolgirl_uniform/wiki/1.-Decorate-steps)
79
+
80
+ <br>
81
+
82
+ #### 2. Define form fields:
83
+
61
84
  ```ruby
62
- attribute :username, String
85
+ attribute :username, :string
86
+ attribute :password, :string
87
+
88
+ attribute :date_of_birth, :date
89
+ attribute :gender, :string
90
+ attribute :favourite_color, :string
91
+ attribute :device_type, :string
92
+
93
+ attribute :email, :string
94
+ attribute :phone_number, :string
95
+ attribute :country, :string
96
+ attribute :city, :string
97
+ attribute :address_field_1, :string
98
+ attribute :address_field_2, :string
99
+ attribute :zip_code, :string
63
100
  ```
64
- 3. Define validation and select appropriate step for it
101
+ :blue_book: Different types and [how to add custom types such as `Array` and `hash` explained.](https://github.com/vergilet/schoolgirl_uniform/wiki/4.-Array-and-Hash-and-other-custom-types)
102
+
103
+ <br>
104
+
105
+ #### 3. Use block validations with step condition to group needed checks:
106
+
65
107
  ```ruby
66
- validates :username, presence: true, if: proc { on_step('second') }
108
+ with_options if: :first? do |step|
109
+ step.validates :username, presence: true, length: 3..10
110
+ step.validate :custom_username_validation
111
+ ...
112
+ end
113
+
114
+ with_options if: :second? do |step|
115
+ step.validates :date_of_birth, presence: true
116
+ step.validate :custom_date_of_birth_validation
117
+ ...
118
+ end
67
119
  ```
68
- 4. Inside `save!` method build your records, set them with form attributes and save them in transaction.
69
- Use `.save!(validate: false)` to skip native validations on model.
70
- In order to return the result set the `@identifier` with created records reference/references
71
-
72
- ( e.g. simple `1234` or complex `{user_id: 1234, personal_data_id: 5678}` )
120
+ <br>
121
+
122
+ #### 4. Inside `save!` method build your records, set them with form attributes and save.
123
+
73
124
  ```ruby
125
+
126
+ attr_reader :identifier
127
+
74
128
  def save!
75
- user = User.new(username: username)
76
- personal_data = user.build_personal_data(email: email)
77
-
78
- ActiveRecord::Base.transaction do
79
- user.save!(validate: false)
80
- personal_data.save!(validate: false)
81
- end
82
-
129
+ user.save!(validate: false)
130
+ personal_detail.save!(validate: false)
131
+ contact_info.save!(validate: false)
132
+
83
133
  @identifier = user.id
84
134
  end
85
135
  ```
136
+ :blue_book: Check more [complex examples and how to read them on the controller](https://github.com/vergilet/schoolgirl_uniform/wiki/5.-Returning-Complex-Results).
137
+
138
+ <br>
139
+
86
140
  ### :dress: View
87
141
  - Scaffolding will generate example structure of view files:
88
142
  - _show.html.erb_
@@ -97,10 +151,12 @@ To achieve working multistep form you need to configure FVC:
97
151
 
98
152
  :exclamation: Please notice that **_show_** and **_finish_** are action views, others are partials. \
99
153
  :art: Feel free to modify html and styles around the form.
154
+ <br>
100
155
 
101
156
  #### :infinity: Steps
102
157
 
103
- By default Scaffolding generates 3 steps, but you can modify, delete them or add new steps. Just make sure that steps are **__partials_** and match corresponded names inside **_Form_** (e.g. CatgirlsSurveyForm):
158
+ By default Scaffolding generates 3 steps, but you can modify, delete or add new steps. \
159
+ Just make sure that steps are **__partials_** and match corresponded names inside **_Form_** (e.g. CatgirlsSurveyForm):
104
160
 
105
161
  ```ruby
106
162
  # app/views/catgirls_survey/steps/_first.html.erb
@@ -111,30 +167,21 @@ By default Scaffolding generates 3 steps, but you can modify, delete them or add
111
167
  <%= form.label :password %>
112
168
  <%= form.text_field :password %>
113
169
  ```
114
-
170
+ <br>
115
171
 
116
172
  ### :school_satchel: Controller
117
- e.g. CatgirlsSurveyController - app/controllers/catgirls_survey_controller.rb
173
+ e.g. CatgirlsSurveyController - app/controllers/catgirls_survey_controller.rb
118
174
 
119
- 1. Make sure you have listed all form fields (used for permit params)
120
- ```ruby
121
- def form_attributes
122
- [:username, :password, :email, :phone]
123
- end
124
- ```
125
- 2. Fetch resource/resources from DB using identifier, which you set in `.save!`
175
+ Fetch resource(s) from DB using `identifier`, which you set in `.save!`
126
176
  ```ruby
127
177
  def finish
128
- @record = User.find_by(uuid: params[:identifier])
129
- ...
130
- # or if you have a few identifiers
131
- ...
132
- @record1 = Book.find_by(title: params[:identifier][:title])
133
- @record2 = Author.find_by(id: params[:identifier][:author_id])
178
+ @record = User.find_by(id: params[:identifier])
134
179
  end
135
180
  ```
136
-
137
-
181
+ :blue_book: Check more [complex examples and how to read them on the controller](https://github.com/vergilet/schoolgirl_uniform/wiki/5.-Returning-Complex-Results).
182
+
183
+ <br>
184
+
138
185
  ## Contributing
139
186
 
140
187
  Bug reports and pull requests are welcome on GitHub at https://github.com/vergilet/schoolgirl_uniform
@@ -2,7 +2,7 @@ module SchoolgirlUniform
2
2
  class BaseController < ActionController::Base
3
3
  before_action :reset_session, only: :show
4
4
  before_action :refresh_session, only: :current
5
- before_action :initialize_form, except: [:index]
5
+ before_action :initialize_form
6
6
  after_action :refresh_current_step, only: [:show, :current, :previous]
7
7
  helper_method :form_carrier
8
8
 
@@ -26,6 +26,10 @@ module SchoolgirlUniform
26
26
  def current
27
27
  if request.post?
28
28
  return render :show unless @form.valid?
29
+
30
+ @form.save_form! if @form.last_step?
31
+ return render :show if @form.errors.present?
32
+
29
33
  redirect_to redirect_options
30
34
  elsif request.get?
31
35
  return render :show if params[:step] == @form.current_step
@@ -44,6 +48,10 @@ module SchoolgirlUniform
44
48
 
45
49
  private
46
50
 
51
+ def form_attributes
52
+ initialize_form.attribute_names
53
+ end
54
+
47
55
  def paths
48
56
  {
49
57
  current: nil,
@@ -69,9 +77,8 @@ module SchoolgirlUniform
69
77
 
70
78
  def redirect_options
71
79
  if @form.last_step?
72
- @form.save!
73
80
  reset_session
74
- { action: :finish, identifier: @form.identifier }
81
+ { action: :finish, identifier: @form.identifier.to_param }
75
82
  else
76
83
  @form.next_step
77
84
  { action: :current, step: @form.current_step }
@@ -1,21 +1,59 @@
1
1
  module SchoolgirlUniform
2
2
  module Uniformable
3
+ module ClassMethods
4
+ def steps(*step_list)
5
+ @defined_steps = step_list.flatten.map(&:to_s)
6
+ attribute :step, :string, default: -> { @defined_steps.first }
7
+
8
+ @defined_steps.each do |step_name|
9
+ helper_method_name = "#{step_name}?"
10
+ unless method_defined?(helper_method_name)
11
+ define_method helper_method_name do
12
+ current_step == step_name
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ def defined_steps
19
+ @defined_steps || []
20
+ end
21
+
22
+ def steps_details
23
+ defined_steps.each_with_object({}) { |step, hash| hash[step] = step }
24
+ end
25
+ end
3
26
 
4
27
  def self.included(base)
28
+ base.extend(ClassMethods)
5
29
  base.include ActiveModel::Model
6
- base.include Virtus.model
30
+ base.include ActiveModel::Attributes
7
31
  end
8
32
 
9
- def initialize(options = {})
10
- initialize_attributes(options)
33
+ def save!
34
+ raise NotImplementedError, "#{self.class.name} must implement #save!"
11
35
  end
12
36
 
13
- def save!
14
- raise NotImplementedError
37
+ def save_form!
38
+ unless defined?(ActiveRecord::Base)
39
+ raise "ActiveRecord::Base not available for transaction"
40
+ end
41
+
42
+ ActiveRecord::Base.transaction do
43
+ save!
44
+ rescue => e
45
+ if e.respond_to?(:record) && e.record.respond_to?(:errors)
46
+ e.record.errors.each { |error| errors.add(error.attribute, error.message) }
47
+ else
48
+ errors.add(:base, e.message)
49
+ end
50
+ raise ActiveRecord::Rollback
51
+ end
52
+ errors.empty?
15
53
  end
16
54
 
17
55
  def current_step
18
- step || steps.first
56
+ step
19
57
  end
20
58
 
21
59
  def next_step
@@ -24,46 +62,43 @@ module SchoolgirlUniform
24
62
  end
25
63
 
26
64
  def previous_step
65
+ return if first_step?
27
66
  shift_step(-1)
28
67
  end
29
68
 
30
69
  def first_step?
31
- current_step == steps.first
70
+ current_step == self.class.defined_steps.first
32
71
  end
33
72
 
34
73
  def last_step?
35
- current_step == steps.last
74
+ current_step == self.class.defined_steps.last
36
75
  end
37
76
 
38
77
  def steps
39
- self.class.steps
78
+ self.class.defined_steps
40
79
  end
41
80
 
42
- def current_step_index
43
- steps.index(current_step)
81
+ def steps_details
82
+ self.class.steps_details
44
83
  end
45
84
 
46
- private
47
-
48
- def initialize_attributes(new_attributes)
49
- self.errors ||= ActiveModel::Errors.new(self)
50
- self.attributes = defaults.merge(new_attributes)
51
- end
52
-
53
- def defaults
54
- { }
85
+ def current_step_index
86
+ Array(steps).index(current_step)
55
87
  end
56
88
 
57
89
  def persisted?
58
90
  false
59
91
  end
60
92
 
93
+ private
94
+
61
95
  def shift_step(delta)
62
- self.step = steps[steps.index(current_step) + delta]
63
- end
96
+ idx = current_step_index
97
+ return unless idx
64
98
 
65
- def on_step(step)
66
- current_step == step
99
+ new_index = idx + delta
100
+ self.step = steps[new_index] if new_index >= 0 && new_index < steps.length
67
101
  end
102
+
68
103
  end
69
- end
104
+ end
@@ -0,0 +1,7 @@
1
+ <div class="schoolgirl-uniform-step-wizard">
2
+ <% form.steps.each_with_index do |step, index| %>
3
+ <div class="milestone <%= 'active' if form.current_step_index >= index %>">
4
+ <span><%= form.steps_details[step.to_sym] || step %></span>
5
+ </div>
6
+ <% end %>
7
+ </div>
@@ -0,0 +1,202 @@
1
+ <style>
2
+ body {
3
+ font-family: Arial, sans-serif;
4
+ background-color: #f4f4f4;
5
+ display: flex;
6
+ justify-content: center;
7
+ align-items: flex-start;
8
+ min-height: 100vh;
9
+ margin: 0;
10
+ padding: 20px 0;
11
+ }
12
+
13
+ .form-container {
14
+ background-color: #fff;
15
+ padding: 30px;
16
+ border-radius: 8px;
17
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
18
+ width: 100%;
19
+ max-width: 600px;
20
+ margin-top: 20px;
21
+ margin-bottom: 20px;
22
+ }
23
+
24
+ /* Step Wizard Styling */
25
+ .schoolgirl-uniform-step-wizard {
26
+ display: flex; /* Use flexbox for alignment */
27
+ justify-content: space-around; /* Distribute milestones evenly */
28
+ margin-bottom: 30px;
29
+ padding-bottom: 15px;
30
+ border-bottom: 1px solid #e0e0e0;
31
+ }
32
+
33
+ .schoolgirl-uniform-step-wizard .milestone {
34
+ padding: 8px 15px;
35
+ border: 1px solid #ccc;
36
+ border-radius: 20px; /* More rounded */
37
+ background-color: #f9f9f9;
38
+ color: #555;
39
+ font-size: 0.9em;
40
+ text-align: center;
41
+ flex-grow: 1; /* Allow milestones to grow */
42
+ margin: 0 5px; /* Add some spacing between milestones */
43
+ }
44
+
45
+ .schoolgirl-uniform-step-wizard .milestone.active { /* Active step */
46
+ background-color: #4CAF50; /* A more standard green */
47
+ color: white;
48
+ border-color: #388E3C;
49
+ }
50
+
51
+ .schoolgirl-uniform-step-wizard .milestone.completed { /* Completed step (optional, if you want to differentiate) */
52
+ background-color: #D4EDDA;
53
+ color: #155724;
54
+ border-color: #C3E6CB;
55
+ }
56
+
57
+ /* Form Styling */
58
+ .multistep-form .form-step-content {
59
+ border: 1px solid #ddd; /* Border for each step's content area */
60
+ padding: 25px;
61
+ border-radius: 6px;
62
+ margin-bottom: 25px;
63
+ background-color: #fdfdfd;
64
+ }
65
+
66
+ /* Styling for individual form fields/groups */
67
+ .multistep-form .form-group {
68
+ margin-bottom: 20px;
69
+ }
70
+
71
+ .multistep-form .form-group label {
72
+ display: block;
73
+ margin-bottom: 8px;
74
+ font-weight: bold;
75
+ color: #333;
76
+ font-size: 0.95em;
77
+ }
78
+
79
+ .multistep-form .form-group input[type="text"],
80
+ .multistep-form .form-group input[type="password"],
81
+ .multistep-form .form-group input[type="date"],
82
+ .multistep-form .form-group input[type="email"],
83
+ .multistep-form .form-group input[type="tel"], /* For phone_number */
84
+ .multistep-form .form-group select {
85
+ width: 100%; /* Make inputs take full width of their container */
86
+ padding: 12px;
87
+ border: 1px solid #ccc;
88
+ border-radius: 4px;
89
+ box-sizing: border-box; /* Important for width calculation */
90
+ font-size: 1em;
91
+ transition: border-color 0.3s ease;
92
+ }
93
+
94
+ .multistep-form .form-group input[type="text"]:focus,
95
+ .multistep-form .form-group input[type="password"]:focus,
96
+ .multistep-form .form-group input[type="date"]:focus,
97
+ .multistep-form .form-group input[type="email"]:focus,
98
+ .multistep-form .form-group input[type="tel"]:focus,
99
+ .multistep-form .form-group select:focus {
100
+ border-color: #4CAF50;
101
+ outline: none;
102
+ box-shadow: 0 0 5px rgba(76, 175, 80, 0.5);
103
+ }
104
+
105
+ /* Error Styling */
106
+ /* Error Styling for individual fields (Rails default) */
107
+ .field_with_errors {
108
+ display: inline-block; /* Or 'block' if you prefer full width */
109
+ width: 100%;
110
+ }
111
+ .field_with_errors input,
112
+ .field_with_errors select {
113
+ border: 1px solid red !important;
114
+ }
115
+
116
+ /* Custom Error Messages Block Styling (User Provided) */
117
+ .schoolgirl-uniform-errors {
118
+ border: 1px solid red;
119
+ padding: 10px; /* Added some padding for better appearance */
120
+ margin-bottom: 20px; /* Added margin for spacing */
121
+ border-radius: 4px; /* Optional: for rounded corners */
122
+ background-color: #ffebeb; /* Optional: light red background */
123
+ }
124
+ .schoolgirl-uniform-errors ul {
125
+ list-style-type: none; /* Removes default bullet points */
126
+ padding-left: 0; /* Removes default padding */
127
+ margin: 0;
128
+ }
129
+ .schoolgirl-uniform-errors li {
130
+ margin-bottom: 5px; /* Spacing between error messages */
131
+ }
132
+ .schoolgirl-uniform-errors span {
133
+ color: red;
134
+ }
135
+
136
+ /* Navigation Buttons Styling */
137
+ .form-navigation {
138
+ display: flex;
139
+ justify-content: space-between; /* Puts 'back' and 'next/submit' on opposite ends */
140
+ align-items: center;
141
+ margin-top: 25px;
142
+ padding-top: 20px;
143
+ border-top: 1px solid #e0e0e0;
144
+ }
145
+
146
+ .form-navigation a,
147
+ .form-navigation input[type="submit"] {
148
+ padding: 12px 25px;
149
+ border: none;
150
+ border-radius: 5px;
151
+ text-decoration: none;
152
+ font-size: 1em;
153
+ cursor: pointer;
154
+ transition: background-color 0.3s ease, box-shadow 0.3s ease;
155
+ }
156
+
157
+ .form-navigation a { /* Back button */
158
+ background-color: #6c757d;
159
+ color: white;
160
+ }
161
+ .form-navigation a:hover {
162
+ background-color: #5a6268;
163
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1);
164
+ }
165
+
166
+ .form-navigation input[type="submit"] { /* Next/Submit button */
167
+ background-color: #007bff; /* A nice blue for primary action */
168
+ color: white;
169
+ }
170
+ .form-navigation input[type="submit"]:hover {
171
+ background-color: #0056b3;
172
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1);
173
+ }
174
+ </style>
175
+ <div class="form-container">
176
+
177
+ <%= render 'wizard', form: @form %>
178
+
179
+ <%= form_for @form, as: "#{controller_name}_form".to_sym, url: form_carrier.current_step_path, html: { id: "#{controller_name}_form", class: 'multistep-form' } do |f| %>
180
+ <%= f.hidden_field :step, value: @form.current_step %>
181
+
182
+ <%= render "form_errors", form: @form %>
183
+
184
+ <div class="form-step-content">
185
+ <%= render "#{ controller_name }/steps/#{@form.current_step.underscore}", form: f %>
186
+ </div>
187
+
188
+ <div class="form-navigation">
189
+ <% if !@form.first_step? %>
190
+ <%= link_to 'Back', form_carrier.previous_step_path %>
191
+ <% else %>
192
+ <span>&nbsp;</span>
193
+ <% end %>
194
+
195
+ <% if @form.last_step? %>
196
+ <%= f.submit 'Submit Details' %>
197
+ <% else %>
198
+ <%= f.submit 'Next Step' %>
199
+ <% end %>
200
+ </div>
201
+ <% end %>
202
+ </div>
@@ -5,17 +5,13 @@ class <%= class_name.camelcase %>Controller < SchoolgirlUniform::BaseController
5
5
  end
6
6
 
7
7
  def finish
8
- @record = User.find_by(uuid: params[:identifier])
9
- end
10
-
11
- def form_attributes
12
- [:username, :password, :email, :phone]
8
+ @record = User.find_by(id: params[:identifier])
13
9
  end
14
10
 
15
11
  def paths
16
12
  {
17
- current: current_<%= class_name.snakecase %>_path(step: @form.current_step),
18
- previous: previous_<%= class_name.snakecase %>_path
13
+ current: current_<%= class_name.underscore %>_path(step: @form.current_step),
14
+ previous: previous_<%= class_name.underscore %>_path
19
15
  }
20
16
  end
21
17
  end