Build a Contact Us Form: Part 1

This post applies to Vinson and Elbrus. It could be retro-fitted to older versions (swap the service controller for a service file), and to newer versions (change the Sass variable names and classes).

NOTE — this was updated on October 16, 2017 - the try block in the backend model was modified to catch additional status codes (without these, you may have gotten a JSON.parse error)

Being able to directly contact the administrators of a site is often considered a baseline feature for any site, regardless of whether it is an ecommerce site or not. While we don't include a public-facing one out of the box in SuiteCommerce, we do support the ability for customers to contact administrators via the my account application.

For whatever reason, you may not think this goes far enough: why should a customer have to be registered and signed in before they can contact you? Surely, if a customer has to do all of those extra steps just to send you a message, they'd sooner switch to another site than have to sign up? Maybe.

So, to address this: you can also add a new way for (anonymous) shoppers to contact you. While this will use the case functionality in NetSuite, you won't obviously be able to respond to them through it — you'll have to follow up with an email. But that's expected, right? Using some standard Backbone code and some SuiteScript, you can create a neat little interface between a case form and the frontend of your site.

This is a two part tutorial: in this article, we're going to build the barebones of the module so that, by the end of it, you'll be able to create case records via a form on the frontend. The second part of the tutorial will look at things like validating data and improving the user experience.

Let's do this!

Backend Setup and Basic Module Preparation

By now, you should know the score: we start as we always do with some basic file structure.

In your customizations directory, create a folder named ContactUs@1.0.0 and in it put folders for Configuration, JavaScript, Sass, SuiteScript and Templates. Then create an ns.package.json file with the following in it:


{
  "gulp":
  {
    "configuration":
    [
      "Configuration/*.json"
    ]
  , "javascript":
    [
      "JavaScript/*.js"
    ]
  , "sass":
    [
      "Sass/*.scss"
    ]
  , "ssp-libraries":
    [
      "SuiteScript/*.js"
    ]
  , "templates":
    [
      "Templates/*.tpl"
    ]
  }
}

Now we need to create an entry point file. In JavaScript, create ContactUs.js and in it put the following:

define('ContactUs',
[

], function
(

)
{
  'use strict';

  return {
    mountToApp: function(application)
    {
      console.log('ContactUs.js loaded')
    }
  }
});

Finally, you need to update distro.json:

  1. Register the module at the top of the file
  2. Add its JavaScript to shopping.js
  3. Add its Sass to shopping.css

That's it. Save and deploy and you'll see a console message saying that the module loaded!

Online Case Form Setup

As mentioned, this functionality relies on the cases functionality. In NetSuite, it's possible to create forms that can have data submitted to them via our APIs. This is what we're going to do for the contact form.

In NetSuite, go to Setup > Support > Online Case Forms. Click New Online Case Form and select Default Form Template. Set the title as "Contact Us" and then set up the Select Fields as follows:

  • Email
  • Incoming Message
  • Subsidiary
  • Title
  • First Name
  • Last Name

All fields, except for Subsidiary, are mandatory.

Save and then edit the form. Take a look at the External tab: we have URLs for internal and external use — keep this tab open, we're going to need these in a moment.

Configuration Setup

To speed things along, we're going to do the configuration stuff now.

In the Configuration folder, create ContactUs.json and in it put the following:

{
  "type": "object"

, "subtab":
  {
    "id": "contactUs"
  , "title": "Contact Us"
  , "group": "shoppingApplication"
  }

, "properties":
  {
    "contactUs.enabled":
    {
      "group": "shoppingApplication"
    , "subtab": "contactUs"
    , "type": "boolean"
    , "title": "Enabled"
    , "default": false
    }

  , "contactUs.formId":
    {
      "group": "shoppingApplication"
    , "subtab": "contactUs"
    , "type": "string"
    , "title": "Form ID"
    }

  , "contactUs.hash":
    {
      "group": "shoppingApplication"
    , "subtab": "contactUs"
    , "type": "string"
    , "title": "Generated Hash"
    }
  }
}

Here we're adding in three new settings for our site:

  1. Enabled — a simple toggle we'll use to enable/disable the functionality via the configuration screen
  2. Form ID — the ID of the form, which is where you paste the integer after the formid parameter in the URL for the Internal Form URL
  3. Generated Hash — the hashed ID of the form, which is where you paste the hash after the h parameter in the URL for the Publishable Form URL

