Do You Have a Pattern for Multi Step Checkout?


#1

I’m getting ready to build a really long multi-step form and i’d love to know how you handle an 8-10 step ‘checkout’ flow. I have an example of what i’m currently using (IR)

I’d really like to move it out of the Router controller and keep everything in a set of ‘components’ (blaze templates and js files in one folder basically) and have the parent ‘page’ template handle all the state for each step.

It has to have next and back buttons. Also I think handling the browser’s back button would be needed for good UX. The browser’s back button is tripping me up with keeping all the state in the parent template.

Prev. i’ve used an IR controller with query params for each step, along it having it set a state dict. for reactive. This works for basic state if you want to save state for each step it get’s really hairy.
Here’s an example of that:

PostsController.New = AppController.extend({
  template: 'postsNew',

  action: function() {
    var query = this.params.query;
    // parent template with named yield 'stepContent'
    this.render('postsNew');

    switch(query.step) {
      case '2':
        this.state.set('stepNumber', 2);
        this.render('postsNewBar', {to: 'stepContent'});
        break;
      case '3':
        this.state.set('stepNumber', 3);
        this.render('postsNewBaz', {to: 'stepContent'});
        break;
      default:
        this.state.set('stepNumber', 1);
        this.render('postsNewFoo', {to: 'stepContent'});
    }
  }
});

#2

Working on something similar (User onboarding), and wondering how you handled this. I was planning on using Flow Router instead of IR.


#3

Cool, I would recommend Flow Router, it worked out rather well !

Because of the form being complex and huge amounts of state on this form I opted to go with React for just the form and I haven’t looked back. It really helps deal with passing state around.

To give you an idea of the form it has around a hundred inputs, user can skip form steps, sidebar that shows step progress (incomplete, invalid, complete), add/remove sub forms as applicable, fill-in input values when going forward/back, etc…

React at it’s core is all about state management and dealing with complexity which is made easier with the one way data flow. If you’re interested in React just let me know and I can go over more.

However if you’re using Blaze I would do something like this (very similar layout).

FlowRouter defines a route for the form page this page renders a FormContainer template. This is where I stop with router logic. Going from step to step would just call something like /onboard?step=2 , /onboard?step=3.

Gather as much data in FormContainer as you can and push it down into the children. This helps keep it in one place and makes children templates more usable and easier to reason about. You can setup subscriptions, call FlowRouter API to get the query params (steps), setup a shared state that the onboarding steps can all share. Checkout this article on Template subscription. It works better to subscribe here rather than the router.

FormContainer also sets up a parent template ‘state’ that the children steps can consume. This state can hold the stepNumber or any other data needed (even the input values). See ‘Initializing The Reactive Variables’ in the link above for this.

Finally the FormContainer can setup a dynamic template helper that changes based on the step number using {{> Template.dynamic template=getStepTemplate }} using a switch statement using the template state (sort of like the IR example above). The switches the page depending on the ?step=3 query params.

Lastly I needed to be able to fill in input values as you go forward/back. Because React’s state is so simple I opted to fill out the values based on the current state. Blaze doesn’t make this as easy but you could store the inputs value in the template state on blur. Or if you can just save to the DB as you go that would work too.

I would also make Input blaze helpers to wrap your inputs if your form is large, this makes editing them easier (as well as saving state).

Hope this helps!


#4

Awesome, thanks. That’s pretty in line with what I was thinking.


#5

Your approach is newer and more shiny than the Payment Crowdsourcing demo from cookbook; but it’s mostly the same basic concept: put it all into one big page, and then apply some origami to the page to selectively display each step in the workflow.

http://payment-crowdsourcing.meteor.com/

Payment Crowdsourcing was written back in 0.6.5 days, way before React was released. So it uses Session objects, and toggles classes to show/hide divs. Nowadays I’d probably do something very similar to what you’re doing… define one layout template to keep the state in, and then render different templates into it.


#6

Very nice! My apps were a mess back then!

One thing i’m noticing is that if you need to share state across two different components (in either Blaze or React) it gets hard to do. Using a ‘domain’ or as React’s Flux calls them ‘stores’ helps abstract the state out of the component:

Something like this but with a reactive dict or var

Session.set('PostsDomain:state', []);

PostsDomain = {

  getPosts() {
    return Session.get('PostsDomain:posts');
  },

  addPost(data) {
    .....
    return Session.set('PostsDomain:posts', newData);
  }

  ....
};

#7

I have a more through example app of this ‘Domain’ mentioned above here (also an example app):

@patrickjmorris You could use something like this to store onboarding state without having to wrangle sharing different views.

Would love your thoughts @awatson1978 !


#8

Hi!
Just took a close look at the Flux for Meteor using Blaze or React thread. It’s very nicely done. Looking through it, my thoughts were along the lines of “well, yes. Of course. Why is this even an issue? This seems a bit like rediscovering the wheel.”

