2
votes

I am using ember to built a "wizard accordion".

Basically what I want is:

  • an accordion which is always shown
  • the accordion contains all the steps
  • one step is active but it is also possibly to change the header of previous steps
  • each step has its own model (e.g. Selecting from countries in first step, selecting from products in second)
  • it should be possible to jump back and force between steps
  • out of all the selections a central model is built which is sent to the server after the final step
  • The number of steps is dynamic and sometimes certain steps are skipped

The architecture i thought about is have 3 outlets in the application template:

  • {{previousSteps}}
  • {{outlet}}
  • {{nextSteps}}

and always rendering 3 templates in each Route. Each step would have it's own route and controller and the create action of each step would save the shared model to application controller and transition to the next step.

But I guess this solution is by far not the best. Does somebody have a better architecture for this? Especially how to structure the routes, controller and templates

Thank you

Wizard MockupUpdate:

I am doing it the following way now. I appreciate any comments on my solution.

App.Router.map(function() {
   this.resource('steps', { path: '/' });
});

App.StepsRoute = Ember.Route.extend({
  model: function() {
    return [
      { controllerName: 'countries', templateName: 'countries', items:    this.store.find('country') },
      { controllerName:  'products', templateName: 'products', items: this.store.find('product') }
    ]
  }
});

App.StepsController = Ember.ArrayController.extend({
  currentStep: "countries",

  transitionToStep: function(step) {
    this.set('currentStep', step);
  },

  lookupItemController: function(modelObject) {
    return modelObject.controllerName;
  }
});

App.CountriesController = Ember.ObjectController.extend({
  needs: ['steps'],
  currentStep: Ember.computed.oneWay('controllers.steps.currentStep'),

  isExpanded: function() {
    return "countries" === this.get('currentStep');
  }.property('currentStep')
});

Steps.handlebars:

{{#each step in controller}}
  <div {{bind-attr class=":step controller.isExpanded:expanded"}}>
    {{view Ember.View templateName=step.templateName}}
  </div>
{{/each}}

I had to use ObjectController for Countries controller since using an ArrayController as itemController did not work

1

1 Answers

4
votes

Based on the way you laid out the UI, I think I'd do something like this in the router:

App.Router.map(function() {
  this.resource('wizard', function() {
    this.route('step1');
    this.route('step2');
    this.route('step3');
  });
});

Then, have a wizard template that looks like this:

{{#link-to 'wizard.step1'}}Step 1{{/link-to}}
<div {{bind-attr class='step1Expanded}}>{{outlet 'step1'}}</div>

{{#link-to 'wizard.step2'}}Step 2{{/link-to}}
<div {{bind-attr class='step2Expanded}}>{{outlet 'step2'}}</div>

{{#link-to 'wizard.step3'}}Step 3{{/link-to}}
<div {{bind-attr class='step3Expanded}}>{{outlet 'step3'}}</div>

Then, inside each of the step routes, you would need to override renderTemplate so that it will render in the appropriate outlet, like this:

App.WizardStep1Route = Ember.Route.extend({
  renderTemplate: function() {
    this.render({outlet: 'step1'});
  }
});

Finally, inside the WizardController, you would need to add computed properties to handle the step1Expanded logic being applied to the classes so you can know which one is displaying at any given time.

All this will allow you to load different models per step and will also allow you to handle model validation logic inside your routes. For example, if you cannot proceed to step3 until step1 and step2 are completed, you can add logic to handle that inside the step1 and step2 routes.