hat-trick 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/README.md +73 -0
- data/Rakefile +6 -0
- data/app/assets/javascripts/hat-trick.js.coffee +233 -0
- data/app/views/hat_trick/_wizard_meta.html.erb +1 -0
- data/hat-trick.gemspec +28 -0
- data/lib/hat-trick.rb +8 -0
- data/lib/hat_trick/controller_hooks.rb +118 -0
- data/lib/hat_trick/dsl.rb +123 -0
- data/lib/hat_trick/form_helper.rb +30 -0
- data/lib/hat_trick/model_methods.rb +24 -0
- data/lib/hat_trick/rails_engine.rb +13 -0
- data/lib/hat_trick/step.rb +35 -0
- data/lib/hat_trick/step_definition.rb +104 -0
- data/lib/hat_trick/version.rb +3 -0
- data/lib/hat_trick/wizard.rb +176 -0
- data/lib/hat_trick/wizard_definition.rb +22 -0
- data/lib/hat_trick/wizard_steps.rb +95 -0
- data/lib/hat_trick.rb +2 -0
- data/spec/lib/hat_trick/step_spec.rb +9 -0
- data/spec/lib/hat_trick/wizard_definition_spec.rb +39 -0
- data/spec/lib/hat_trick/wizard_spec.rb +64 -0
- data/spec/spec_helper.rb +33 -0
- data/vendor/assets/javascripts/jquery.ba-bbq.js +1137 -0
- data/vendor/assets/javascripts/jquery.form.js +1050 -0
- data/vendor/assets/javascripts/jquery.form.wizard.js +456 -0
- data/vendor/assets/javascripts/jquery.validate.js +1188 -0
- data/vendor/assets/javascripts/vendor_js.js +1 -0
- metadata +182 -0
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
# Hat Trick
|
2
|
+
> Combines jQuery FormWizard, validation_group, and gon for the perfect triple-play of Rails wizarding.
|
3
|
+
|
4
|
+
## Install
|
5
|
+
gem install hat-trick
|
6
|
+
|
7
|
+
### Rails 3.2
|
8
|
+
(older versions not supported)
|
9
|
+
|
10
|
+
Put this in your Gemfile:
|
11
|
+
|
12
|
+
gem 'hat-trick'
|
13
|
+
|
14
|
+
## Setup
|
15
|
+
In your controller:
|
16
|
+
|
17
|
+
wizard do
|
18
|
+
step :first_step
|
19
|
+
step :second_step
|
20
|
+
step :third_step
|
21
|
+
end
|
22
|
+
|
23
|
+
In your view:
|
24
|
+
|
25
|
+
<%= wizard_form_for @model do |f| %>
|
26
|
+
<fieldset id="first_step">...</fieldset>
|
27
|
+
<fieldset id="second_step">...</fieldset>
|
28
|
+
<fieldset id="third_step">...</fieldset>
|
29
|
+
<% end %>
|
30
|
+
|
31
|
+
The id's of the fieldsets in your form should match the step names you define in your controller.
|
32
|
+
|
33
|
+
## Controlling the wizard flow
|
34
|
+
Sometimes you need to specify different paths through a wizard based on certain conditions. The way you do that with hat-trick is in the wizard DSL in the controller. Here are some examples:
|
35
|
+
|
36
|
+
Jumping to a step based on logged in status:
|
37
|
+
|
38
|
+
wizard do
|
39
|
+
step :first_step do
|
40
|
+
# after_this_step defines a callback to run after the current step is completed by the user
|
41
|
+
after_this_step do
|
42
|
+
# code in this block will be exec'd in the context of your controller
|
43
|
+
if user_signed_in?
|
44
|
+
next_step :third_step
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
step :second_step # wizard will go here after :first_step if user is not signed in
|
50
|
+
|
51
|
+
step :third_step # wizard will go here after :first_step if user is signed in
|
52
|
+
|
53
|
+
Repeating a previous step (for example, to show address sanitization results to the user):
|
54
|
+
|
55
|
+
wizard do
|
56
|
+
step :enter_address
|
57
|
+
|
58
|
+
step :confirm_santized_address do
|
59
|
+
repeat_step :enter_address
|
60
|
+
end
|
61
|
+
|
62
|
+
Skipping a step under certain conditions:
|
63
|
+
|
64
|
+
wizard do
|
65
|
+
step :first_step
|
66
|
+
step :second_step do
|
67
|
+
# before_this_step defines a callback to run before the user sees this step
|
68
|
+
before_this_step do
|
69
|
+
# code in this block will be exec'd in the context of your controller
|
70
|
+
skip_this_step unless foo.present?
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/Rakefile
ADDED
@@ -0,0 +1,233 @@
|
|
1
|
+
#= require vendor_js
|
2
|
+
|
3
|
+
class HatTrickWizard
|
4
|
+
constructor: (formElem, @wizard) ->
|
5
|
+
@form = $(formElem)
|
6
|
+
fieldsets = @form.find("fieldset")
|
7
|
+
fieldsets.addClass("step")
|
8
|
+
wizard_buttons = '<input type="reset" /><input type="submit" />'
|
9
|
+
fieldsets.find("div.buttons").html wizard_buttons
|
10
|
+
window.htData = {}
|
11
|
+
# prevent submitting the step that happens to be the last fieldset
|
12
|
+
this.addFakeLastStep()
|
13
|
+
this.enableFormwizard() # unless this.formwizardEnabled()
|
14
|
+
this.setCurrentStepField()
|
15
|
+
# TODO: Try this out instead of putting :start first
|
16
|
+
# this.showStep(@wizard.currentStep)
|
17
|
+
this.bindEvents()
|
18
|
+
|
19
|
+
buttons: {}
|
20
|
+
|
21
|
+
findStep: (stepId) ->
|
22
|
+
@form.find("fieldset##{stepId}")
|
23
|
+
|
24
|
+
createMethodField: (method) ->
|
25
|
+
"""<input type="hidden" name="_method" value="#{method}" />"""
|
26
|
+
|
27
|
+
setAction: (url, method) ->
|
28
|
+
methodLower = method.toLowerCase()
|
29
|
+
console.log "Setting form action to #{methodLower} #{url}"
|
30
|
+
@form.attr("action", url)
|
31
|
+
@form.attr("method", "post")
|
32
|
+
methodField = @form.find('input[name="_method"]')
|
33
|
+
methodField.remove()
|
34
|
+
if methodLower isnt "post"
|
35
|
+
@form.prepend(this.createMethodField(method))
|
36
|
+
|
37
|
+
currentStepId: ->
|
38
|
+
@form.formwizard("state").currentStep
|
39
|
+
|
40
|
+
currentStep: ->
|
41
|
+
stepId = this.currentStepId()
|
42
|
+
this.findStep(stepId)
|
43
|
+
|
44
|
+
nextStepFieldHTML: """<input type="hidden" name="_ht_next_step" class="_ht_link" value="" />"""
|
45
|
+
|
46
|
+
fieldsets: ->
|
47
|
+
@form.find("fieldset")
|
48
|
+
|
49
|
+
ajaxEvents: (firstStep=false) ->
|
50
|
+
remoteAjax = {}
|
51
|
+
$fieldsets = this.fieldsets()
|
52
|
+
$fieldsets = $fieldsets.filter(":first") if firstStep
|
53
|
+
$fieldsets.each (index, element) =>
|
54
|
+
stepId = $(element).attr("id")
|
55
|
+
remoteAjax[stepId] = this.createAjaxEvent(stepId)
|
56
|
+
remoteAjax
|
57
|
+
|
58
|
+
createAjaxEvent: (step) ->
|
59
|
+
# console.log "Adding AJAX to step #{step}"
|
60
|
+
ajax =
|
61
|
+
url: @form.attr("action"),
|
62
|
+
dataType: "json",
|
63
|
+
beforeSubmit: (data) =>
|
64
|
+
console.log "Sending these data to the server: #{$.param(data)}"
|
65
|
+
success: (data) =>
|
66
|
+
console.log "Successful form POST; got #{$.param(data)}"
|
67
|
+
if data.wizardMetadata?
|
68
|
+
this.setAction(data.wizardMetadata.url, data.wizardMetadata.method)
|
69
|
+
# merge new data with window.htData
|
70
|
+
$.extend(window.htData, data)
|
71
|
+
error: (data) =>
|
72
|
+
appErrors = eval "(#{data.responseText})"
|
73
|
+
this.addErrorItem value[0] for key, value of appErrors.formModel
|
74
|
+
ajax
|
75
|
+
|
76
|
+
addErrorItem: (message) ->
|
77
|
+
$errorList = this.currentStep().find("ul.hat_trick_errors")
|
78
|
+
if $errorList.length > 0
|
79
|
+
$errorList.append("<li>#{message}</li>")
|
80
|
+
$errorList.show()
|
81
|
+
|
82
|
+
updateSteps: ->
|
83
|
+
@form.formwizard("update_steps")
|
84
|
+
@form.formwizard("option", remoteAjax: this.ajaxEvents())
|
85
|
+
|
86
|
+
goToStepId: (stepId) ->
|
87
|
+
console.log "Setting up goto #{stepId}"
|
88
|
+
this.setHTMeta("next_step", stepId)
|
89
|
+
@form.formwizard("next")
|
90
|
+
|
91
|
+
repeatStep: (step) ->
|
92
|
+
$sourceStep = this.findStep(step.repeatOf.fieldset)
|
93
|
+
console.log "Cloning repeated step #{step.repeatOf.fieldset}"
|
94
|
+
$clonedStep = $sourceStep.clone(true, true)
|
95
|
+
$clonedStep.css("display", "none")
|
96
|
+
$clonedStep.attr("id", step.name)
|
97
|
+
$sourceStep.after($clonedStep)
|
98
|
+
this.updateSteps()
|
99
|
+
@form.formwizard("show", step.name)
|
100
|
+
|
101
|
+
showStep: (step) ->
|
102
|
+
console.log "Showing step #{step.fieldset}"
|
103
|
+
@form.formwizard("show", step.fieldset)
|
104
|
+
|
105
|
+
formwizardEnabled: ->
|
106
|
+
@form.formwizard?
|
107
|
+
|
108
|
+
addFakeLastStep: ->
|
109
|
+
@form.append """<fieldset id="_ht_fake_last_step" style="display: none;" class="step"></fieldset>"""
|
110
|
+
|
111
|
+
enableFormwizard: ->
|
112
|
+
@form.formwizard
|
113
|
+
formPluginEnabled: true,
|
114
|
+
validationEnabled: true,
|
115
|
+
focusFirstInput: true,
|
116
|
+
historyEnabled: true,
|
117
|
+
disableUIStyles: true,
|
118
|
+
inDuration: 0,
|
119
|
+
linkClass: "_ht_link",
|
120
|
+
remoteAjax: this.ajaxEvents(true), # adds first Ajax event
|
121
|
+
formOptions:
|
122
|
+
success: (data) =>
|
123
|
+
console.log "Successful form POST"
|
124
|
+
beforeSubmit: (data) =>
|
125
|
+
console.log "Sending these data to the server: #{$.param(data)}"
|
126
|
+
|
127
|
+
htMetaHTML: (name) ->
|
128
|
+
"""<input type="hidden" name="_ht_meta[#{name}]" id="_ht_#{name}" value="" />"""
|
129
|
+
|
130
|
+
setHTMeta: (key, value) ->
|
131
|
+
$meta = @form.find("input:hidden#_ht_#{key}")
|
132
|
+
if $meta.length is 0
|
133
|
+
$meta = @form.prepend(this.htMetaHTML(key)).find("#_ht_#{key}")
|
134
|
+
$meta.val(value)
|
135
|
+
|
136
|
+
clearHTMeta: (key) ->
|
137
|
+
@form.find("input:hidden#_ht_#{key}").remove()
|
138
|
+
|
139
|
+
setCurrentStepField: ->
|
140
|
+
stepId = this.currentStepId()
|
141
|
+
this.setHTMeta("step", stepId)
|
142
|
+
console.log "Current form step: #{stepId}"
|
143
|
+
|
144
|
+
clearNextStepField: ->
|
145
|
+
this.clearHTMeta("next_step")
|
146
|
+
|
147
|
+
fieldRegex: /^([^\[]+)\[([^\]]+)\]$/
|
148
|
+
|
149
|
+
setFieldValues: (formModel, selector, callback) ->
|
150
|
+
$currentStep = this.currentStep()
|
151
|
+
$currentStep.find(selector).each (index, element) =>
|
152
|
+
$element = $(element)
|
153
|
+
elementName = $element.attr("name")
|
154
|
+
if elementName? and elementName.search(@fieldRegex) isnt -1
|
155
|
+
[_, modelName, fieldName] = elementName.match(@fieldRegex)
|
156
|
+
if formModel[modelName]? and formModel[modelName][fieldName]?
|
157
|
+
fieldValue = formModel[modelName][fieldName]
|
158
|
+
callback($element, fieldValue) if fieldValue?
|
159
|
+
|
160
|
+
fillTextFields: (formModel) ->
|
161
|
+
this.setFieldValues formModel, "input:text", ($input, value) =>
|
162
|
+
$input.val(value)
|
163
|
+
|
164
|
+
setSelectFields: (formModel) ->
|
165
|
+
this.setFieldValues formModel, "select", ($select, value) =>
|
166
|
+
$select.find("option[value=\"#{value}\"]").attr("selected", "selected")
|
167
|
+
|
168
|
+
setCheckboxes: (formModel) ->
|
169
|
+
this.setFieldValues formModel, "input:checkbox", ($checkbox, value) =>
|
170
|
+
$checkbox.attr("checked", "checked") if value
|
171
|
+
|
172
|
+
setRadioButtons: (formModel) ->
|
173
|
+
this.setFieldValues formModel, "input:radio", ($radio, value) =>
|
174
|
+
$radio.find("[value=\"#{value}\"]").attr("checked", "checked")
|
175
|
+
|
176
|
+
setFormFields: (formModel) ->
|
177
|
+
this.fillTextFields(formModel)
|
178
|
+
this.setSelectFields(formModel)
|
179
|
+
this.setCheckboxes(formModel)
|
180
|
+
this.setRadioButtons(formModel)
|
181
|
+
|
182
|
+
createButton: (name, label) ->
|
183
|
+
"""<input type="button" name="#{name}" value="#{label}" />"""
|
184
|
+
|
185
|
+
setButton: (name, label) ->
|
186
|
+
$buttonsDiv = this.currentStep().find("div.buttons")
|
187
|
+
switch name
|
188
|
+
when "next"
|
189
|
+
console.log "Setting submit button val to #{label}"
|
190
|
+
$buttonsDiv.find('input:submit').val(label)
|
191
|
+
when "back"
|
192
|
+
console.log "Setting reset button val to #{label}"
|
193
|
+
$buttonsDiv.find('input:reset').val(label)
|
194
|
+
else
|
195
|
+
buttonSelector = """input:button[name="#{name}"][value="#{label}"]"""
|
196
|
+
$existingButtons = $buttonsDiv.find(buttonSelector)
|
197
|
+
if $existingButtons.length == 0
|
198
|
+
console.log "Adding new #{name}:#{label} button"
|
199
|
+
$newButton = $buttonsDiv.append(this.createButton(name, label))
|
200
|
+
$newButton.click (event) =>
|
201
|
+
event.preventDefault()
|
202
|
+
this.goToStepId(name)
|
203
|
+
|
204
|
+
bindEvents: ->
|
205
|
+
@form.bind "step_shown", (event, data) =>
|
206
|
+
this.setCurrentStepField()
|
207
|
+
this.clearNextStepField()
|
208
|
+
this.setFormFields(htData.formModel)
|
209
|
+
|
210
|
+
buttons = this.buttons[this.currentStepId()]
|
211
|
+
if buttons?
|
212
|
+
this.setButton(name, label) for own name, label of buttons
|
213
|
+
|
214
|
+
if data.previousStep is data.firstStep
|
215
|
+
console.log "Adding additional Ajax events"
|
216
|
+
# adds additional Ajax events now that we have the update URL
|
217
|
+
@form.formwizard("option", remoteAjax: this.ajaxEvents())
|
218
|
+
|
219
|
+
@form.bind "after_remote_ajax", (event, data) =>
|
220
|
+
if htData.wizardMetadata?.currentStep.buttons?
|
221
|
+
stepId = htData.wizardMetadata.currentStep.fieldset
|
222
|
+
this.buttons[stepId] = htData.wizardMetadata.currentStep.buttons
|
223
|
+
|
224
|
+
if htData.wizardMetadata?.currentStep.repeatOf?
|
225
|
+
this.repeatStep(htData.wizardMetadata.currentStep)
|
226
|
+
else
|
227
|
+
this.showStep(htData.wizardMetadata.currentStep)
|
228
|
+
|
229
|
+
$ ->
|
230
|
+
$form = $("form.wizard")
|
231
|
+
if htData? and !htWizard?
|
232
|
+
console.log "Creating new HatTrickWizard instance"
|
233
|
+
window.htWizard = new HatTrickWizard($form, htData.wizardMetadata)
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= include_gon(:namespace => 'htData', :camel_case => true) %>
|
data/hat-trick.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "hat_trick/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "hat-trick"
|
7
|
+
s.version = HatTrick::VERSION
|
8
|
+
s.authors = ["Wes Morgan"]
|
9
|
+
s.email = ["cap10morgan@gmail.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{Rails wizards done right}
|
12
|
+
s.description = %q{Combines jQuery FormWizard, validation_group, and gon for the perfect triple-play of Rails wizarding.}
|
13
|
+
|
14
|
+
s.rubyforge_project = "hat-trick"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_development_dependency "rspec"
|
22
|
+
s.add_development_dependency "mocha"
|
23
|
+
s.add_development_dependency "debugger"
|
24
|
+
|
25
|
+
s.add_runtime_dependency "rails", "~> 3.1"
|
26
|
+
s.add_runtime_dependency "validation_group"
|
27
|
+
s.add_runtime_dependency "gon"
|
28
|
+
end
|
data/lib/hat-trick.rb
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'hat_trick/model_methods'
|
2
|
+
|
3
|
+
module HatTrick
|
4
|
+
module ControllerHooks
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
alias_method_chain :render, :hat_trick
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.def_action_method_aliases(action_methods)
|
12
|
+
action_methods.each do |meth|
|
13
|
+
Rails.logger.info "Aliasing #{meth}"
|
14
|
+
module_eval <<-RUBY_EVAL
|
15
|
+
def #{meth}_with_hat_trick(*args)
|
16
|
+
Rails.logger.info "#{meth}_with_hat_trick called"
|
17
|
+
#{meth}_hook(*args) if respond_to?("#{meth}_hook", :include_private)
|
18
|
+
common_hook(*args)
|
19
|
+
#{meth}_without_hat_trick(*args)
|
20
|
+
end
|
21
|
+
private "#{meth}_with_hat_trick"
|
22
|
+
RUBY_EVAL
|
23
|
+
end
|
24
|
+
true
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def params_model_name
|
30
|
+
params.each do |k,v|
|
31
|
+
return class_name(k) if v.is_a?(Hash) && is_model?(k)
|
32
|
+
end
|
33
|
+
nil
|
34
|
+
end
|
35
|
+
|
36
|
+
def is_model?(model_name)
|
37
|
+
begin
|
38
|
+
class_name(model_name).constantize
|
39
|
+
rescue NameError
|
40
|
+
return false
|
41
|
+
end
|
42
|
+
true
|
43
|
+
end
|
44
|
+
|
45
|
+
def class_name(hash_key)
|
46
|
+
hash_key.to_s.camelize
|
47
|
+
end
|
48
|
+
|
49
|
+
def model_class
|
50
|
+
model_name = params_model_name
|
51
|
+
return nil if model_name.nil?
|
52
|
+
begin
|
53
|
+
model_class = params_model_name.constantize
|
54
|
+
rescue NameError
|
55
|
+
Rails.logger.error "Could not find model class #{params_model_name.camelize}"
|
56
|
+
nil
|
57
|
+
else
|
58
|
+
model_class
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def setup_validation_group_for(wizard_step)
|
63
|
+
klass = model_class
|
64
|
+
return if klass.nil?
|
65
|
+
step_name = wizard_step.name
|
66
|
+
validation_groups = ::ActiveRecord::Base.validation_group_classes[klass] || []
|
67
|
+
unless validation_groups.include?(step_name)
|
68
|
+
klass.validation_group(step_name, :fields => params.keys)
|
69
|
+
end
|
70
|
+
HatTrick::ModelMethods.set_current_validation_group_for(model_class, step_name)
|
71
|
+
unless klass.included_modules.include?(HatTrick::ModelMethods)
|
72
|
+
klass.send(:include, HatTrick::ModelMethods)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def create_hook(*args)
|
77
|
+
setup_validation_group_for(ht_wizard.current_step)
|
78
|
+
end
|
79
|
+
|
80
|
+
def update_hook(*args)
|
81
|
+
setup_validation_group_for(ht_wizard.current_step)
|
82
|
+
end
|
83
|
+
|
84
|
+
def common_hook(*args)
|
85
|
+
# nothing here for now
|
86
|
+
end
|
87
|
+
|
88
|
+
def render_with_hat_trick(*args)
|
89
|
+
if args.first.has_key?(:json)
|
90
|
+
model = args[0][:json]
|
91
|
+
ht_wizard.model = model
|
92
|
+
end
|
93
|
+
|
94
|
+
if params.has_key?('_ht_meta')
|
95
|
+
next_step = params['_ht_meta']['next_step']
|
96
|
+
ht_wizard.advance_step(next_step)
|
97
|
+
end
|
98
|
+
|
99
|
+
wizard_metadata = {
|
100
|
+
:url => ht_wizard.current_form_url,
|
101
|
+
:method => ht_wizard.current_form_method,
|
102
|
+
:currentStep => ht_wizard.current_step,
|
103
|
+
}
|
104
|
+
|
105
|
+
# this sets the wizard_metadata for the initial page load
|
106
|
+
gon.wizard_metadata = wizard_metadata
|
107
|
+
|
108
|
+
if ht_wizard.model && args[0].has_key?(:json)
|
109
|
+
# this sets the wizard metadata for subsequent AJAX requests
|
110
|
+
args[0][:json] = { :formModel => ht_wizard.model,
|
111
|
+
:wizardMetadata => wizard_metadata }
|
112
|
+
args[0][:json].merge! ht_wizard.include_data
|
113
|
+
end
|
114
|
+
|
115
|
+
render_without_hat_trick(*args)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'hat_trick/wizard_definition'
|
2
|
+
require 'hat_trick/controller_hooks'
|
3
|
+
|
4
|
+
module HatTrick
|
5
|
+
module DSL
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def wizard(&block)
|
10
|
+
if block_given?
|
11
|
+
include HatTrick::DSL::ControllerInstanceMethods
|
12
|
+
include HatTrick::ControllerHooks
|
13
|
+
|
14
|
+
@wizard_def = HatTrick::WizardDefinition.new
|
15
|
+
|
16
|
+
@wizard_dsl = HatTrick::DSL::WizardContext.new(@wizard_def)
|
17
|
+
@wizard_dsl.instance_eval &block
|
18
|
+
|
19
|
+
else
|
20
|
+
raise ArgumentError, "wizard called without a block"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
module ControllerInstanceMethods
|
26
|
+
extend ActiveSupport::Concern
|
27
|
+
|
28
|
+
included do
|
29
|
+
before_filter :setup_wizard
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
attr_reader :ht_wizard
|
35
|
+
|
36
|
+
def setup_wizard
|
37
|
+
wizard_def = self.class.instance_variable_get("@wizard_def")
|
38
|
+
@ht_wizard = wizard_def.get_wizard(self)
|
39
|
+
|
40
|
+
if params.has_key?('_ht_meta')
|
41
|
+
step_name = params['_ht_meta']['step']
|
42
|
+
@ht_wizard.current_step = step_name if step_name
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class WizardContext
|
48
|
+
attr_reader :wizard_def
|
49
|
+
attr_accessor :wizard
|
50
|
+
|
51
|
+
delegate :model, :previously_visited_step, :to => :wizard
|
52
|
+
|
53
|
+
def initialize(wizard_def)
|
54
|
+
@wizard_def = wizard_def
|
55
|
+
end
|
56
|
+
|
57
|
+
def step(name, args={}, &block)
|
58
|
+
wizard_def.add_step(name, args)
|
59
|
+
instance_eval &block if block_given?
|
60
|
+
end
|
61
|
+
|
62
|
+
def next_step(name=nil)
|
63
|
+
if name.nil?
|
64
|
+
wizard.next_step
|
65
|
+
else
|
66
|
+
raise "next_step should only be called from an after block" if wizard.nil?
|
67
|
+
step = wizard.find_step(name)
|
68
|
+
wizard.current_step.next_step = step
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def repeat_step(name)
|
73
|
+
repeated_step = wizard_def.find_step(name)
|
74
|
+
raise ArgumentError, "Couldn't find step named #{name}" unless repeated_step
|
75
|
+
new_step = repeated_step.dup
|
76
|
+
# use the repeated step's fieldset id
|
77
|
+
new_step.fieldset = repeated_step.fieldset
|
78
|
+
# but use the current step's name
|
79
|
+
new_step.name = wizard_def.last_step.name
|
80
|
+
if wizard
|
81
|
+
# TODO: Might turn all these into run-time methods; which would get
|
82
|
+
# rid of this wizard / wizard_def distinction
|
83
|
+
else
|
84
|
+
# replace the step we're in the middle of defining w/ new_step
|
85
|
+
wizard_def.replace_step(wizard_def.last_step, new_step)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def skip_this_step
|
90
|
+
if wizard
|
91
|
+
wizard.skip_step(wizard.current_step)
|
92
|
+
else
|
93
|
+
# skip_this_step in wizard definition context means the step
|
94
|
+
# can be explicitly jumped to, but won't be in the normal flow
|
95
|
+
wizard_def.last_step.skipped = true
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def button_to(name, options=nil)
|
100
|
+
if wizard
|
101
|
+
raise "button_to not yet supported in before/after blocks"
|
102
|
+
end
|
103
|
+
label = options[:label] if options
|
104
|
+
label ||= name.to_s.humanize
|
105
|
+
step = wizard_def.last_step
|
106
|
+
step.buttons = step.buttons.merge(name => label)
|
107
|
+
end
|
108
|
+
|
109
|
+
def before(&block)
|
110
|
+
wizard_def.last_step.before_callback = block
|
111
|
+
end
|
112
|
+
|
113
|
+
def after(&block)
|
114
|
+
wizard_def.last_step.after_callback = block
|
115
|
+
end
|
116
|
+
|
117
|
+
def include_data(key, &block)
|
118
|
+
wizard_def.last_step.include_data = { key.to_sym => block }
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module HatTrick
|
2
|
+
module FormHelper
|
3
|
+
def wizard_form_for(record, *args, &proc)
|
4
|
+
options = args.extract_options!
|
5
|
+
options[:html] = { :class => 'wizard' }
|
6
|
+
|
7
|
+
wizard = controller.send(:ht_wizard)
|
8
|
+
wizard.start unless wizard.started?
|
9
|
+
|
10
|
+
# Do we still need these 2 lines?
|
11
|
+
wizard.model = record
|
12
|
+
controller.gon.form_model = record
|
13
|
+
|
14
|
+
options[:url] = wizard.current_form_url
|
15
|
+
options[:method] = wizard.current_form_method.to_sym
|
16
|
+
|
17
|
+
output = ActiveSupport::SafeBuffer.new
|
18
|
+
output.safe_concat(wizard_partial)
|
19
|
+
|
20
|
+
# now run the default FormBuilder & append to output
|
21
|
+
output << self.form_for(record, *(args << options), &proc)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def wizard_partial
|
27
|
+
controller.render_to_string(:partial => 'hat_trick/wizard_meta')
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module HatTrick
|
2
|
+
module ModelMethods
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
mattr_accessor :current_validation_group
|
5
|
+
|
6
|
+
included do
|
7
|
+
alias_method_chain :save, :hat_trick
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.set_current_validation_group_for(klass, validation_group_name)
|
11
|
+
self.current_validation_group ||= {}
|
12
|
+
current_validation_group[klass.to_s.underscore] = validation_group_name
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.current_validation_group_for(klass)
|
16
|
+
current_validation_group[klass.to_s.underscore]
|
17
|
+
end
|
18
|
+
|
19
|
+
def save_with_hat_trick(*args)
|
20
|
+
enable_validation_group HatTrick::ModelMethods.current_validation_group_for(self.class)
|
21
|
+
save_without_hat_trick(*args)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'hat_trick/form_helper'
|
2
|
+
|
3
|
+
module HatTrick
|
4
|
+
class RailsEngine < ::Rails::Engine
|
5
|
+
# just defining this causes Rails to look for assets inside this gem
|
6
|
+
|
7
|
+
initializer 'hat-trick.form_helpers' do
|
8
|
+
ActiveSupport.on_load(:action_view) do
|
9
|
+
include HatTrick::FormHelper
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|