Build a Contact Us Form: Part 2

This is the final part of a two-part tutorial on how to build a contact us form in SuiteCommerce Advanced. By the end of the first part of the tutorial, we had created the form in NetSuite, created a module in our code and connected the two together. This allowed us to submit data from the frontend and have it create a record in the backend.

In this part, we're going to flesh things out and add in all the things that should be part of the module but aren't strictly necessary for it to work. Most of these additions will be aesthetic ones, so I hope they give you ideas about what you want you can do to your own site.

Validation

Maybe I should have included this in the previous tutorial: on the one hand, validating data before it is sent to the server isn't strictly necessary but, on the other, it's such a good idea that it's a no-brainer.

Remember, we add in validation in two places: first on the frontend and then on the backend. Frontend validation ensures a speedy summary response about the user's data, without wasting server time; whereas backend validation ensures that the data is in a valid format, so we can cut down wasted attempts to create/update records.

Validation is built into SuiteCommerce Advanced, so it's just a case of making some small changes to your code to take advantage of it.

Frontend Validation

Let's start by taking a look at frontend validation.

I've talked about adding frontend validation in numerous articles before but, for the sake of completeness, let's walk through it with our new module.

Frontend validation is added to the frontend model. We've built it into Backbone (via a third-party library), so we do not need to add any dependencies. To add validation, we need add a validation object to the return statement in JavaScript > ContactUs.Model.js.

For each field we want to validate, we then add another object with key-value pairs detailing the type of validation we want to apply (key) and how we want it to be validated (value). Thus, in our model, add the following:

, validation:
  {
    firstname:
    {
      required: true
    , msg: 'Please enter a first name'
    }
  , lastname:
    {
      required: true
    , msg: 'Please enter a last name'
    }
  , email:
    [
      {
        required: true
      , msg: 'Please enter an email address'
      }
    , {
        pattern: 'email'
      , msg: 'Please enter a valid email address'
      }
    ]
  , title:
    {
      required: true
    , msg: 'Please write a subject'
    }
  , incomingmessage:
    {
      required: true
    , msg: 'Please write a message'
    }
  }
});

Because the data we're submitting is just a number of text strings, there isn't much to validate other than the fact it exists at all. However, note the email field: we both require it and expect it to conform to a pattern that is an email address.

Each field can have multiple rules applied to it, each with its own message. To do this, you can just create an array of objects for a field rather than going straight for a single object. Alternatively, if you don't want to have a different message, you can just have the rules in one object. The decision comes down to whether you wish to have a message for each rule, or whether you're content with the same message being displayed both times.

You can find a full list of validation rules available to you by inspecting Modules > third_parties > backbone.validation > backbone-validation.js, or by reading their documentation.

Anyway, simply adding rules isn't enough. I mean, if you run your local server now and break one of these rules then your form won't submit and trigger a request to the server (so that works) but we have no feedback to the user about any of this. They won't know, and might assume it went through. Let's add in some feedback to the template.

Modify contact_us.tpl and replace it with the following:

<section>
    <form>
        <fieldset>
            <div data-input="firstname" data-validation="control-group">
                <label for="firstname">First Name</label>
                <span data-validation="control">
                    <input name="firstname" type="text" id="firstname">
                </span>
            </div>
            <div data-input="lastname" data-validation="control-group">
                <label for="lastname">Last Name</label>
                <span data-validation="control">
                    <input name="lastname" type="text" id="lastname">
                </span>
            </div>
            <div data-input="email" data-validation="control-group">
                <label for="email">Email</label>
                <span data-validation="control">
                    <input name="email" type="text" id="email">
                </span>
            </div>
            <div data-input="title" data-validation="control-group">
                <label for="title">Subject</label>
                <span data-validation="control">
                    <input name="title" type="text" id="title">
                </span>
            </div>
            <div data-input="incomingmessage" data-validation="control-group">
                <label for="incomingmessage">Message</label>
                <span data-validation="control">
                    <textarea name="incomingmessage" type="text" id="incomingmessage"></textarea>
                </span>
            </div>
        </fieldset>

        <button type="submit">Submit</button>
    </form>
</section>

Run your local server and go to the page. Hit submit on a blank form and see the page light up! Ever field should throw a validation error message and, most importantly, you shouldn't see a XHR to the service in the Network tab. If you put a junk string into the email field and hit submit again, you should see a different error message, eg:

