Develop Your First SuiteCommerce Advanced Module (Kilimanjaro) - Part 2: Data Basics

This tutorial is the second in a four-part tutorial on the basics of developing a SuiteCommerce Advanced site. It is intended for the Kilimanjaro version, but can be adapted to work with Vinson and Elbrus. If you're using Denali or Mont Blanc, refer to our older tutorial.

In the first part of this tutorial, we did the basics of new module development: created a module with an entry point, router, view and template, and had it render a simple message when we visited a particular URL path on our site.

This part of the tutorial will build on the foundations of that by:

  1. Creating a service controller and backend model to simulate HTTP requests for data
  2. Creating a chain from the NetSuite servers to the frontend template of the site, so the data can flow
  3. Adding a frontend model to handle data, and adding a collection to handle the models

There's a few interstitial steps too, but that's the gist.

Add a Backend Model

Your site has data stored in NetSuite and you want to get it into your JavaScript so that you can serve it to your shoppers. You also may want to save data that the shopper submits. These are all things that you can do with SuiteScript, our proprietary scripting language.

SuiteScript is based on JavaScript, but it can only run on NetSuite servers. This means that when you write some SuiteScript for your site, you have to upload changes to NetSuite so that it can run on the servers. There are two key features to this:

  1. It gains privileged access to record data objects and methods
  2. The scripts are obscured from the user, which can be good for security or privacy (eg to hide proprietary code or API keys)

In Backbone, models are there to handle data. They define what to do when each method is called. We're going to write a SuiteScript model that simulates getting data from NetSuite, so that file is going to say, "when I'm told to get data, I'm going to return an object of data".

Specifically in SCA, we have backend and frontend models. When it comes to customization to fit your module's needs, you will typically only need to make change to your backend model — the base frontend model class only requires the path to your backend model.

To begin, create a folder called SuiteScript in UserPreferences, and in it create a file called UserPreferences.Model.js.

In that file, put the following:

define('UserPreferences.Model'
, [
    'SC.Model'
  ]
, function
  (
    SCModel
  )
{
  'use strict';

  return SCModel.extend({
    name: 'UserPreferences'

  , list: function ()
    {
      return [
        {internalid: 1, type: 'Color', value: '7'}
      , {internalid: 2, type: 'Size', value: '5'}
      ]
    }
  })
});

We define the file as normal, but this time we include SC.Model which is our base class for backend models. After extending it, you'll note that we give it a name — this'll be used to register it within the application and be a name to which we can refer.

Then we have a list method: note that in models, we don't have to match 1:1 HTTP method names to the methods in our model. So, we can create a method that lists all record data at once; the call to this method will be made in the service (controller) that we'll get to later.

You'll note that when it's called, it'll just return an array of hardcoded data objects, but that's fine for now. So, how do we call it?

Add a Service Controller

A backend model is only half of what we need: the other is a service controller.

As the name suggests, it controls services. What's a service? It is a NetSuite term to describe a SuiteScript file that handles the typical HTTP/CRUD (create, read, update, delete) methods associated with a website and its associated data. In older versions (ie those before Vinson), you had to manually code your own service file. However, modern versions use service controllers instead.

Service controllers allowed us to standardize so much of the service as the files were frequently so similar to each other. They also offer a lot of other useful things such as permission validation, events and extensibility, AMD, and more.

For now, however, what we need to know is that the connect with the backend model and then compile to form the service file. In other words, we haven't got rid of service files, instead we've just implemented a system that can automatically generate them based on how we define the service controller.

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

define('UserPreferences.ServiceController'
, [
    'ServiceController'
  , 'UserPreferences.Model'
  ]
, function
  (
    ServiceController
  , UserPreferencesModel
  )
{
  'use strict';

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

  , get: function ()
    {
      var id = this.request.getParameter('internalid');
      return id ? UserPreferencesModel.get(id) : UserPreferencesModel.list()
    }
  })
});

The two dependencies for this are the base service controller that we're going to extend, and the backend model we just created.

The service controller's methods must match the following names: get, update, create, remove, which correspond respectively to read, update, create and delete. So, therefore, when a call from the frontend is made to get a record or some records, the get method, specifically, is called. However, as you'll see from the code, how we handle this (ie what we code we write) is up to us.