So, I suppose my take on the Flux domain object approach is that there’s a bit of a naming convention and communications issue that’s going on. Like, what you’re proposing… I personally know the pattern you’re advocating for simply as an ‘isomorphic object’. And your domain objects are, in my mind, what naturally happens when somebody uses Meteor and follows the Rule of Three. So, we’re using the same patterns, but calling them by different names.

I think what’s happening is that there are enough apps out in the wild that people are starting to recognize that particular isomorphic domain object as a best practice. It loosely de-couples functionality from the templates, and makes functionality easy to test; easy to debug; easy to organize, and easy to reason about. And that’s letting people start to build higher-level APIs. And instead of talking about ‘refactored isomorphic objects’ or whatnot, it’s easier to just coin the term ‘Flux’.

So, yeah. I’m all in favor of documenting this as a best-practice and general pattern. I can certainly add it to the cookbook, if you’d like. Also, the new starrynight documentation is coming along nicely, and I think adding unit test and tiny test examples using this Flux domain model would be very useful documentation.

$0.02


#9

Thanks for checking it out!

Lol, perhaps :slight_smile: It started from using Alt (Flux) and taking away the event emitters that Meteor doesn’t need and then dropping the dispatcher because we don’t have store events.


[quote="awatson1978, post:8, topic:4896"] So, I suppose my take on the Flux domain object approach is that there's a bit of a naming convention and communications issue that's going on. [/quote]

Yea Flux is 90% convention but it typically has some unique attributes. It’s geared around being able to re-render the whole page efficiently. So if anything in a store changes (from an action) it just emits a ‘change’ event and every view listening to the store does a full re-render. In re-render React this is pretty cheap.

I suppose you could add a reactive Store.listenForChange() method that just returns a reactive var and stores could trigger a change by updating this var. However just using the normal reactive data is prob. the easiest path and would work best for Blaze.

Another thing used often is immutable data in the store so that you can do a === comparison on an object so that didComponentUpdate can cancel rendering on all children components.

Some of the newer Flux frameworks also can serialize the store state and save it, allowing you to literally replay a client’s buggy session!

Here’s a nice short article on the basics of Flux stores: http://jaysoo.ca/2015/03/09/on-flux-stores-and-actions/


[quote="awatson1978, post:8, topic:4896"] So, yeah. I'm all in favor of documenting this as a best-practice and general pattern. I can certainly add it to the cookbook, if you'd like. [/quote]

Cool, I should have it nailed down pretty soon!


#10

Just read through the Jack Hsu article. Notes below:

  • The concept of Actions is good, and works very nicely with the MultiActor pattern that’s being developed for Clinical Meteor. I’m all in favor of a more formalized Actions pattern.
  • Storing ITEM_REMOVED in the Action id field instead of a type field is going to create confusion, since Mongo records have _id fields by default. Plus, moving ITEM_REMOVED to type would allow creating a collection of Actions, for replay purposes.
  • Not a huge fan of the term ‘Stores’. It’s a bit too much slang, and confuses with commerce systems. ‘Datastore’ would maybe be better. But it’s maybe a moot point, since it seems like Store and Collection are synonymous.
  • Would be nice to see the following as a pattern for extending Collections, similar to dburles:collection-helpers.
    class RecentlyRemovedItemCollection extends Collection {
      constructor(Dispatcher) {
        this._removedItems = [];
        Dispatcher.register('ITEM_REMOVED', this.onItemRemoved);
      }
    
      onItemRemoved(item) {
        this._removedItems.push(item);
        this.emit('change');
      }
    
      recentlyRemovedItems() {
        this._removedItems;
      }
    }
  • Immutable Actions are extremely similar to what’s implemented in the clinical:hipaa-audit-log package. Specifically, HPAA audit events can neither be updated nor destroyed. Which is the core concept of any regulatory logging… HIPAA, SOX, etc.

  • Would be interesting to ask MDG if the technical spec for Collections is to be idempotent. If so, they’re definitely synonymous Stores. Otherwise, that may be the compelling reason to implement a separate Store or Datastore object.


#11

Thanks for the input!

Yea his version is a little different than the norm. Usually it’s an ‘actionType’ key such as this:

var TodoActions = {

  create: function(text) {
    AppDispatcher.handleViewAction({
      actionType: TodoConstants.TODO_CREATE,
      text: text
    });
  },
  ...
}

I think this is mostly for the dispatcher to use since it’s aggregating all of the actions coming in. In the example I created I don’t think this would be necessary at all.


I agree. The only pro is that it’s something that current Flux users can relate to. However since we’re not storing data in a static array, perhaps moving to FooDomain would be best even for React devs.

The cool part is that if your PostsDomain.getAll() is returning a reactive data source, all of the event stuff in a typical Flux Store is not needed.

Transient state can be stored in a Reactive Dict, Var or Session and then again no emitter is needed :smile: Then of course Blaze templates will just auto rerun and the data mixin for React allows for the same.


#12

actionType is exactly right. Run with that. That works well.


#13

I did something similar when playing with Vue.js. I created a shared store so multiple instances could bind to a single source of truth.