You'll notice that the first invalid input is focussed on. This is something we have programmed into Backbone.Formview.js: it relies on us using data-validation="control-group" in our templates, amongst other things, so make sure you include them in your templates!

That's it for the frontend validation: we successfully block invalid data and prevent calls to the service until it's passes the tests. We also let the user know what's wrong with their data so that they can correct it.

Now let's move onto backend validation.

Backend Validation

Backend validation is still necessary even if you have frontend validation. For starters, there's no real reason not to adopt a 'belt and braces' approach to this, plus there's always a chance that the frontend validation could be disabled, thus allowing invalid data to be sent to the server — and we wouldn't want that.

I actually recently wrote about adding backend validation, so for more detail you can take a look at that (including how to add custom rules). However, for the purposes of this tutorial, let's just go over what we need for this functionality.

Validation for SCA SuiteScript data uses a modified version of the same validation scripts we use on the frontend. The first step is to add the validation rules, which we can just copy and paste from the frontend model and add as a method to the extended backend model.

In SuiteScript > ContactUs.Model.js, copy and paste the validation object into the return statement.

Then, at the top of the create method, add in:

this.validate(data)

We run this first so that we check the data before doing anything with it; if it's bad then the code stops and dumps out an error.

Finally, so that we get error messages back from the server and show them to the user, we need to update the template again.

Open contact_us.tpl and put this at the bottom, below the submit button:

<div data-type="alert-placeholder"></div>

Save and deploy.

Now, while we still have frontend validation rules in effect, we can't test the backend ones because the frontend ones fire first and will catch any invalid data. Thus, we need to temporarily disable the frontend validation. The easiest way, in my mind, to do this is to comment out the validation object from the frontend model. Do that, and then restart your local server.

Now when you try to submit, you'll get an error back from the service. You'll get a generic "internal error" in the alert placeholder div we created, but if you inspect the response from the service in your developer tools, you'll see the error messages we coded into the validation. For example, if I complete every field except for the message, I'll get the following:

Clearly, the next steps is to get all returned server (error and success) messages shown on the page.

First let's build a skeleton to illustrate how this is done. Head to ContactUs.View.js and add jQuery and underscore as dependencies.

Then modify the events object so that submit form points to a new function called saveTheForm.

Next we need to make this function. Add the following method:

, saveTheForm: function(e)
  {
    var self = this;
    var promise = BackboneFormView.saveForm.apply(this, arguments);

    e.preventDefault();

    return promise && promise.then
    (
      function(success)
      {
        if (success.successMessage)
        {
          self.showMessage(success.successMessage, 'success');
        }
        else {
          self.showMessage('An error occured, please try again', 'error')
        }
      }
    , function(fail)
      {
        fail.preventDefault = true;

        _.each(fail.responseJSON.errorMessage, function(message, field)
        {
          self.showMessage(message, 'error', field);
        });
      }
    );
  }

That's right, we're using promises (deferred objects). As a quick summary, promises allow you to bind callbacks to an AJAX call. These callbacks are usually split into two kinds: successful and unsuccessful. By using them here, we can turn the submission of the form into a promise and then tell the view to execute code based on the response from the server.

In order to do this, we're using jQuery's Deferred.then() method, which we've given two parameters: one for success and one for failures. Each of these are functions, which are passed the response from the AJAX call (which you'll remember is the thing we saw in the developer tools). From there, we can write some code to handle the messages.

For now we can write some simple function. Add the following as a method:

, showMessage: function(message, type, field)
  {
    console.log(type + ': ' + message);
  }

Now, save and head back to your local server page. With your console open, submit an empty form you should get back a bunch of logs. Similarly, if you complete all the fields and then submit the form, you should get back a 'success' log. For example:

Now we know that we've made the connection, we can work on improving the user experience of this.

Global Message View

You'll probably know that we have a fancy pants way of adding messaging to the site. For our endeavour, we're going to make use of the global messaging view that creates and styles a child view based on what we supply it. You'll note that we've already set things up so that showMessage is given the message, type and field, so let's put this into practice.

Add GlobalViews.Message.View (MessageView) as a dependency to ContactUs.View.js and then head down to the showMessage function. Replace it with the following:

