Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ember.js - Creating a multistep wizard - How to?

I need to create a wizard with multiple steps. Each step will display a form with options, and depending on the user choices, the wizard should go to a certain step, as well as keeping user preferences (choices) stored in some place.

These preferences are not saved in the model, they are only relevant to the step of model creation.

In order to give some context, the goal of this is:

  • Make the user a few questions regarding the opening hours of his business. For example: Is it open on weekends?, Is it different in summer?.
  • According to the answers to these questions, a final form will be displayed to create the timetable(s) model(s).

The question is, which would be the best way to accomplish this inside Ember?

Here are my –Ember newbie– thoughts:

  • Create a template for each wizard step.
  • Keep track of the current step. Where? Controller? Route?
  • Display these templates into outlets, which should be rendered dynamically according to the current step. This is where I get completely lost. How to do this? Should each step have a different route or not?
  • Keep track of user answers in the controller.
  • Once the wizard is finished, load the form template, which will read user preferences stored in controller.

Versions being used:

  • Ember.VERSION : 1.0.0-rc.1 application.js:9268
  • Handlebars.VERSION : 1.0.0-rc.3 application.js:9268
  • jQuery.VERSION : 1.9.1
like image 717
Manuel Pedrera Avatar asked Mar 13 '13 11:03

Manuel Pedrera


3 Answers

Sounds like you are on the right track.

Create a template for each wizard step.

Yes, that is a good start.

Keep track of the current step. Where? Controller? Route?

Either controller or route would work. Route makes most sense if you want bookmarkable urls for each step and for back/forward to work and is probably the most straightforward solution. Will assume you've chosen route.

Display these templates into outlets, which should be rendered dynamically according to the current step. This is where I get completely lost. How to do this? Should each step have a different route or not?

Since each step will be a route, ember will take care of rendering appropriate template automagically.

Keep track of user answers in the controller.

Once the wizard is finished, load the form template, which will read user preferences stored in controller.

Think of "finished" as just another step. Each step gets it's own controller which is used to record user responses. The last controller uses "needs" to access earlier controllers in order to customize behavior based on responses to the wizard.

like image 53
Mike Grassotti Avatar answered Oct 19 '22 19:10

Mike Grassotti


I know this an old thread and probably won't help the OP, but stackoverflow leads to more answers than anything else. I just wrote a blog post on this.

Look at this JSBin to see what I've done. I'll summarize here.

Route per step in template:

Router.map(function() {
  this.resource('wizard', function(){
    this.route('index');
    this.route('review');
    this.route('complete');
  });
});

Custom computed property that changes values when the routes change (in this case when the steps of the wizard changes)

import Ember from 'ember';  
var get = Ember.get;  
var computed = Ember.computed;

export function routeVal(routeVals, prop){  
    return computed('currentPath', function(){
        var currentRoute = get(this, 'currentPath');
        var routeValues = get(this, routeVals);
        for (var i = 0; i < routeValues.length; i++) {
            if (routeValues[i].route === currentRoute) {
                return routeValues[i][prop];
            }
        }
    });
}

A route-value object:

export default Ember.Object.extend({
    route: null
    //all other props can be added dynamically
});

A controller mixin for being aware of the current route:

export default Ember.Mixin.create({  
    needs: ['application'],
    currentPath: Ember.computed.alias("controllers.application.currentPath")

});

The resource controller:

import CurrentRouteAware from 'path/to/mixin'; 
import {routeVal} from 'app_name/utils/macros';
export default Ember.Controller.extend(CurrentRouteAware, {
  routeValues: [
        RouteVal.create({
            route: 'wizard.index',
            step: 'Create',
            next: 'Review',
            nextTransition: 'wizard.review',
            prevTransition: 'wizard.index',
            showNext: true,
            showPrev: false
        }),
        RouteVal.create({
            route: 'wizard.review',
            step: 'Review',
            next: 'Complete',
            prev: 'Create',
            nextTransition: 'wizard.complete',
            prevTransition: 'wizard.index',
            showNext: true,
            showPrev: true
        }),
        RouteVal.create({
            route: 'wizard.complete',
            step: 'Complete',
            next: 'Make Another',
            prev: 'Review',
            nextTransition: 'wizard.complete',
            prevTransition: 'wizard.review',
            showNext: false,
            showPrev: true
        })
    ], 
    nextButton: routeVal('routeValues', 'next'),
    prevButton: routeVal('routeValues', 'prev'),
    nextTransition: routeVal('routeValues', 'nextTransition'),
    showButtons: routeVal('routeValues', 'showButtons'),
    prevTransition: routeVal('routeValues', 'prevTransition'),
    showNext: routeVal('routeValues', 'showNext'),
    showPrev: routeVal('routeValues', 'showPrev'),
    actions: {
        next: function(){
            this.transitionToRoute(this.get('nextTransition'));
        },
        prev: function(){
            this.transitionToRoute(this.get('prevTransition'));
        }
    }
});

Think of the route value object as meaning, "When the route equals routeVal.route, the following properties will have these values" eg "When the currently active route is 'wizard.index' the next transition is to 'wizard.review', the next button text is 'Review', the previous button should be hidden, etc"

And lastly, your resource template:

<div class="wizard" id="wizardIllustration">
    {{form-wizard steps=routeValues currentPath=currentPath}}
  <div class="actions">
  {{#if showPrev}}
    <button type="button" {{action 'prev'}}class="btn btn-default btn-prev"><span class="glyphicon glyphicon-arrow-left"></span>{{prevButton}}</button>
    {{/if}}
    {{#if showNext}}
    <button {{action 'next'}}type="button" class="btn btn-default btn-next" >{{nextButton}}<span class="glyphicon glyphicon-arrow-right"></span></button>
    {{/if}}
  </div>
  <div class="step-content">
    {{outlet}}
  </div>

</div>

You can look at the jsbin for what the form-wizard component was (just a wrapping around the css for Fuelux wizards that keeps the active class on the correct step based on the route). The body of the wizard is the template for each of the subroutes. The next/prev button's text change depending on the route, as do their transitions (since the transition depends on the current state of the wizard). It's more or less a FSM

like image 8
mistahenry Avatar answered Oct 19 '22 17:10

mistahenry


Answering an old question in case someone needs some answers.

You can use something like this Multi-step form (or multiple "pages") in one route using Ember.js

this utilizes 1 route and each step/page is shown or hidden using {{#if}} To answer Nino's question, when I implemented this I had an object in my controller keeping track of all the values of the form fields and updating it as I click on next. When you get to the last page where you submit, you can simply plug this object into your controller's submit function like this

submitSurvey: function() {
  var survey = this.store.createRecord('survey', this.get('surveyObj'));
  // surveyObj contains all the form field's values that you've been
  // keeping track of everytime you go from one step to another
  survey.save();
  this.transitionToRoute('somewhere'); // go somewhere after submitting the form
  this.set('firstPage', true); // make first page the page to show
  this.set('lastPage', false);
  this.init();
  // constructor's init contains code that empties all the form fields alternatively
  // you can create a separate function instead of using init
}

Good for simple implementation of a multi-page form. Problem I have with this is I have jQuery UI code that hooks into the view's didInsertElement and it works just that when I go from one step to another and come back (each page has 'next' and 'prev' buttons), I find that whatever the code that was ran at didInsertElement becomes undone. It's like the chunk of html code wasn't just hidden and re-shown. It was reloaded and thus all effects are gone.

like image 2
celerius Avatar answered Oct 19 '22 18:10

celerius