Deploy this file. When it's done, head over to Setup > SuiteCommerce Advanced > Configuration and edit the record for your site. In the Shopping tab, you should see your fields in the Contact Us subtab; complete them using the information above. For example, mine now looks like this:

Great! Now let's going with some coding.

Barebones: Template to Router

Let's get the essential parts of the site in place. The end goal is that when a user visits a particular URL, a page shows with some text on it. For this we'll need a router (to control the URLs), a view (something for the router to call) and a template (a page for the view to render). We'll need a frontend model and backend services later, but for now let's stick to the core stuff.

In the Templates directory, create contact_us.tpl and in it put the following:

<p>Soon you will be able to contact us!</p>

This is all we need for now.

Next, in JavaScript, create ContactUs.View.js and put the following in:

define('ContactUs.View'
, [
    'Backbone'
  , 'contact_us.tpl'
  ]
, function
  (
    Backbone
  , contactUsTpl
  )
{
  'use strict';

  return Backbone.View.extend({
    template: contactUsTpl
  });
});

So all we're doing is telling it to extend the standard Backbone view and render the template we just created.

Next, we need to create the router so that it's possible to get to a point where we can view our wonderful new view and template.

In JavaScript, create ContactUs.Router.js and in it put the following:

define('ContactUs.Router'
, [
    'Backbone'
  , 'ContactUs.View'
  ]
, function
  (
    Backbone
  , View
  )
{
  'use strict';

  return Backbone.Router.extend({
    routes:
    {
      'contact-us': 'contactUs'
    }

  , initialize: function(application)
    {
      this.application = application;
    }

  , contactUs: function(options)
    {
      var view = new View({
        application: this.application
      });

      view.showContent();
    }
  });
});

A basic router so far: create the routes and tell it to create a new instance of our view once the route is called.

The final part is to update the entry point file so it knows the router exists and that it should create a new instance of it when the application is loaded.

Replace the contents of ContactUs.js with:

define('ContactUs'
, [
    'ContactUs.Router'
  , 'SC.Configuration'
  ]
, function
  (
    Router
  , Configuration
  )
{
  'use strict';

  return {
    mountToApp: function(application)
    {
      var enabled = Configuration.get('contactUs.enabled');

      if (enabled)
      {
        return new Router(application);
      }
    }
  }
});

So what we're doing is checking if the functionality has been enabled in the backend (if you haven't already, go and tick the checkbox!) and, if it is, create a new instance of our router.

With all that in place, run gulp local and hit your local site, appending #contact-us to the URL. When it loads, you should see something like this:

Create the Frontend Model

Next up, we need to create a frontend model. Remember, this is the thing that will take data (in this case from a form) and connect with the backend model and service controller so the site can communicate with the backend.

In JavaScript, create ContactUs.Model.js. In it, put:

define('ContactUs.Model'
, [
    'Backbone'
  , 'underscore'
  , 'Utils'
  ]
, function
  (
    Backbone
  , _
  , Utils
  )
{
  'use strict';

  return Backbone.Model.extend({
    urlRoot: Utils.getAbsoluteUrl('services/ContactUs.Service.ss')
  });
});

There's not much to say here other than we've referenced a service that doesn't exist, so we should probably deal with that in a moment.

Before we do that, we just need to update the router so that when we create the view when the route is called, the model is included.

Head back to ContactUs.Router.js and add our newly created model as a dependency.

Then, add the model as a property to the view declaration in the contactUs function:

var view = new View({
  application: this.application
, model: new Model()
});

This'll tell it to include the model when the view is created.

Add the Form

OK, we added the model and we referenced a non-existent backend service, which we'll get to later, but first let's create the interface for capturing the user's data.

For this part of the tutorial, we're going to stick to basics: we can add in extra markup for styling and organization later.

What we need to do is two-fold:

  1. Create a template that has a form in it that is sufficiently marked up so that the user's data can be mapped to the fields we set up in the backend
  2. Modify the view so that it understands that mapped data and can send it on to the model

So let's create a simple form. Replace the contents of contact_us.tpl with:

<section>
    <form>
        <fieldset>
            <label for="firstname">First Name</label>
            <input name="firstname" type="text" id="firstname">

            <label for="lastname">Last Name</label>
            <input name="lastname" type="text" id="lastname">

            <label for="email">Email</label>
            <input name="email" type="text" id="email">

            <label for="title">Subject</label>
            <input name="title" type="text" id="title">

            <label for="incomingmessage">Message</label>
            <textarea name="incomingmessage" type="text" id="incomingmessage"></textarea>
        </fieldset>

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

