Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Knockout-JS Multi-Step Form with Validation

Looking for a sanity check here. I've recently started learning knockout, and have been instructed to convert an existing multi-step form.

The basic idea is to validate each step before allowing the user to continue. There are also certain restrictions set up (not shown) that determine whether to continue onward or submit using all of the current data (ex: if they don't qualify).

Here is a fiddle with a simplified version (the actual form contains about 40 fields over 4 steps)

http://jsfiddle.net/dyngomite/BZcNg/

HTML:

<form id="register">
 <fieldset>
      <h2>About You</h2>
    <ul>
        <li>
            <label for="firstName">First Name:</label>
            <input type="text" data-bind="value: firstName" required="required" />
        </li>
        <li>
            <label for="lastName">Last Name</label>
            <input type="text" data-bind="value: lastName" required="required" />
        </li>
    </ul>
 </fieldset>
 <fieldset>
     <h2>Your Business</h2>

    <ul>
        <li>
            <label for="businessName">Business Name:</label>
            <input type="text" data-bind="value: businessName" required="required" />
        </li>
        <li>
            <label for="currentCustomer">Were you referred by someone?</label>
            <input type="checkbox" data-bind="checked: referred" />
        </li>
    </ul>
</fieldset>
<fieldset>
     <h2>User Info</h2>

    <ul>
        <li>
            <label for="userName">Referrer's First Name:</label>
            <input type="text" data-bind="value: referralFirst" required="required" />
        </li>
        <li>
            <label for="password">Referrer's Last Name:</label>
            <input type="password" data-bind="value: referralLast" required="required" />
        </li>
    </ul>
  </fieldset>
 </form>
<div class="nav-buttons"> <a href="#" data-bind='click: stepForward'>Continue</a>
    <a href="#" data-bind='click: stepBack'>Back</a>
    <a href="#" data-bind='click: resetAll'>Cancel</a>
 </div>

JS:

 $("#register").children().hide().first().show();

ko.validation.init({
   parseInputAttributes: true,
   decorateElement: true,
   writeInputAttributes: true,
   errorElementClass: "error"
});

function myViewModel() {

var self = this;

//observable init
self.firstName = ko.observable();
self.lastName = ko.observable();
self.businessName = ko.observable();
self.referred = ko.observable();
self.referralFirst = ko.observable();
self.referralLast = ko.observable();

//validaiton observable init
self.step1 = ko.validatedObservable({
    firstName: self.firstName,
    lastName: self.lastName
});

self.step2 = ko.validatedObservable({
    businessName: self.businessName,
    referred: self.referred
});

self.step3 = ko.validatedObservable({
    referralFirst: self.referralFirst,
    referralLast: self.referralLast
});

//navigation init
self.currentStep = ko.observable(1);

self.stepForward = function () {
    if(self.currentStep()<4){
        self.changeSection(self.currentStep() + 1);
    }
}

self.stepBack = function () {
    if (self.currentStep() > 1) {
        self.changeSection(self.currentStep() - 1);
    }
}

self.changeSection = function(destIdx){
    var validationObservable = "step" + self.currentStep();
    if(self[validationObservable]().isValid()){
        self.currentStep(destIdx);
        $("#register").children().hide().eq(self.currentStep() - 1).show();
        return true;
    }else{
        self[validationObservable]().errors.showAllMessages();
    }
    return false;
}

self.resetAll = function(){
    //TODO
    return false;
}

}

 ko.applyBindings(new myViewModel());

My questions:

  1. Does it make sense to declare all of the fields initially as observables and then cluster them together into validatedObservables() ?

  2. If at the end I want to submit the entire form, is there a smarter way of accomplishing this than concatenating each step using ko.toJSON(self.step1()). Would I need to create a "full form" observable that contains all of the input observables? In other words, what's the best way to serialize the full form? Would I want to use ko.toJSON(self) ?

  3. What's the best way to reset the form to the initial configuration? Is there a way of re-applying ko.applyBindings(new myViewModel()) ?

Am I going about this correctly?

Thanks for any clarification.

like image 489
lyma Avatar asked May 23 '13 19:05

lyma


1 Answers

It's a good start. I suggest you manage visibility using knockout and turn to jQuery only when there is no other option. By that I mean managing visibility of fieldsets:

<fieldset data-bind="visible: currentStep() === 1">
  1. Yes, it does make sense to have all fields as observables initially. Good strategy is to get your data as JSON from server and use mapping plugin to convert everything to observables. If you prefer to code everything by hand, that's OK.

  2. At the end just submit whole view model: ko.toJSON(self) will do the job serializing it to JSON. You may want to convert it to JS object: ko.toJS, then cleanup data that you don't want to submit (e.g. lookup data and etc.) and then use JSON.stringify to convert to JSON.

  3. It's hard to reset validation state using validation plugin. To reset the form, just remove existing form from the DOM and applyBindings on the new HTML. Keep HTML somewhere handy on the page:

To reset form then do:

<script type="text/html" id="ko-template">
   <form id="register"> 
   ...
   </form>
</script>

<div id="context"></div>

JavaScript:

var template = $('#ko-template').html();

$('#context').empty().html(template);

ko.applyBindings(new myViewModel(), document.getElementById('context'));

Form tag is not necessary in this case, since you manage everything using JS objects.

like image 133
Tomas Kirda Avatar answered Sep 28 '22 08:09

Tomas Kirda