, showMessage: function(message, type, field)
  {
    var messageView = new MessageView
    ({
      message: message
    , type: type
    });

    if (typeof field !== 'undefined')
    {
      this.application.getLayout().$('[data-input="' + field + '"]').append(messageView.render().$el);
    }
    else
    {
      this.application.getLayout().$('form').append(messageView.render().$el);
    }
  }

So our new function accepts three parameters: message, type and field. We use the message and type to build up what we want to show the user (this could be a success or an error message) and plug that straight into the global message view. Then we perform a check to determine where we should render the message view. If the function was passed a defined value, then we use that as the place to render the message; if it wasn't, then it just puts it at the bottom of the form. Simple.

This results in messaging like the following. On one side we have what shows when a success message is passed to it, and the other when some fields fail validation:

So with that, we have the backend validation working. Make sure, now, that you put back (uncomment) the frontend validation from the model. In truth, we may never need the backend validation if the frontend validation does it job, but it's good to have it.

Automatic Frontend Validation

One extra you can add in, by the way, is automatic frontend validation. This makes the validation fire as soon as the user moves their focus off they were just editing.

This functionality is built into Backbone.FormView.js so it's just a case of adding a bindings object to your view, where the keys are references to fields into templates, and the values are the names of the rules in validation object in the model. Seeing as we already have the template and validation rules set up, we can just plug it in and it'll work.

In ContactUs.View.js put the following:

bindings:
{
  '[name="firstname"]': 'firstname'
, '[name="lastname"]': 'lastname'
, '[name="email"]': 'email'
, '[name="title"]': 'title'
, '[name="incomingmessage"]': 'incomingmessage'
}

Now, whenever you tap onto a field and then tap away, the validation rule for that field will fire. This is great for instant feedback.

Styling

We've pretty much got the entire functionality down now: it works by submitting a form, which creates a case, and we have useful extras attached to it, such as frontend and backend validation, so we know that the data is good. Now we can focus on making it look nice.

A lot of what I'm going to provide in this section is based on my site and its styling, so you won't necessarily be able to copy and paste it into your site. However, it'll be relatively simple so the styling should translate to your site.

We need to start by marking up the template. Replace contact_us.tpl with the following:

<section class="contactus-container">
    <h2>{{translate 'Contact Us'}}</h2>
    <p>{{translate 'Use this form to submit a question or query to us and we\'ll get back to you as soon as possible.'}}</p>

<small class="contactus-required">{{translate 'Required'}}*</small>

    <form class="contactus-form">
        <fieldset>
            <div class="contactus-firstname" data-input="firstname" data-validation="control-group">
                <label for="firstname">{{translate 'First Name'}}<small class="contactus-required">*</small></label>
                <span data-validation="control">
                    <input name="firstname" type="text" id="firstname">
                </span>
            </div>
            <div class="contactus-lastname" data-input="lastname" data-validation="control-group">
                <label for="lastname">{{translate 'Last Name'}}<small class="contactus-required">*</small></label>
                <span data-validation="control">
                    <input name="lastname" type="text" id="lastname">
                </span>
            </div>
            <div class="contactus-email" data-input="email" data-validation="control-group">
                <label for="email">{{translate 'Email'}}<small class="contactus-required">*</small></label>
                <span data-validation="control">
                    <input name="email" type="text" id="email">
                </span>
            </div>
            <div class="contactus-subject" data-input="title" data-validation="control-group">
                <label for="title">{{translate 'Subject'}}<small class="contactus-required">*</small></label>
                <span data-validation="control">
                    <input name="title" type="text" id="title">
                </span>
            </div>
            <div class="contactus-message" data-input="incomingmessage" data-validation="control-group">
                <label for="incomingmessage">{{translate 'Message'}}<small class="contactus-required">*</small></label>
                <span data-validation="control">
                    <textarea name="incomingmessage" type="text" id="incomingmessage"></textarea>
                </span>
            </div>
        </fieldset>

        <div class="contactus-button-container">
            <button class="contactus-button-submit" type="submit">{{translate 'Submit'}}</button>
        </div>
    </form>
</section>

