Build a Testimonials Module: Part 2

This is the second part in a series of articles on building a testimonials module on an SCA site. Before following this article, you should read part one first.

In this part we're going to look at the following:

  1. Creating the first version of the form that will collect user's submissions
  2. Working with models and validation to begin to verify the data and interface with the backend
  3. Implement a service that sends the submitted data to NetSuite, and can get it back

In short, this is a tutorial that will focus a lot on the data aspect of the module.

Build the Form

In order to get people to submit their testimonials to us, we need to build a form. Things like forms go in templates, which is the HTML we use to build the final page that the user sees.

Open up testimonials_form.tpl and replace its contents with the following:

<div class="testimonials-form">
    <header class="testimonials-form-header">
        <h1 class="testimonials-form-title">
            {{translate 'New Testimonial'}}
        </h1>
    </header>
    <div class="testimonials-form-content">
        <form id="testimonials-form-new" class="testimonials-form-new">
            <fieldset class="testimonials-form-fieldset">
                <div data-type="alert-placeholder"></div>

                <div class="testimonial-form-content-groups">
                    <div class="testimonials-form-content-group" data-validation="control-group" data-input="writerName">
                        <label class="testimonials-form-content-group-label" for="text">{{translate 'Your Name'}} <span class="testimonials-form-content-required">*</span></label>
                        <div class="testimonials-form-controls" data-validation="control">
                            <input type="text" class="testimonials-form-content-group-input" id="writerName" name="writerName" maxlength="50" value="{{writerName}}">
                        </div>
                    </div>
                    <div class="testimonials-form-content-group" data-validation="control-group" data-input="rating">
                        <label class="testimonials-form-content-group-label" for="rating">{{translate 'Rating'}} <span class="testimonials-form-content-required">*</span></label>
                        <div class="testimonials-form-controls" data-validation="control">
                            <input type="number" min="1" max="5" class="testimonials-form-content-group-input" id="rating" name="rating" value="{{rating}}">
                        </div>
                    </div>
                    <div class="testimonials-form-content-group" data-input="title" data-validation="control-group" data-input="title">
                        <label class="testimonials-form-content-group-label" for="title">
                            {{translate 'Title'}} <span class="testimonials-form-content-required">*</span>
                        </label>
                        <div class="testimonials-form-controls" data-validation="control">
                            <input type="text" class="testimonials-form-content-group-input" id="title" name="title" maxlength="255" value="{{title}}">
                        </div>
                    </div>
                    <div class="testimonials-form-content-group" data-validation="control-group" data-input="text">
                        <label class="testimonials-form-content-group-label" for="text">{{translate 'Write your testimonial'}} <span class="testimonials-form-content-required">*</span></label>
                        <div class="testimonials-form-controls" data-validation="control">
                            <textarea id="text" class="testimonials-form-content-group-text" name="text">{{text}}</textarea>
                        </div>
                    </div>
                </div>
            </fieldset>
            <div class="testimonials-form-actions">
                <button type="submit" class="testimonials-form-actions-button-submit">{{translate 'Submit'}}</button>
            </div>
        </form>
    </div>
</div>

 

It looks like there's a lot here but in reality there isn't; it really is just a simple form. However, we've also marked it up with classes for styling. Shortly, we're going to add some Sass that applies styles to these selectors, so we may as well add them now. Have you noticed how canonical we've made the class names? As we nest elements we build out the classes so that they are semantic. This is good practice: it means we're able to identify exactly where we are applying our styles.

Style the Form

So we've marked up the template with classes, so let's make use of them.

In your Sass directory, create _testimonials-form.scss and in it put the following:

