wizard_steps 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +51 -0
- data/README.md +427 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/wizard_steps/base.rb +131 -0
- data/lib/wizard_steps/step.rb +70 -0
- data/lib/wizard_steps/store.rb +42 -0
- data/lib/wizard_steps/version.rb +3 -0
- data/lib/wizard_steps.rb +86 -0
- data/wizard_steps.gemspec +27 -0
- metadata +61 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 8439cdcbed27763997d550c7f93720ce6af9e4d14e727f12f459e9363db8ffa2
|
4
|
+
data.tar.gz: 06ad68edfc684da3713bdb534a891400fd42958acd429ff1b038cab8f566ccac
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 012110dd09c32f3c6f50e1b9bb1dcfbe9900435d96e5d374e94cc91e2ee007356b8a045a3cce4054143730bbce8e1838de24828f25ca63118a9f8c1ce0a3f255
|
7
|
+
data.tar.gz: c77b7ca3de33d643d4213bcb0b5afbd5be30672bd7244a26cca3b7cbb76baa0b6777b211d1fb31330930b463b68a7ef66bbb749e7e17b579c0be1b9ab2c60d8a
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
wizard_steps (0.1.3)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
activemodel (6.1.2.1)
|
10
|
+
activesupport (= 6.1.2.1)
|
11
|
+
activesupport (6.1.2.1)
|
12
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
13
|
+
i18n (>= 1.6, < 2)
|
14
|
+
minitest (>= 5.1)
|
15
|
+
tzinfo (~> 2.0)
|
16
|
+
zeitwerk (~> 2.3)
|
17
|
+
concurrent-ruby (1.1.8)
|
18
|
+
diff-lcs (1.4.4)
|
19
|
+
i18n (1.8.8)
|
20
|
+
concurrent-ruby (~> 1.0)
|
21
|
+
minitest (5.14.3)
|
22
|
+
rake (12.3.3)
|
23
|
+
rspec (3.10.0)
|
24
|
+
rspec-core (~> 3.10.0)
|
25
|
+
rspec-expectations (~> 3.10.0)
|
26
|
+
rspec-mocks (~> 3.10.0)
|
27
|
+
rspec-core (3.10.1)
|
28
|
+
rspec-support (~> 3.10.0)
|
29
|
+
rspec-expectations (3.10.1)
|
30
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
31
|
+
rspec-support (~> 3.10.0)
|
32
|
+
rspec-mocks (3.10.2)
|
33
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
34
|
+
rspec-support (~> 3.10.0)
|
35
|
+
rspec-support (3.10.2)
|
36
|
+
tzinfo (2.0.4)
|
37
|
+
concurrent-ruby (~> 1.0)
|
38
|
+
zeitwerk (2.4.2)
|
39
|
+
|
40
|
+
PLATFORMS
|
41
|
+
ruby
|
42
|
+
|
43
|
+
DEPENDENCIES
|
44
|
+
activemodel (~> 6.1, >= 6.1.2.1)
|
45
|
+
activesupport (~> 6.1, >= 6.1.2.1)
|
46
|
+
rake (~> 12.0)
|
47
|
+
rspec (~> 3.0)
|
48
|
+
wizard_steps!
|
49
|
+
|
50
|
+
BUNDLED WITH
|
51
|
+
2.1.4
|
data/README.md
ADDED
@@ -0,0 +1,427 @@
|
|
1
|
+
# WizardSteps
|
2
|
+
|
3
|
+
TODO: describe your gem
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'wizard_steps'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle install
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install wizard_steps
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
Firstly, there is a pre-requisite of knowing what a multi-step form is if you don't already. A great resource is [this railscast](http://railscasts.com/episodes/217-multistep-forms).
|
24
|
+
|
25
|
+
The gem is fits a typical MVC style (model-view-controller)
|
26
|
+
|
27
|
+
## Model
|
28
|
+
|
29
|
+
List your steps in a wizard.rb file, located at the base of your multi-step folder. Take the following file structure below as an example for a module to create a user:
|
30
|
+
|
31
|
+
```
|
32
|
+
|models
|
33
|
+
|__user_creation
|
34
|
+
| |__steps
|
35
|
+
| | |__name.rb
|
36
|
+
| | |__date_of_birth.rb
|
37
|
+
| | |__gender.rb
|
38
|
+
| | |__review_answers.rb
|
39
|
+
| |__wizard.rb <--- list your steps here
|
40
|
+
|__user.rb
|
41
|
+
```
|
42
|
+
For the above example with a User class in user.rb, a user_creation folder with four steps, lets have a look at what the wizard.rb would look like:
|
43
|
+
|
44
|
+
```
|
45
|
+
# app/models/user_creation/wizard.rb
|
46
|
+
|
47
|
+
module UserCreation
|
48
|
+
class Wizard < WizardSteps::Base
|
49
|
+
self.steps = [
|
50
|
+
Steps::Name,
|
51
|
+
Steps::DateOfBirth,
|
52
|
+
Steps::Gender,
|
53
|
+
Steps::ReviewAnswers
|
54
|
+
].freeze
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def do_complete
|
59
|
+
User.create!(
|
60
|
+
first_name: @store.data["first_name"],
|
61
|
+
last_name: @store.data["last_name"],
|
62
|
+
date_of_birth: @store.data["date_of_birth"],
|
63
|
+
gender: @store.data["gender"],
|
64
|
+
)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
```
|
69
|
+
|
70
|
+
You create a module to wrap the multi step form. Inside this module, create a new wizard which derives from WizardSteps::Base, and register the steps you plan on using in the correct order.
|
71
|
+
|
72
|
+
The private method, `do_complete`, is what will be called **when the last step has been submitted**, in this example we are creating a User instance in the database. Note how `@store.data` is accessed.
|
73
|
+
|
74
|
+
In this example, our multi step form is for the User model, so we require various attributes in each step, such as Name, Date Of Birth and Gender, store them, review the answers, and if all is good, we submit.
|
75
|
+
|
76
|
+
Wait, but what does each step look like? Similarly to the above, it follows a modular pattern. Take the below as an example.
|
77
|
+
|
78
|
+
```
|
79
|
+
# app/models/user_creation/steps/name.rb
|
80
|
+
|
81
|
+
module UserCreation
|
82
|
+
module Steps
|
83
|
+
class Name < WizardSteps::Step
|
84
|
+
|
85
|
+
attribute :first_name, :string
|
86
|
+
attribute :last_name, :string
|
87
|
+
|
88
|
+
validates :first_name, :last_name, presence: true
|
89
|
+
|
90
|
+
def reviewable_answers
|
91
|
+
{
|
92
|
+
"name" => "#{first_name} #{last_name}"
|
93
|
+
}
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
```
|
99
|
+
The step name (`Name`) inherits from `WizardSteps::Step` which includes ActiveModel, so we can define and validate attributes in each class. The `reviewable_answers` method defines a hash that will be passed to the review_answers view.
|
100
|
+
|
101
|
+
### Date attribute
|
102
|
+
|
103
|
+
As the steps are ActiveModels, we need to include `ActiveRecord::AttributeAssignment` to simplify processing Rails date fields:
|
104
|
+
|
105
|
+
```
|
106
|
+
# models/user_creation/steps/date_of_birth.rb
|
107
|
+
|
108
|
+
module UserCreation
|
109
|
+
module Steps
|
110
|
+
class DateOfBirth < ::Wizard::Step
|
111
|
+
include ActiveRecord::AttributeAssignment
|
112
|
+
|
113
|
+
attribute :date_of_birth, :date
|
114
|
+
|
115
|
+
validates :date_of_birth, presence: true
|
116
|
+
|
117
|
+
def reviewable_answers
|
118
|
+
{
|
119
|
+
"date_of_birth" => date_of_birth,
|
120
|
+
}
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# views/user_creation/steps/_date_of_birth.html.erb
|
127
|
+
|
128
|
+
<%= form_for current_step, url: step_path do |f| %>
|
129
|
+
<%= f.date_field, :date_of_birth %>
|
130
|
+
<% end %>
|
131
|
+
```
|
132
|
+
## Review Answers
|
133
|
+
|
134
|
+
Your review_answers will look like this:
|
135
|
+
|
136
|
+
```
|
137
|
+
# models/user_creation/steps/review_answers.rb
|
138
|
+
|
139
|
+
module UserCreation
|
140
|
+
module Steps
|
141
|
+
class ReviewAnswers < WizardSteps::Step
|
142
|
+
def answers_by_step
|
143
|
+
@answers_by_step ||= @wizard.reviewable_answers_by_step
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# views/user_creation/steps/_review_answers.html.erb
|
150
|
+
|
151
|
+
<% f.object.answers_by_step.each do |step, answers| %>
|
152
|
+
<% answers.each do |answer| %>
|
153
|
+
# you have `step.key`, `answer.first`, `answer.last`
|
154
|
+
# and you can link back to a `(step)`
|
155
|
+
<% end %>
|
156
|
+
<% end %>
|
157
|
+
```
|
158
|
+
|
159
|
+
## Lets move onto the controller.
|
160
|
+
|
161
|
+
Your controller layout should follow:
|
162
|
+
|
163
|
+
```
|
164
|
+
|controllers
|
165
|
+
|__user_creation
|
166
|
+
| |__steps_controller.rb
|
167
|
+
```
|
168
|
+
|
169
|
+
Yep, its that simple. And in the controller:
|
170
|
+
|
171
|
+
```
|
172
|
+
# app/controllers/user_creation/steps_controller.rb
|
173
|
+
|
174
|
+
module UserCreation
|
175
|
+
class StepsController < ApplicationController
|
176
|
+
include WizardSteps
|
177
|
+
self.wizard_class = UserCreation::Wizard
|
178
|
+
|
179
|
+
private
|
180
|
+
|
181
|
+
def step_path(step = params[:id])
|
182
|
+
user_creation_step_path(step)
|
183
|
+
end
|
184
|
+
|
185
|
+
def wizard_store_key
|
186
|
+
:user_creation
|
187
|
+
end
|
188
|
+
|
189
|
+
def on_complete(user)
|
190
|
+
redirect_to(<your custom route>)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
```
|
195
|
+
|
196
|
+
Inside the module for your steps, you can see it follows a general controller layout deriving from ApplicationController.
|
197
|
+
|
198
|
+
And the views;
|
199
|
+
|
200
|
+
```
|
201
|
+
|views
|
202
|
+
|__user_creation
|
203
|
+
| |__steps
|
204
|
+
| |__ _form.html.erb
|
205
|
+
| |__ _name.html.erb
|
206
|
+
| |__ _date_of_birth.html.erb
|
207
|
+
| |__ _gender.html.erb
|
208
|
+
| |__ _review_answers.html.erb
|
209
|
+
| |__show.html.erb
|
210
|
+
```
|
211
|
+
|
212
|
+
```
|
213
|
+
# app/views/user_creation/_name.html.erb
|
214
|
+
|
215
|
+
<%= f.govuk_fieldset legend: { text: "Name" } do %>
|
216
|
+
<%= f.govuk_text_field :first_name, label: { text: 'First name' } %>
|
217
|
+
<%= f.govuk_text_field :last_name, label: { text: 'Last name' } %>
|
218
|
+
<% end %>
|
219
|
+
```
|
220
|
+
|
221
|
+
```
|
222
|
+
# app/views/user_creation/show.html.erb
|
223
|
+
|
224
|
+
<%= render "form", current_step: current_step, wizard: wizard %>
|
225
|
+
```
|
226
|
+
The form partial can check for `wizard.previous_key` as a conditional for a back button, and `wizard.can_proceed?` for a continue/submit button.
|
227
|
+
The other key lines are:
|
228
|
+
```
|
229
|
+
<%= form_for current_step, url: step_path do |f| %>
|
230
|
+
<%= render current_step.key, current_step: current_step, f: f %>
|
231
|
+
<% end >
|
232
|
+
```
|
233
|
+
As an example:
|
234
|
+
|
235
|
+
```
|
236
|
+
# app/views/user_creation/steps/_form.html.erb
|
237
|
+
|
238
|
+
<% if wizard.previous_key %>
|
239
|
+
<% content_for(:back_button) do %>
|
240
|
+
<%= back_link step_path(wizard.previous_key) %>
|
241
|
+
<% end %>
|
242
|
+
<% end %>
|
243
|
+
|
244
|
+
<div class="govuk-grid-row">
|
245
|
+
<div class="govuk-grid-column-two-thirds">
|
246
|
+
<%= form_for current_step, url: step_path do |f| %>
|
247
|
+
<%= f.govuk_error_summary %>
|
248
|
+
|
249
|
+
<%= render current_step.key, current_step: current_step, f: f %>
|
250
|
+
|
251
|
+
<% if wizard.can_proceed? %>
|
252
|
+
<%= f.govuk_submit("Continue") %>
|
253
|
+
<% end %>
|
254
|
+
<% end %>
|
255
|
+
</div>
|
256
|
+
</div>
|
257
|
+
```
|
258
|
+
|
259
|
+
|
260
|
+
And finally, in your routes
|
261
|
+
|
262
|
+
```
|
263
|
+
namespace :children_creation do
|
264
|
+
resources :steps, only: %i[show update]
|
265
|
+
end
|
266
|
+
```
|
267
|
+
|
268
|
+
## Context
|
269
|
+
|
270
|
+
It is possible to include a `context` where a stepped model belongs_to another model, in order to pass the latter id (foreign_key) to the stepped model. As an example we have a DiaryEntry which belongs to a Placement:
|
271
|
+
```
|
272
|
+
# app/models/diary_entry.rb
|
273
|
+
|
274
|
+
class DiaryEntry < ApplicationRecord
|
275
|
+
belongs_to :placement, optional: false, inverse_of: :diary_entries
|
276
|
+
|
277
|
+
validates :event, presence: true
|
278
|
+
validates :note, presence: true
|
279
|
+
end
|
280
|
+
|
281
|
+
# app/models/placement.rb
|
282
|
+
|
283
|
+
class Placement < ApplicationRecord
|
284
|
+
has_many :diary_entries, inverse_of: :placement
|
285
|
+
...
|
286
|
+
end
|
287
|
+
```
|
288
|
+
|
289
|
+
The model structure follows:
|
290
|
+
```
|
291
|
+
|models
|
292
|
+
|__diary
|
293
|
+
| |__steps
|
294
|
+
| | |__note.rb
|
295
|
+
| | |__event.rb
|
296
|
+
| | |__review_answers.rb
|
297
|
+
| |__wizard.rb <--- list your steps here
|
298
|
+
|__diary_entry.rb
|
299
|
+
|__placement.rb
|
300
|
+
```
|
301
|
+
|
302
|
+
In the controller we have a `placement_id` in `step_path` and `wizard_context`:
|
303
|
+
```
|
304
|
+
# app/controllers/diary/steps_controller.rb
|
305
|
+
|
306
|
+
module Diary
|
307
|
+
class StepsController < ApplicationController
|
308
|
+
include WizardSteps
|
309
|
+
self.wizard_class = Diary::Wizard
|
310
|
+
|
311
|
+
private
|
312
|
+
|
313
|
+
def step_path(step = params[:id])
|
314
|
+
placement_diary_step_path(placement_id: params[:placement_id], id: step)
|
315
|
+
end
|
316
|
+
|
317
|
+
def wizard_store_key
|
318
|
+
:diary
|
319
|
+
end
|
320
|
+
|
321
|
+
def wizard_context
|
322
|
+
{
|
323
|
+
"placement_id" => params[:placement_id],
|
324
|
+
}
|
325
|
+
end
|
326
|
+
|
327
|
+
def set_page_title
|
328
|
+
@page_title = "#{@current_step.title.downcase} step"
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|
332
|
+
```
|
333
|
+
|
334
|
+
In our routes:
|
335
|
+
```
|
336
|
+
resources :placements, only: :create do
|
337
|
+
resources :diary_entries,
|
338
|
+
only: %i[index show] do
|
339
|
+
end
|
340
|
+
namespace :diary do
|
341
|
+
resources :steps,
|
342
|
+
only: %i[index show update] do
|
343
|
+
collection do
|
344
|
+
get :completed
|
345
|
+
end
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
349
|
+
```
|
350
|
+
|
351
|
+
The placement_id is now available in `@context["placement_id"]` in wizard.rb
|
352
|
+
```
|
353
|
+
# app/models/diary/wizard.rb
|
354
|
+
|
355
|
+
module Diary
|
356
|
+
class Wizard < ::Wizard::Base
|
357
|
+
self.steps = [
|
358
|
+
Steps::SelectEvent,
|
359
|
+
Steps::Note,
|
360
|
+
Steps::ReviewAnswers,
|
361
|
+
].freeze
|
362
|
+
|
363
|
+
private
|
364
|
+
|
365
|
+
def do_complete
|
366
|
+
DiaryEntry.create!(
|
367
|
+
placement_id: @context["placement_id"],
|
368
|
+
event: @store.data["event"],
|
369
|
+
note: @store.data["entry"],
|
370
|
+
)
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|
374
|
+
```
|
375
|
+
|
376
|
+
## Skipping Steps
|
377
|
+
|
378
|
+
The order of the steps are linear however it is possible to create a branching flow by conditionally skipping any number of steps. Steps have a default `skipped?` status of false. This can be altered by defining `skipped?` in the individual step on some condition, ususally dependent on the contents of the `@store` hash derived from previous steps, e.g.
|
379
|
+
```
|
380
|
+
def skipped?
|
381
|
+
result = @store["some condition here is true"]
|
382
|
+
|
383
|
+
result
|
384
|
+
end
|
385
|
+
```
|
386
|
+
|
387
|
+
A step with a `skipped?` status of true will not be shown in the form flow. In this manner it is possible to build quite complex branching forms, although the conditional logic can become convoluted!
|
388
|
+
|
389
|
+
## Accessing the store data
|
390
|
+
|
391
|
+
The `store` is a reflection of part of the session data, and can be accessed by placing a `<% byebug %>` in any step view. The session key is set from the `wizard_store_key` defined in relevent `steps_controller.rb`, e.g.
|
392
|
+
```
|
393
|
+
#app/controllers/children_creation/steps_controller.rb
|
394
|
+
|
395
|
+
module ChildrenCreation
|
396
|
+
class StepsController < ApplicationController
|
397
|
+
include WizardSteps
|
398
|
+
self.wizard_class = ChildrenCreation::Wizard
|
399
|
+
|
400
|
+
private
|
401
|
+
|
402
|
+
def step_path(step = params[:id])
|
403
|
+
children_creation_step_path(step)
|
404
|
+
end
|
405
|
+
|
406
|
+
def wizard_store_key
|
407
|
+
:children_creation # KEY DEFINED HERE
|
408
|
+
end
|
409
|
+
|
410
|
+
def on_complete(child)
|
411
|
+
redirect_to(new_child_placement_need_path(child.id))
|
412
|
+
end
|
413
|
+
end
|
414
|
+
end
|
415
|
+
```
|
416
|
+
|
417
|
+
With `byebug` activated in a step view, in the console all data collected up to that view will be available:
|
418
|
+
|
419
|
+
```
|
420
|
+
(byebug) session[:children_creation]
|
421
|
+
{"first_name"=>"joe", "last_name"=>"bloggs", "date_of_birth"=>"2000-01-01"}
|
422
|
+
```
|
423
|
+
|
424
|
+
## Contributing
|
425
|
+
|
426
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/goodviber/wizard_steps.
|
427
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "wizard_steps"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
require 'active_support/core_ext/module'
|
3
|
+
|
4
|
+
module WizardSteps
|
5
|
+
class UnknownStep < RuntimeError; end
|
6
|
+
|
7
|
+
class Base
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
class_attribute :steps
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def indexed_steps
|
13
|
+
@indexed_steps ||= steps.index_by(&:key)
|
14
|
+
end
|
15
|
+
|
16
|
+
def step(key)
|
17
|
+
indexed_steps[key] || raise(UnknownStep)
|
18
|
+
end
|
19
|
+
|
20
|
+
def key_index(key)
|
21
|
+
steps.index step(key)
|
22
|
+
end
|
23
|
+
|
24
|
+
def step_keys
|
25
|
+
indexed_steps.keys
|
26
|
+
end
|
27
|
+
|
28
|
+
def first_key
|
29
|
+
step_keys.first
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
delegate :step, :key_index, :indexed_steps, :step_keys, to: :class
|
34
|
+
delegate :can_proceed?, to: :find_current_step
|
35
|
+
attr_reader :current_key
|
36
|
+
|
37
|
+
def initialize(store, current_key, context: {})
|
38
|
+
raise(UnknownStep) unless step_keys.include?(current_key)
|
39
|
+
|
40
|
+
@store = store
|
41
|
+
@context = context
|
42
|
+
@current_key = current_key
|
43
|
+
end
|
44
|
+
|
45
|
+
def find(key)
|
46
|
+
step(key).new self, @store
|
47
|
+
end
|
48
|
+
|
49
|
+
def find_current_step
|
50
|
+
find current_key
|
51
|
+
end
|
52
|
+
|
53
|
+
def previous_key(key = current_key)
|
54
|
+
earlier_keys(key).reverse.find do |k|
|
55
|
+
!find(k).skipped?
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def next_key(key = current_key)
|
60
|
+
later_keys(key).find do |k|
|
61
|
+
!find(k).skipped?
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def first_step?
|
66
|
+
previous_key.nil?
|
67
|
+
end
|
68
|
+
|
69
|
+
def last_step?
|
70
|
+
next_key.nil?
|
71
|
+
end
|
72
|
+
|
73
|
+
def valid?
|
74
|
+
active_steps.all?(&:valid?)
|
75
|
+
end
|
76
|
+
|
77
|
+
def complete?
|
78
|
+
last_step? && valid?
|
79
|
+
end
|
80
|
+
|
81
|
+
def complete!
|
82
|
+
return unless complete?
|
83
|
+
|
84
|
+
do_complete.tap do |result|
|
85
|
+
@store.purge!
|
86
|
+
yield(result) if block_given?
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def invalid_steps
|
91
|
+
active_steps.select(&:invalid?)
|
92
|
+
end
|
93
|
+
|
94
|
+
def first_invalid_step
|
95
|
+
active_steps.find(&:invalid?)
|
96
|
+
end
|
97
|
+
|
98
|
+
def later_keys(key = current_key)
|
99
|
+
steps[(key_index(key) + 1)..].to_a.map(&:key)
|
100
|
+
end
|
101
|
+
|
102
|
+
def earlier_keys(key = current_key)
|
103
|
+
index = key_index(key)
|
104
|
+
return [] unless index.positive?
|
105
|
+
|
106
|
+
steps[0..(index - 1)].map(&:key)
|
107
|
+
end
|
108
|
+
|
109
|
+
def export_data
|
110
|
+
all_steps.map(&:export).reduce({}, :merge)
|
111
|
+
end
|
112
|
+
|
113
|
+
def reviewable_answers_by_step
|
114
|
+
all_steps.reject(&:skipped?).each_with_object({}) do |step, hash|
|
115
|
+
hash[step.class] = step.reviewable_answers
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def all_steps
|
122
|
+
step_keys.map(&method(:find))
|
123
|
+
end
|
124
|
+
|
125
|
+
def active_steps
|
126
|
+
all_steps.reject(&:skipped?)
|
127
|
+
end
|
128
|
+
|
129
|
+
def do_complete; end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module WizardSteps
|
2
|
+
class Step
|
3
|
+
include ActiveModel::Model
|
4
|
+
include ActiveModel::Attributes
|
5
|
+
include ActiveModel::Validations::Callbacks
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def key
|
9
|
+
name.split("::").last.underscore
|
10
|
+
end
|
11
|
+
|
12
|
+
def contains_personal_details?
|
13
|
+
false
|
14
|
+
end
|
15
|
+
|
16
|
+
def title
|
17
|
+
key.humanize
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
delegate :key, :contains_personal_details?, to: :class
|
22
|
+
alias_method :id, :key
|
23
|
+
|
24
|
+
def initialize(wizard, store, attributes = {}, *args)
|
25
|
+
@wizard = wizard
|
26
|
+
@store = store
|
27
|
+
super(*args)
|
28
|
+
assign_attributes attributes_from_store
|
29
|
+
assign_attributes attributes
|
30
|
+
end
|
31
|
+
|
32
|
+
def save!
|
33
|
+
return false unless valid?
|
34
|
+
|
35
|
+
persist_to_store
|
36
|
+
end
|
37
|
+
|
38
|
+
def can_proceed?
|
39
|
+
true
|
40
|
+
end
|
41
|
+
|
42
|
+
def persisted?
|
43
|
+
!id.nil?
|
44
|
+
end
|
45
|
+
|
46
|
+
def skipped?
|
47
|
+
false
|
48
|
+
end
|
49
|
+
|
50
|
+
def export
|
51
|
+
return {} if skipped?
|
52
|
+
|
53
|
+
Hash[attributes.keys.zip([])].merge attributes_from_store
|
54
|
+
end
|
55
|
+
|
56
|
+
def reviewable_answers
|
57
|
+
attributes
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def attributes_from_store
|
63
|
+
@store.fetch attributes.keys
|
64
|
+
end
|
65
|
+
|
66
|
+
def persist_to_store
|
67
|
+
@store.persist attributes
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'active_support/core_ext/module'
|
2
|
+
|
3
|
+
module WizardSteps
|
4
|
+
class Store
|
5
|
+
attr_reader :data
|
6
|
+
delegate :keys, to: :data
|
7
|
+
|
8
|
+
def initialize(data)
|
9
|
+
raise InvalidBackingStore unless data.respond_to?(:[]=)
|
10
|
+
|
11
|
+
@data = data
|
12
|
+
end
|
13
|
+
|
14
|
+
def [](key)
|
15
|
+
data[key.to_s]
|
16
|
+
end
|
17
|
+
|
18
|
+
def []=(key, value)
|
19
|
+
data[key.to_s] = value
|
20
|
+
end
|
21
|
+
|
22
|
+
def fetch(*keys)
|
23
|
+
data.slice(*Array.wrap(keys).flatten.map(&:to_s)).stringify_keys
|
24
|
+
end
|
25
|
+
|
26
|
+
def persist(attributes)
|
27
|
+
data.merge! attributes.stringify_keys
|
28
|
+
|
29
|
+
true
|
30
|
+
end
|
31
|
+
|
32
|
+
def purge!
|
33
|
+
data.clear
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_camelized_hash
|
37
|
+
data.transform_keys { |k| k.camelize(:lower).to_sym }
|
38
|
+
end
|
39
|
+
|
40
|
+
class InvalidBackingStore < RuntimeError; end
|
41
|
+
end
|
42
|
+
end
|
data/lib/wizard_steps.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
require "active_support/concern"
|
2
|
+
Dir[File.join(__dir__, "wizard_steps", "*.rb")].each { |file| require file }
|
3
|
+
|
4
|
+
module WizardSteps
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
class_attribute :wizard_class
|
9
|
+
helper_method :wizard, :current_step, :step_path
|
10
|
+
end
|
11
|
+
|
12
|
+
def index
|
13
|
+
redirect_to step_path(wizard_class.first_key)
|
14
|
+
end
|
15
|
+
|
16
|
+
def show; end
|
17
|
+
|
18
|
+
def update
|
19
|
+
current_step.assign_attributes step_params
|
20
|
+
|
21
|
+
if current_step.save!
|
22
|
+
if wizard.complete?
|
23
|
+
wizard.complete! { |result| on_complete(result) }
|
24
|
+
else
|
25
|
+
redirect_to(next_step_path)
|
26
|
+
end
|
27
|
+
else
|
28
|
+
render :show
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def completed; end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def wizard
|
37
|
+
@wizard ||= wizard_class.new(wizard_store, params[:id], context: wizard_context)
|
38
|
+
end
|
39
|
+
|
40
|
+
def current_step
|
41
|
+
@current_step ||= wizard.find_current_step
|
42
|
+
end
|
43
|
+
|
44
|
+
def next_step_path
|
45
|
+
if (next_key = wizard.next_key)
|
46
|
+
step_path next_key
|
47
|
+
elsif (invalid_step = wizard.first_invalid_step)
|
48
|
+
step_path invalid_step
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def step_path(step = params[:id])
|
53
|
+
raise(NotImplementedError)
|
54
|
+
end
|
55
|
+
|
56
|
+
def step_params
|
57
|
+
return {} unless params.key?(step_param_key)
|
58
|
+
|
59
|
+
params.require(step_param_key).permit current_step.attributes.keys
|
60
|
+
end
|
61
|
+
|
62
|
+
def step_param_key
|
63
|
+
current_step.class.model_name.param_key
|
64
|
+
end
|
65
|
+
|
66
|
+
def wizard_store
|
67
|
+
::WizardSteps::Store.new(session_store)
|
68
|
+
end
|
69
|
+
|
70
|
+
def session_store
|
71
|
+
session[wizard_store_key] ||= {}
|
72
|
+
end
|
73
|
+
|
74
|
+
def wizard_context
|
75
|
+
{}
|
76
|
+
end
|
77
|
+
|
78
|
+
def wizard_store_key
|
79
|
+
raise(NotImplementedError)
|
80
|
+
end
|
81
|
+
|
82
|
+
def on_complete(_result)
|
83
|
+
redirect_to(action: :completed)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require_relative 'lib/wizard_steps/version'
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = "wizard_steps"
|
5
|
+
spec.version = WizardSteps::VERSION
|
6
|
+
spec.author = "Max Mills"
|
7
|
+
spec.email = ["8balldigitalsolutions@gmail.com"]
|
8
|
+
|
9
|
+
spec.summary = %q{ A helper module to create multi-step forms typical of gov.uk forms }
|
10
|
+
spec.description = %q{ See README for full description }
|
11
|
+
spec.homepage = "https://github.com/goodviber/wizard_steps"
|
12
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
|
13
|
+
|
14
|
+
spec.metadata['allowed_push_host'] = "https://rubygems.org"
|
15
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
16
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
17
|
+
|
18
|
+
# Specify which files should be added to the gem when it is released.
|
19
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
20
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
21
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
22
|
+
end
|
23
|
+
spec.bindir = "exe"
|
24
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
25
|
+
spec.require_paths = ["lib"]
|
26
|
+
end
|
27
|
+
|
metadata
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: wizard_steps
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Max Mills
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-03-17 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: " See README for full description "
|
14
|
+
email:
|
15
|
+
- 8balldigitalsolutions@gmail.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- ".gitignore"
|
21
|
+
- ".rspec"
|
22
|
+
- ".travis.yml"
|
23
|
+
- CHANGELOG.md
|
24
|
+
- Gemfile
|
25
|
+
- Gemfile.lock
|
26
|
+
- README.md
|
27
|
+
- Rakefile
|
28
|
+
- bin/console
|
29
|
+
- bin/setup
|
30
|
+
- lib/wizard_steps.rb
|
31
|
+
- lib/wizard_steps/base.rb
|
32
|
+
- lib/wizard_steps/step.rb
|
33
|
+
- lib/wizard_steps/store.rb
|
34
|
+
- lib/wizard_steps/version.rb
|
35
|
+
- wizard_steps.gemspec
|
36
|
+
homepage: https://github.com/goodviber/wizard_steps
|
37
|
+
licenses: []
|
38
|
+
metadata:
|
39
|
+
allowed_push_host: https://rubygems.org
|
40
|
+
homepage_uri: https://github.com/goodviber/wizard_steps
|
41
|
+
source_code_uri: https://github.com/goodviber/wizard_steps
|
42
|
+
post_install_message:
|
43
|
+
rdoc_options: []
|
44
|
+
require_paths:
|
45
|
+
- lib
|
46
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: 2.7.0
|
51
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
requirements: []
|
57
|
+
rubygems_version: 3.1.2
|
58
|
+
signing_key:
|
59
|
+
specification_version: 4
|
60
|
+
summary: A helper module to create multi-step forms typical of gov.uk forms
|
61
|
+
test_files: []
|