There's quite a few additions to the template here so let's go through them:

  1. I've added a header and an opening paragraph. This is standard stuff for a page like this: you need to introduce it so that people understand what it's for and what they need to do. Sometimes, you can send this to the template via the context object in the view: this is up to you — the prime benefit of serving it from the view would be if you expect the values to change depending on the context. In a contact form like this, there's probably no scope for that, but with other forms there certainly is.
  2. Fields are now marked up as required and there's some explanatory text at the top to indicate this.
  3. Elements have now got classes on them. What I do is canonically add class names to the elements — this gives us descriptive names for each of them, making selectors easy to use and remember.
  4. Text has been changed to use the {{translate}} Handlebars helper. As some of these strings will be unique to the functionality, you'll need to add custom translation text to the dictionary files, if you want to take advantage of multi-language functionality. If you only operate in one language, then you can leave them as strings or use the {{translate}} functionality — untranslatable strings are simply returned as they are, so you may as well use the {{translate}} helper.

From here we can start to add in some Sass.

Forms are commonplace throughout SuiteCommerce Advanced sites and we have a lot of existing styles that you can extend. When it comes to styling the form itself, it's up to you. In the following Sass, I've chosen styles for my site: a good idea for you is to look at other forms and then extend the classes they extend in order to make your form look like it.

Create _contactus-form.scss in your Sass directory and paste in:

.contactus-container {
    @extend .container;
}

.contactus-form {
    margin: $sc-base-margin * 3;
}

.contactus-firstname,
.contactus-lastname,
.contactus-email,
.contactus-subject,
.contactus-message {
    @extend .row;

    label {
        @extend .input-label;
    }

    input {
        @extend .input-large;
    }
}

.contactus-message textarea {
    @extend .input-textarea;
    display: block;
    width: 100%;
}

.contactus-required {
    @extend .input-required;
}

.contactus-button-container {
    @extend .row;
    margin-top: $sc-base-margin * 4;
}

.contactus-button-submit {
    @extend .button-primary;
    @extend .button-medium;
}

As we've added a new page, we'll need to stop and restart our local server so that it's aware of the file. Once you've done that, refresh the page and you should see something like this:

Now, of note in this stylesheet is the fact that we've nested input and label styling in their parents' declarations. I know that some developers are on the fence about whether to do this (ie give each one a class and then style that) but I think it's fine if you consider that these are uniform, repeating elements. Admittedly, this is not the case for the textarea element, so you could go back and follow the advice on this and change it to a class.

Also note how we have a lot of class names that start .input- — this is because we've built these into our base styles, so you can just use them whenever you're working with a form.

Other than that, the rest of the stylesheet is really simple. Let's take a look at a couple more small things that need tidying up.

Attributes, Breadcrumbs and Title

The first thing I want to talk about is the ability to set attributes from the view. When the view is rendered, a single div is created which then contains the rendered template. If you like, you can attach attributes to the div, which may be useful for styling or JavaScript. For example, in ContactUs.View.js, put the following:

attributes:
{
  'class': 'contactus'
}

When you refresh the page and inspect the element, you should see the element has a class on it.

As for breadcrumbs, this is not really necessary for this functionality as it only goes one level deep from the homepage. However, you may find it useful for other projects, or simply include it for familiarity.

Add the following method:

, getBreadcrumbPages: function()
  {
    return [{
      text: _('Contact Us').translate()
    , href: '/contact-us'
    }]
  }

In other projects, if you wish to add an extra level then you can add in an extra object to the array. The order of the objects will determine the order that they appear in the page.

Finally, the page title: nice and simple to put this in place.

title: _('Contact us').translate()

Simple stuff.

Final Thoughts

From here, your form is ready to go: it accepts inputs, validates that data (twice!) and then decides whether to create a record or return error messages. It's styled and has all the bells and whistles that you'd associate with a page, such as a page title and breadcrumbs. From here, all you need to do is add a link to it on your site and you're good to go (eg by editing the footer navigation links in your site's configuration).

If you want to extend the idea further then you could look into perhaps having the form trigger in a modal when it's clicked in the footer, so that users aren't taken away from their current page when they want to contact you. This is something I've talked about before and shouldn't be too difficult to implement.

Another idea I had is that you could embed the contact form as a child view in another page. For example, if you have an in-depth support hierarchy on your site, then you could build a more compact version of the form and embed it at the button of every support page.

Finally, keep in mind that we natively support cases, which can only be opened by the customer logging into their account. Generally, it's better that customers use this functionality, so you may want to encourage users to log in and use that instead.

For a final collection of annotated files, see ContactUs@1.0.0-part2.zip.