.testimonials-form {
  @extend .container;
  background: $sc-color-theme-background-light;
  padding: $sc-base-padding * 3;

}
.testimonials-form-content {
  @extend .row;

  @media (min-width: $screen-md-min){
    margin: 0;
    @extend .col-md-12;
  }
}
.testimonials-form-content-title {
  @extend .input-label;
  margin-top: $sc-base-margin * 4;
}
.testimonials-form-content-label-required {
  @extend .input-optional;
  display: block;
}
.testimonials-form-content-required {
  @extend .input-required;
}
.testimonials-form-content-rating {
  @extend .clearfix;
}
.testimonials-form-content-group {
  clear: both;
  padding: 0;

  .testimonials-form-help {
    margin-top: $sc-base-margin;
    margin-bottom: $sc-base-margin;
  }
}
.testimonials-form-content-groups {
  padding: 0;
  float: left;
  clear: both;
  margin-bottom: $sc-base-margin * 3;

  @media (min-width: $screen-md-min){
    margin-bottom: $sc-base-margin * 4;
    clear: both;
    @extend .col-md-8;
  }
}
.testimonials-form-content-group-label {
  @extend .input-label;
  display: inline-block;
  margin-top: $sc-base-margin * 4;
  margin-bottom: $sc-base-margin;
}
.testimonials-form-content-group-input {
  @extend .input-generic;
  margin-bottom: $sc-base-margin;
}
.testimonials-form-content-group-text {
  @extend .input-textarea;
  min-width: 100%;
}
.testimonials-form-actions {
  @extend .row;
  padding: $sc-base-margin * 6 $sc-base-margin * 3 $sc-base-margin * 4;
  clear: both;
}
.testimonials-form-actions-button-submit {
  @extend .button-large;
  @extend .button-primary;
  float: right;
}

 

If you save and refresh the new testimonials form page, you'll see the page render without any styling. Why? Because we need to do two things first:

  1. Update ns.package.json to include a value for the Sass
  2. Update distro.json so that the testimonials module is included in shopping.css

After you've done that, restart your local server and refresh the testimonials page, you should see it styled something like this:

We can also do a couple of other small changes to how the page looks by editing the view. Open up Testimonials.Form.View.js and replace it with the following:

define('Testimonials.Form.View'
, [
    'testimonials_form.tpl'
  , 'Backbone'
  ]
, function TestimonialsFormView(
    testimonialsFormTpl
  , Backbone
  )
{
  'use strict';

  return Backbone.View.extend({

    template: testimonialsFormTpl

  , title: _('New Testimonial').translate()

  , getBreadcrumbPages: function getBreadcrumbPages() {
      return [{
        text: this.title,
        href: '/testimonials/new'
      }];
    }
  });
});

 

After removing the code around getContext, we are adding two new things here:

  1. Page title — rather than having to manually use a title tag in your page, you set the page title using the view. We're using the translate function: if the site is multi-language and there's a translation available, it'll serve it in the user's set language.
  2. Breadcrumbs — not particularly useful in this situation but an example of how to use it. Again, rather than having to manually code them into your template, we can use getBreadcrumbPages, give it some data, and it'll be generated automatically.

Refresh your browser. The page title will be set and they'll be neat little breadcrumbs at the top of the page's body. Cool.

Models and Validation

So we've spent some time making the page look good, so let's move on to making it functional by collecting data and preparing it. This will involve:

  1. Connecting the form with a Backbone model, which will handle the data
  2. Validating the data the user inputs

After that, we'll move to connecting it to the backend.

Models

When building a module in SCA, there are two things called models. In this part of the tutorial, I'm talking about Backbone models. A lot of different MVC systems have various definitions of what models are, so the people behind Backbone specifically say what they are:

Models are the heart of any JavaScript application, containing the interactive data as well as a large part of the logic surrounding it: conversions, validations, computed properties, and access control.

So let's add a model. Create Testimonials.Model.js in the JavaScript folder and put the following into it:

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

  return Backbone.Model.extend({

    urlRoot: Utils.getAbsoluteUrl('services/Testimonials.Service.ss')

  });
});

 

