wizard_steps 0.1.3

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: 8439cdcbed27763997d550c7f93720ce6af9e4d14e727f12f459e9363db8ffa2
4
+ data.tar.gz: 06ad68edfc684da3713bdb534a891400fd42958acd429ff1b038cab8f566ccac
5
+ SHA512:
6
+ metadata.gz: 012110dd09c32f3c6f50e1b9bb1dcfbe9900435d96e5d374e94cc91e2ee007356b8a045a3cce4054143730bbce8e1838de24828f25ca63118a9f8c1ce0a3f255
7
+ data.tar.gz: c77b7ca3de33d643d4213bcb0b5afbd5be30672bd7244a26cca3b7cbb76baa0b6777b211d1fb31330930b463b68a7ef66bbb749e7e17b579c0be1b9ab2c60d8a
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ ---
2
+ language: ruby
3
+ cache: bundler
4
+ rvm:
5
+ - 2.7.1
6
+ before_install: gem install bundler -v 2.1.4
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ # Changelog
2
+ ## 0.1.0 (10-Feb-21)
3
+ * Initial release
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in wizard_steps.gemspec
4
+ gemspec
5
+
6
+ gem "rake", "~> 12.0"
7
+ gem "rspec", "~> 3.0"
8
+ gem 'activesupport', '~> 6.1', '>= 6.1.2.1'
9
+ gem 'activemodel', '~> 6.1', '>= 6.1.2.1'
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
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
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,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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
@@ -0,0 +1,3 @@
1
+ module WizardSteps
2
+ VERSION = "0.1.3"
3
+ end
@@ -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: []