Internal IDs are assigned to every record in NetSuite — they are numerical (starting from 1) in the context of the type of the record they are. Thus, when we create a custom record, the first one we create will have the internal ID of 1, despite there being other records of other types that have an ID of 1.

In our module's functionality, we anticipate two kinds of get request:

  1. Load a specific record's data (aka a detailed view)
  2. Load all records (aka a list view)

So how do we differentiate between a request for a detailed view of a record, and a request for all records? In our get method, we have two lines of code that handles this request. The first sets a variable plucking the ID from the request. Imagine looking at a list of a records and clicking a View button next to one: the clicking of the button is saying "show me more about this record"; in technical terms, that means we will have to provide a record ID. However, if we've just loaded a landing page for records, then no specific ID will be request and the parameter for the internal ID will be left blank.

This brings us to the second line of code: what it's saying is, if there's an ID in the request then run the get method on the model; if there isn't, then you run the list method.

Deploy and Test the Service

We'll add some more methods later, but for now we need to think about how we're going to make the application aware of our SuiteScript. The first part, if you remember, is to update the module's package file.

Open ns.package.json and in it add the following to the gulp object:

, "ssp-libraries": ["SuiteScript/*.js"]
, "autogenerated-services": {"UserPreferences.Service.ss": "UserPreferences.ServiceController"}

We're pointing to our new SuiteScript, which compiles into ssp_libraries.js, and then the service controller that will be used to generate our service file.

We also need to update our distro file. Like our JavaScript, we also need to tell the tools to compile the code into the right places.

Open distro.json and find the ssp-libraries object. In the dependencies array, add "UserPreferences.ServiceController".

Don't forget to put a comma on the previous line.

And that's kinda it for the backend stuff: we've joined those two bits together, and made our application aware of them, so after deploying it we can test it out.

gulp deploy --no-backup

You can actually access services directly in your browser. For something that is a simple get request, you can just put the service URL into your web browser and it'll run the method, for example:

// Pattern
https://<BASE URL>/<SSP>/services/UserPreferences.Service.ss?n=<SITE>

// Example
https://checkout.netsuite.com/c.TSTDRV12345678/sca-dev-kilimanjaro/services/UserPreferences.Service.ss?n=3

If you don't have an active, logged-in user session then you'll get a permissions error. However, if you are logged in then you should see this:

Great! That means the constituent parts of the service — the model and service controller — are functioning. That means we can now move on to the frontend aspect of this module.

Create the Collection

We know that data can be requested from the server, but now we need to write code to do it for us, as well as add some new files to handle this data: this is where we add another model and a collection.

We also know that the model is what handles the data, but you might not know what a collection is. In short, a collection is a group of models — specifically, it is the same model, iterated multiple times for records of the same type. We use collections when the data we're expecting back is an array (collection) of records, rather than a single record.

In our example we need one because we're going to call a list method, and a list will return some details about every record in that list.

In JavaScript, create UserPreferences.Collection.js and in it put:

define('UserPreferences.Collection'
, [
    'Backbone'
  , 'UserPreferences.Model'
  , 'underscore'
  ]
, function
  (
    Backbone
  , UserPreferencesModel
  , _
  )
{
  'use strict';

  return Backbone.Collection.extend({
    model: UserPreferencesModel
  , url: _.getAbsoluteUrl('services/UserPreferences.Service.ss')
  });
});

Relatively speaking, it's quite a simple file; we extend a base collection class and then do two things:

  1. Specify the model to use
  2. Point to the service where the data will come from / go to

Firstly, note that we haven't created this model yet — despite it having the same name as the backend model, the above refers to the frontend model. Remember, the backend model is uploaded to the server and handles the data at that part of the interaction; the frontend model is what handles it on the user's end. We'll create this soon.

Secondly, the url key has a relative URL that we're transforming into an absolute one using _.getAbsoluteUrl. We haven't really talked about Underscore yet, but it's worth doing so.

Underscore is a library — a very useful library — that runs right through our application. It has a number of methods that make tedious things very easy. Furthermore, because of it's multifacetedness, we use its namespace (_) for our own utility functions too. One of those functions is getAbsoluteUrl, which, as I said, is used to create links for resources such as services.