As I said, simple — we can mark it up later.

Next we need to add to our view. Open up ContactUs.View.js. The first thing we need to do is add Backbone.FormView as a dependency, so do that.

With it as a dependency, we need to add an initialize method that adds this view as an instance of it. Add the following:

initialize: function(options)
{
  this.options = options;
  this.application = options.application;

  BackboneFormView.add(this);
}

With the standard form view initialized, we can make use of it. Next, bind the submission of the form with the standard saveForm function (which we've built to handle the submission of forms):

events:
{
  'submit form': 'saveForm'
}

This'll listen for when that submit button is clicked and then process the data.

If you want to, you can stop and restart your local server and see the form load. If you want, you can try submitting the form — if you do, you should get an error that says something like the following:

And, with that, we're temporarily done with frontend stuff, so let's take a look at the backend service stuff.

The Service Controller

Remember, when dealing with the backend stuff, it comes in two parts: a model and a service controller. The service controller is used to generate a service so that we don't have to. As most services are very similar, and really only differ in terms of the model they call, they are typically quite simple. Let's write the service controller and set it up.

In SuiteScript, create ContactUs.ServiceController.js and in it put:

define('ContactUs.ServiceController'
, [
    'ServiceController'
  , 'Application'
  , 'ContactUs.Model'
  ]
, function
  (
    ServiceController
  , Application
  , ContactUsModel
  )
{
  'use strict';

  return ServiceController.extend({
    name: 'ContactUs.ServiceController'

  , post: function()
    {
      this.sendContent(ContactUsModel.create(this.data));
    }
  });
});

This is pretty uncontroversial and should be pretty clear about what's going on: when the POST method is called, we send the data to the model which will call the create method.

If you're new to service controllers, you'll note that we no longer call Application.sendContent (instead we now call this.sendContent). This was one of the changes when we moved over to having one extendable service controller — this method is now contained within SspLibrariesServiceController.js, which we list as a dependency.

The final part of this, is that we need to tell the application that this service controller exists. We need to do this in two places:

  1. ns.package.json — we must state that we're going to be automatically generating a service file for this module
  2. distro.json — we must state this service controller exists, and must be included with the other service files

So, first, edit ns.package.json and add in an object stating how we're autogenerating the service file, ie:

"autogenerated-services":
{
  "ContactUs.Service.ss": "ContactUs.ServiceController"
}

Next, we need to modify distro.json. Jump to ssp-libraries and add "ContactUs.ServiceController" to the dependency list.

Done. Now, remember that the model we're referencing in our service controller is not the frontend model that we've already created, but a backend model that we're yet to make. So let's do that now.

The Backend Model

The backend model is the thing that handles the data before sending (or receiving) it to the server. What we need it to do is take the form data, process it and then submit it via the online case form we created.

To make things a little easier, I'm going to dump the entire code snippet and then explain it.

In SuiteScript, create ContactUs.Model.js and in it put:

define('ContactUs.Model'
, [
    'Models.Init'
  , 'SC.Model'
  ]
, function
  (
    CommerceAPI
  , SCModel
  )
{

  'use strict';

  return SCModel.extend({

    name: 'ContactUs'

  , create: function(data)
    {
    // Create a bunch of useful variables
      var configuration
      , currentDomain
      , currentDomainMatch
      , request
      , url;

      // Get the config options for the functionality
      configuration = SC.Configuration && SC.Configuration.contactUs;

      // Get the URL for the secure login domain
      currentDomainMatch = CommerceAPI.session.getSiteSettings(['touchpoints'])
      .touchpoints.login
      .match((/^https?\:\/\/([^\/?#]+)(?:[\/?#]|$)/i));

      currentDomain = currentDomainMatch && currentDomainMatch[0];

      // Build the request URL
      url = currentDomain
      + 'app/site/crm/externalcasepage.nl?compid=' + nlapiGetContext().getCompany()
      + '&formid=' + configuration.formId
      + '&h=' + configuration.hash
      + '&globalsubscriptionstatus=1';

    // Include subsidiary data, if relevant
      if (CommerceAPI.context.getFeature('SUBSIDIARIES'))
      {
        data.subsidiary = CommerceAPI.session.getShopperSubsidiary();
      }

    // Make the request!
      try {
        var response = nlapiRequestURL(url, data);
        var responseCode = parseInt(response.getCode(), 10);
        // Just in case someday it accepts the redirect. 206 is netsuite error ('partial content')
        if (responseCode === 200 || responseCode === 302 || responseCode === 201 || responseCode === 404) {
          return {
              successMessage: 'Thanks for contacting us'
          }
        }
      }

      // Unfortunately, even successfully submitting data this way results in an exception, so we need to handle that
      catch(e)
      {
        // The 'successful' exception is a redirect error, so let's intercept that
        if (e instanceof nlobjError && e.getCode().toString() === 'ILLEGAL_URL_REDIRECT')
        {
          return {
            successMessage: 'Thanks for contacting us'
          }
        }

        // Finally, let's catch any other error that may come
        return {
          status: 500
        , code: 'ERR_FORM'
        , message: 'There was an error submitting the form, please try again later'
        }
      }
    }
  });
});

There's a fair bit going on, which I've broken down with code comments. Let's take a look:

Dependencies

We're obviously including the standard model that we're extending, but we're also including SuiteScript > Models.Init.js. This file contains a whole bunch of useful global variables for use within backend services.

Variables

I don't know about you, but I like to declare variables upfront so that I have all the pieces ready when it comes to build what I want. The configuration variable is going to be used to pull out the form ID and hash, which we set earlier.

On the subject of the ID and hash, do you remember that we plucked them from URLs? Well, what we're basically doing now is reconstructing that URL. While the forms subdomain is not available, we can substitute it for checkout one and it'll still work fine. To do this, we need to pluck it from the list of touchpoints and, for that, we're going to use a regex.

Then we start building the URL. You'll note how we build it up from the currentDomain variable and then start attaching URL parameters. We need the company ID, the form ID, and the hash. We also use globalsubscriptionstatus to indicate that we do not want to update any user records.

Next we pull in the subsidiaries information, if available, and then plug it into the data.

Request and Response

Finally, we make the request. For this we use nlapiRequestURL, an application navigation API. The intended use of these kinds of APIs is to redirect users to a webpage; here we're running it serverside, and can use it to request the form, enter data into it, and then submit it.

The downside to this is that doing it this way is technically incorrect, and will lead to an exception being thrown, even when successful. Thus, the next part is intercepting it and checking it to see if it's the exception we're expecting for 'success'; if it's not, then we can return a standard error message.

Deploy and Test

If you've gotten this far then you've done all the work required to get the bare minimum set up, so you're now able to deploy and test. Visit #contact-us and you should see an ugly looking form. Fill it in with some test data:

When you submit the form, it should go through and create a case record in NetSuite, which you can check by going to Lists > Support > Cases:

If it doesn't go through, then check the request payload in the header for the service. In other words, open up the developer console in your browser and (for Chrome) go to the Network tab > XHR subtab > ContactUs.Service.ss > Headers > Request Payload and see what's being sent. Double-check that the keys are exactly the same as the names we use in NetSuite.

Another common problem is that you get an error that says "JSON.parse: Uncaught SyntaxError: Unexpected token u" — what's happening here is that JSON.parse is attempting to parse the value "undefined". Which means, after you made the request, the response from the server was "undefined". In my experience, this is usually because I messed up something in the backend model. So, if I were you, I would look there for issues, such as typos in the URL builder and syntax problems.

And, with that, we have the barebones of a contact form.

Final Thoughts

I think building up a module via a skeleton and testing regularly provides a way of ensuring that what you have works, before you increase the complexity. If you go into complicated stuff straight away then it might be quicker, but errors may be harder to find and correct.

What we have with this tutorial are the core essentials for creating a form on the frontend, connecting to the backend of NetSuite, so that we can create case records based on the submitted information.

There's still plenty of work to do, but this is only part 1 of the tutorial. In the next part we'll look at fleshing this out, including:

  • Validating the data, both on the front and backends
  • Adding feedback
  • Styling the form
  • Adding common page furniture (eg page title, breadcrumbs, etc)

There'll be a few other bits and bobs that you might like.

In the meantime, here is a copy of my source code so far, if you want to compare: ContactUs@1.0.0-part1.zip.

Part two of this tutorial can be found here: Build a Contact Us Form: Part 2.