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
         |