7
votes

I would like to devote a single route of my EmberJS app to being a multi-step form. This is the only time I want my URL to remain unchanged, so location: 'none' is not an option (as far as I can tell). I have controllers at other routes that are tightly integrated with the URL as they should be.

But at this single, unchanging URL I would like to accomplish the following:

  1. User answers some questions.
  2. User clicks a button and old questions are replaced with new questions.
  3. Rinse and repeat until the last "page" where all the data is finally .save()-ed on submit.

The way handlebars works is really throwing me for a loop on this.

I have been pouring over the documentation, but can't really find an example. I have a feeling that it is a case where I just don't know what I don't know yet. So if someone could point me in the right direction, hopefully that's all I'd need.

2

2 Answers

12
votes

You could achieve this with some actions, and some values that defines the state of the form.

Your controller could have some states properties like the following.

// Step one is default.
stepOne: true,
stepTwo: false,
stepThree: false,

How you want to transition from step to step, is a matter of use case, but you would end of changing the step properties, like so.

actions: {
  toStepTwo: function() {
    this.set('stepOne', false)
    this.set('stepOne', true) 
  },
  // But you could put this with some other functionality, say when the user answer a question.
  answerQuestion: function() {
    // Run some question code.
    // Go to next step. 
    this.set('stepOne', false)
    this.set('stepOne', true) 
  },
}

In your template you can just encapsulate your content using the if helper.

{{#if stepOne}}
  Step one
{{/if}
{{#if stepTwo}}
  This is step two
{{/if}}

So the reason for create 3 step properties here, instead of

currentStep: 1,

Is for the sake of handlebars, currently you can't match a current step like so.

{{#if currentStep == 1}}

Well unless you create a handlebars block helper.

26
votes

I started with MartinElvar's excellent answer, but wound up in a different place since I needed to validate a form on each page of the wizard. By making each page of the wizard it's own component, you can easily constrain the validation of each page.

Start with a list of steps on your controller:

// app/controllers/wizard.js
export default Ember.Controller.extend({
  steps: ['stepOne', 'stepTwo', 'stepThree'],
  currentStep: undefined
});

Then make sure that whenever your controller is entered, bounce the user to the first step:

// app/routes/wizard.js
export default Ember.Route.extend({
  setupController (controller, model) {
    controller.set('currentStep', controller.get('steps').get('firstObject');
    this._super(controller, model);
  }
});

Now you can go back to the controller and add some more generic next/back/cancel steps:

// app/controller/wizard.js
export default Ember.Controller.extend({
  steps: ['step-one', 'step-two', 'step-three'],
  currentStep: undefined,

  actions: {
    next () {
      let steps = this.get('steps'),
        index = steps.indexOf(this.get('currentStep'));

      this.set('currentStep', steps[index + 1]);
    },

    back () {
      let steps = this.get('steps'),
        index = steps.indexOf(this.get('currentStep'));

      this.set('currentStep', steps.get(index - 1));
    },

    cancel () {
      this.transitionToRoute('somewhere-else');
    },

    finish () {
      this.transitionToRoute('wizard-finished');
    }
  }
});

Now define a component for page of your wizard. The trick here is to define each component with the same name as each step listed in the controller. (This allows us to use a component helper later on.) This part is what allows you to perform form validation on each page of the wizard. For example, using ember-cli-simple-validation:

// app/components/step-one.js
import {ValidationMixin, validate} from 'ember-cli-simple-validation/mixins/validate';
export default Ember.Component.extend(ValidationMixin, {
  ...
  thingValidation: validate('model.thing'),
  actions: {
    next () {
      this.set('submitted', true);
      if (this.get('valid')) {
        this.sendAction('next');
      }
    },

    cancel () {
      this.sendAction('cancel');
    }
  }
});

And finally, the route's template becomes straight forward:

// app/templates/wizard.hbs
{{component currentStep model=model 
  next="next" back="back" cancel="cancel" finish="finish"}}

Each component gets a reference to the controller's model and adds the required data at step. This approach has turned out to be pretty flexible for me: it allows you to do whatever crazy things are necessary at each stage of the wizard (such as interacting with a piece of hardware and waiting for it to respond).