hat-trick 0.0.1
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.
- 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
|