For now, we've created a link to a service that doesn't exist yet. A service is a JavaScript file that runs on the NetSuite server; thus, it can contain SuiteScript. Anyway, this model is simple for now, we'll expand on it later.

Before we move on, tab to the router and add the model as a dependency. Then replace the newTestimonial function with the following:

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

    view.showContent();
  }

 

Form Views and Saving a Form

We need to make two modifications to our form view. The first is we need to set the view to listen to when the form is submitted, and then perform an action when it does. In the return statement in Testimonials.Form.View.js, add the following:

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

 

saveForm is a mixin powered by Backbone.FormView, which handles the submission of data via a form. It serializes the data and saves the given model using it. In short, it is a nifty add-on we include to make forms easy.

In order to use it, we need to add it as a dependency to the view and then initialize it.

Add Backbone.FormView as a dependency and name it as BackboneFormView. Then, within the return statement, add:

, initialize: function initialize() {
    BackboneFormView.add(this);
  }

 

Great stuff. We're nearly there with the frontend model, let's just add some validation.

Validation

We offer two kinds of validation: frontend and backend. On the frontend, we run sanity checks to ensure that the data the user has provided us meets basic standards. For this we use the Backbone.Validation plugin. Frontend validation is a way to provide good user experience to the user by providing immediate feedback on their data. Without it, it would have to be submitted to the application, processed, and then a message sent back, which is time-consuming.

There are two changes to our files we need to make. The first is to introduce 'bindings'. In the view for the form, we need to add in some code so that we can point to our fields in the template and then give them names so our validation plugin can target them. Thus, in the return statement in Testimonials.Form.View.js, add the following:

, bindings: {
    '[name="text"]': 'text'
  , '[name="title"]': 'title'
  , '[name="writerName"]': 'writerName'
  , '[name="rating"]': 'rating'
  }

 

If you peak into your template, you'll see that these names align with the name attributes we put on our inputs.

The second thing we need to do is bring in the validation. Validation takes place in the model, which, as you'll remember, is the part that deals with our data. Thus, open Testimonials.Model.js and add the following to the return statement:

, validation: {
    writerName: {
      required: true
    , rangeLength: [2, 50]
    }
  , title: {
      required: true
    , rangeLength: [2, 200]
    }
  , text: {
      required: true
    , rangeLength: [2, 999]
    }
  , rating: {
      required: true
    , rangeLength: [1, 5]
    }
  }

 

That's it for the frontend, in terms of data. Now we need to move on to the backend and, in particular, building a service.

Service, Please!

In the first part of this tutorial, we added a custom record type. This means that we now have a schema for our testimonials that the NetSuite backend understands. What we need to do now is connect the frontend to the backend, and put it into that schema.

The backend portion of this comes in two parts: a backend model and a service. In SCA, services are similar to one another as they typically handle the execution of CRUD (create, read, update and delete) operations. Depending on the request, it will then invoke a particular function to be performed. These functions live in the backend model.

Backend Model

To get started, create a SuiteScript folder and then create Testimonials.Model.js in it. Within that file, put the following:

define('Testimonials.Model'
, [
    'SC.Model'
  , 'Application'
  , 'Utils'
  , 'underscore'
  ]
, function TestimonialsModel(
    SCModel
  , Application
  , Utils
  , _
)
{
  'use strict';

  return SCModel.extend({

    name: 'Testimonial'

  , validation: {
      writerName: {
        required: true
      , rangeLength: [2, 50]
      },
      title: {
        required: true
      , rangeLength: [2, 200]
      },
      text: {
        required: true
      , rangeLength: [2, 999]
      },
      rating: {
        required: true
      , range: [1, 5]
      }
    }

  , create: function create(data) {
      var record = nlapiCreateRecord('customrecord_testimonial');

      this.validate(data);

      if (session.isLoggedIn2()) {
        record.setFieldValue('custrecord_t_entity', nlapiGetUser() + '');
      }
      data.writerName && record.setFieldValue('custrecord_t_entity_name', Utils.sanitizeString(data.writerName));
      data.title && record.setFieldValue('name', Utils.sanitizeString(data.title));
      data.text && record.setFieldValue('custrecord_t_text', Utils.sanitizeString(data.text));
      data.rating && record.setFieldValue('custrecord_t_rating', parseInt(data.rating, 10));

      return nlapiSubmitRecord(record);
    }
  });
});

 