I recommend reading their documentation; more advanced users can also read about five crucial functions that we use throughout the site.

Create the Model

As mentioned, our functionality needs a collection because we're going to be offering list functionality for our records. The collection takes the data and then pumps out a frontend model for each one.

Let's create that model now.

In JavaScript, create UserPreferences.Model.js:

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

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

Again, very simple looking. We point to the service again (note that the key name is a little different) and that's basically it. The base model code does the rest of the work for us.

Update the Router

The model and collection are ready to be used, so now we need to plug them into our application. To do this, we need to do three things:

  1. Add the collection as a dependency of the router
  2. Instatiate a new collection object when the list function is called
  3. Fetch the data, and then — when this is complete — render the view

Thus, open up UserPreferences.Router.js and update the opening of the define statement:

define('UserPreferences.Router'
, [
    'Backbone'
  , 'UserPreferences.List.View'
  , 'UserPreferences.Collection'
  ]
, function
  (
    Backbone
  , UserPreferencesListView
  , UserPreferencesCollection
  )
{

Then, change the preferencesList method to this:

, preferencesList: function ()
  {
    var collection = new UserPreferencesCollection();
    var view = new UserPreferencesListView
    ({
      application: this.application
    , collection: collection
    });

    collection.fetch().done(function ()
    {
      view.showContent();
    });
  }

You can see that we're returning a new collection, and then calling the fetch method — this is built into the base collection class and calls the URL we specified in our extended collection. Then, when that's done, we tell the view to show its content (although, at this point, it's not sent any of the collection's data). We're also, as you'll note, passing the collection to the view.

This all means that there is a connection from the frontend to the backend.

  1. The preferences route is called by the visitor, triggering the preferencesList function
  2. The function calls the fetch method on the collection
  3. The collection triggers a call to the service via the URL
  4. The service calls the service controller
  5. The service controller calls the get method on the backend model
  6. The backend model returns our dummy data

Or, in visual terms:

We can actually test this; our data won't show up in the page, but we can inspect the call in our developer tools.

Restart your local server — you need to stop and start it when you add new files — then open your dev tools and log in to the account section of your site again. Edit the URL go to the local version of your SSP, and then replace the end with #preferences.

You should see your normal 'hello world', but if you open up the Network tab of your tools and look in the XHR subtab, you should see a call to your service — click on it. The Response subtab should show you the data we put in our model!

Now, let's plug this data into view and template.

Add a Collection View as a Child View

As we said, a collection is a tool for showing a compilation of records of the same of the type; in SCA this is usually an array of objects. The list view acts like a container for when the route is called, but it does not render the data itself — for this we need a collection view (as we're using a collection).

An easy way to think about this, in terms of your data, is an HTML table. When we want to render a table, there are two elements:

  1. The table element and the headings that make up the structure and context of the table
  2. The body and rows of a table, the ones that contain the data

Thus, the collection view is the first one — the structure — and the second one is the soon-to-be-created details view, which will contain the individual data models.

To add a collection view to render the collection, you have to add it as a child view to another view — in our case, this is the list view.

A child view is simply a view that renders within the context of another view. The template of the parent view is marked up so that you can determine exactly where you want the child view to render. Thus, in our case, we're going to build up the structure of the table in the parent template and then mark up the body element with a special data attribute to show where we want the rows to render.

Open UserPreferences.List.View.js and add two new dependencies:

  1. Backbone.CollectionView as CollectionView
  2. UserPreferences.Details.View as UserPreferencesDetailsView

Then add the following method:

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

Remember how we updated the router to pass through the application and collection when we run the list view constructor? This is where we apply them. As mentioned in the last part, the initialize method runs when the file is loaded: the options argument is all the stuff that's passed during construction.

Now add another new method:

, childViews:
  {
    'UserPreferences.Collection': function ()
    {
      return new CollectionView({
        'childView': UserPreferencesDetailsView
      , 'collection': this.collection
      , 'viewsPerRow': 1
      })
    }
  }

The childViews method is a standard method for what are known as composite views; these are views that have children, ie, they are composed of more than view. As of the Elbrus release all views are now composite views — previously you had to add a dependency every time you wanted to turn a view into a composite view; since then we have rolled this functionality into all views.

This method accepts lists of objects to render as child views. In our case, we are adding only one but you can see that we:

  • Name it
  • Construct it using the base collection view
  • Pass it a number of customization values, such as the view to use (our details view), the collection data to use and then how many views per row (for basic things, set this to 1)

Let's take a look at the details view now.

Create a Details View

The collection view has been told to render a specific child view for each data object (model) in the collection.

There are two important parts to a details view:

  1. The context object, where we have to specify all the individual values that we want to pass to the template
  2. The template we want to render

In JavaScript, create UserPreferences.Details.View.js and add to it:

define('UserPreferences.Details.View'
, [
    'Backbone'
  , 'user_preferences_details.tpl'
  ]
, function
  (
    Backbone
  , user_preferences_details_tpl
  )
{
  'use strict';

  return Backbone.View.extend({
    template: user_preferences_details_tpl

  , getContext: function ()
    {
      return {
        'internalid': this.model.get('internalid')
      , 'type': this.model.get('type')
      , 'value': this.model.get('value')
      }
    }
  })
});

When instantiated, the details view will be passed a model of the data it's being asked to render; from this, we can pluck out the values that we want and add them to the template context. Here, you can see that we're pulling out the three values that we expect to find in the objects.

Let's set up templates to render the collection and the details.

Update the List Template

We'll start with the list template. This will contain the structure for the table.

It's currently set up to display the 'hello world' message, so replace its contents with the following:

<h1>{{translate 'User Preferences'}}</h1>
<table>
    <thead>
      <tr>
          <th>{{translate 'Internal ID'}}</th>
          <th>{{translate 'Type'}}</th>
          <th>{{translate 'Value'}}</th>
      </tr>
    </thead>
    <tbody data-view="UserPreferences.Collection"></tbody>
</table>

OK, so I'm introducing two new things here.

The first is the {{translate}} helper. SuiteCommerce sites support multiple languages and it works by keeping dictionaries of base strings and their translated values. Using this helper, you pass it the string you want to use and it looks up a translation in the user's selected language; if one is found then it is returned, otherwise the original string is used instead. Thus, if you're not using multiple languages, you don't have to do this: you can just put plain text into your templates, but we consider it good practice to do this anyway (in case you plan to go multi-lang in the future.

An alternative to this would be to set all the strings in the view (using _.translate()) and pass them to the template like we did with our 'hello world' message. See our post on how to add custom translation text for more information.

The second is the tbody element that has the data-view attribute on it. You'll notice that it's the same as the name we used in the list view's childViews method. We're essentially saying "render the collection child view here" and pointing at this element.

Create a Details Template

Finally, let's determine how we want the template for each individual table row to look.

In Templates, create user_preferences_details.tpl and in it put:

<tr>
    <td>{{internalid}}</td>
    <td>{{type}}</td>
    <td>{{value}}</td>
</tr>

Yup, all it is is a table row with three cells, populated with values from the view context. This'll repeat each time it's called, depending on the data in the collection.

Test and Tidy Up

That's it! If you kill and restart your local server and go through the process of logging in and then revisiting the local version of your account page, you should see something like this:

Great!

All that's left now is some tidying up. We no longer need the 'hello world' message in the list view's context; so, in UserPreferences.List.View.js, remove the entire getContext method.

You can save and do a full deploy if you like.

Final Thoughts

So, in this post we moved from a simple 'hello world' page that showed in the account area of our site to a page that gets dummy data from NetSuite. Remember, services (composed of a service controller and backend model) are stored on the NetSuite servers, so what this part of the tutorial taught, was how to do a GET request from a page.

We did more than that, though. Once we made the request, we handled the data on the frontend. As we knew our service was going to send multiple records (ie an array of objects), we built a collection to handle them. A collection view was used to display this data, which is like a container that uses the same frontend model and view to render the same type of data.

Where do we go from here? Well, in the third part (available shortly), we will transition the code to use real data, as well as expand to the full array of HTTP methods. We'll create a record in the backend, then write code so that we not only read their values, but also create, update and delete them.

If you're having difficulty with the module, or want to compare code, take a look at UserPreferences@1.0.0-part2.zip, which is my code — note that this does not include the distro file.