So let's look at this.

  1. SC.Model as a dependency — this is the base class for SuiteScript SCA models; whenever we create a backend model, we extend it.
  2. The name property — again, this is required for any backend model. If it's not included you will get an error when the application tries to use it.
  3. Validation — just like the frontend model, we put validation here because this is where the data 'is'. The model we're extending has built in validation, so we just have to provide the rules.
  4. Function for creating — at the moment, I'm just going to be focusing on creating a record, which I'm going to talk about below.

The create function creates a new custom record in the type of our testimonial custom record type using the data passed to it:

  1. It validates the data.
  2. We record their customer name, if the user is logged in.
  3. We set the field values for the record using the data the user supplied. Note that we're sanitizing the data each time we set a field value: this'll remove things like HTML and cross-site request forgeries (CSRFs).

With that complete, let's look at the service.

Service

When it comes to the code used in services, it is pretty standard across all modules. Often, when creating a new service you can just copy and paste an existing one into your new file, make some tweaks and it's good to go. If you don't believe me, compare services between a number of modules and see for yourself.

With that in mind, create Testimonials.Service.ss in your SuiteScript folder. In it, put the following:

function service (request) {
  'use strict';

  var Application = require('Application');
  var method = request.getMethod();
  var Testimonials = require('Testimonials.Model');
  var requestBody;

  try {
    requestBody = JSON.parse(request.getBody() || '{}');

    switch (method) {
      case 'POST':
        Testimonials.create(requestBody);
        Application.sendContent({message: 'Success'}, {status: '201'});
        break;

      default:
        Application.sendError(methodNotAllowedError);
    }
  } catch (e) {
    Application.sendError(e);
  }
}

 

There isn't much to explain here suffice to say that depending on the type of request passed to it, it performs a certain function. You can see that we require the model for the testimonials — the one we just defined. We're going to need something for the GET function later, but for now we're focusing on sending data to the server, not retrieving it.

Tidy and Deploy

Before we can deploy and test this, we need to do some admin.

Firstly, modify the ns.package.json file for the module so it looks like the following:

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

 

We've added two values, one for the service and one for the model.

Next, we also need to update distro.json. Specifically, we need to add Testimonials.Model to the dependencies array for ssp-libraries. Do that and save.

Before we test, we will need to deploy the SuiteScript files to the server. They cannot be run locally. We have added a flag into the deploy task in Gulp for uploading only backend files: do this now, by running gulp deploy --dev.

When that's complete, run gulp local again and visit your page. Fill out the form with a sample testimonial and submit it. If you navigate to the backend, you should see your submitted testimonial. I submitted a few, both logged in and out, to test it out and here's how it looks to me:

Summary

In part two of this series we looked at the data aspects of the module. We started by creating a form so that the users could enter their testimonials. As this template generally just accepts HTML, we had to connect it to the view using bindings and instantiating the generic form view that's included in Backbone.

After that we looked at frontend models and validation. The model connects the form with Backbone. In short, it's the way to handle the data associated with the view.

Finally, once we have that data we need to send it to the NetSuite backend. To do this, we implemented a backend model and service, which takes the data, validates it again, and then sends an API request to create a new record using the data.

In part three of the series, we will look at the read operation, in particular displaying the testimonials on the site after they've been submitted and approved.

If you're experiencing issues, or you think you've done something wrong, you can compare your files to mine by downloading Testimonials-part2.zip.