Developing Your First Custom SuiteCommerce Advanced Module: Part 3

This tutorial is the third in a four-part tutorial on the basics of developing in SCA. It is most appropriate for Denali and Mont Blanc versions. If you're running Vinson or newer, the section on service files will still work but is a deprecated coding style. You may wish to follow up with a tutorial on service controllers, eg how to build a contact us form.

In the previous article of this series, we added code that allows us to use data and display it using models and views. In this – the third article – we're going to interact with the NetSuite application so that we can manipulate data stored on it. This will replace the static data that we included. Information on APIs can be found in the Help Center, so, if you're interested in reading more, you can perform searches for the objects (e.g., nlobjSearchFilter) or functions (e.g., Record APIs) mentioned below to find reference documentation.

Get Records

In NetSuite, go to Customization > Lists, Records, & Fields > Record Types and edit the Artist record. On this page we're going to add a new field called Customer. The point of this is that the records we're going to create are specific to a particular customer (that customer 'owns' a specific list of records) and so we need a field to identify a particular user and their list. Click New Field and enter the following:

  • Label: Owner
  • ID: _owner
  • Type: List/Record
  • List/Record: Customer
  • Validation & Defaulting Tab

    • Mandatory: (checked)

Save the field. Open up SuiteScript > Artist.Model.js again. We're now going to define the get function. Getting data on a record from NetSuite requires you to perform a search query, and a search query takes up to four parameters (of which we are going to use three):

  1. Type – the internal ID of the record type we are accessing.
  2. ID – the internal ID of the saved search. (We're not using a saved search, so we're going to set this as 'null'.)
  3. Filters – an object containing numerous filters to specify the precise record we are looking to get data from. For example, we're going to specify the customer and the ID of the record we want.
  4. Columns – an object specifying which parts of the returned record that we want returned (that is, the internal ID of the record and the artist name and genre).

Put the following in your get function:

        var filters = [
          new nlobjSearchFilter('custrecord_owner', null, 'anyof', nlapiGetUser()),
          new nlobjSearchFilter('internalid', null, 'is', id)
        ];

So, in preparation for our search query we have constructed a variable for the filters. The first filter cites the _owner custom record for the first parameter and uses nlapiGetUser for the second parameter, which returns the ID of the user currently logged in. Thus, we are effectively saying that once the search access the (yet unspecified) record type, find the Owner field that belongs to the user. This uses the internal ID that we're providing it.

Add a second variable to the get as follows:

        var columns = [
          new nlobjSearchColumn('internalid'),
          new nlobjSearchColumn('name'),
          new nlobjSearchColumn('custrecord_genre')
        ];

Here we're saying that what we want returned is the internal ID of the record, the name, and genre custom record field. The final part is performing the query itself. Below that variable, put the following:

        var search = nlapiSearchRecord('customrecord_artist', null, filters, columns);
        if (search && search.length === 1) {
          return {
            internalid: search[0].getValue('internalid'),
            name: search[0].getValue('name'),
            genre: search[0].getValue('custrecord_genre')
          };
        } else {
          throw notFoundError;
        }

So you can see that in the first line, we are performing a search on our _artist custom record using the filters and columns we just specified. The next line then performs a check on the returned result: firstly to check that it has been set (because a failed query returns null) and that it has returned one result. Then we have a return statement that takes the search variable and uses the getValue method to pull out the data we want and return it as an object. Finally, an else statement that throws a standard error message (namely that the record wasn't found).

Create Records

Now we can't test this yet because we don't have any results to return from the search, so we'll need to define our create function so we can create some entries for us to return. Within the create function put the following:

        var record = nlapiCreateRecord('customrecord_artist');
        record.setFieldValue('name', data.name);
        record.setFieldValue('custrecord_genre', data.genre);
        record.setFieldValue('custrecord_owner', nlapiGetUser());
        return nlapiSubmitRecord(record);

Note that when we talk to the NetSuite API, we use JSON; so if you look in Artist.Service.ss, you'll see that we are referencing an object called data. So, here in our model, we are using nlapiCreateRecord to create an object of type customrecord_artist. Then, on the next, lines we are simply specifying the fields and values that we want to put in this object using the setFieldValue method to do this. The POST block in the service will pass this object to the NetSuite application on creation.

The final part of creating a record is the data itself. Later, we're going to add a form so users can add in their own data but for now we're going to hardcode the data. Open up Artist.Router.js. You'll recall that one of the routes we created was newArtist. Add a new function as follows:

    newArtist: function () {
      var model = new Model();
      model.set('name', 'Peter');
      model.set('genre', 'Blues');
      model.save();
    }

Now we have enough to test. As we've made changes to SuiteScript files, we will need to run gulp deploy to push the changes to the server. Return to your site and, after logging in, open the developer console to the network tab and go to #artists/new. You'll see a blank page but if you examine the response to the Artist.Service.ss file, you should see an object. You can refresh the page numerous times to create new records.

List Records

So we want to be able to view the records that we've created and, for this, we need to define our list function in Artist.Model.js. Listing records and getting a record are very similar. When developing your own modules you can usually start by copying and pasting the get function and modifying it but, for ease, copy and paste the below code (replacing the hardcoded data we currently have there):

      list: function() {
        var filters = [
          new nlobjSearchFilter('custrecord_owner', null, 'anyof', nlapiGetUser()),
        ];

        var columns = [
          new nlobjSearchColumn('internalid'),
          new nlobjSearchColumn('name'),
          new nlobjSearchColumn('custrecord_genre')
        ];

        var searchResults = nlapiSearchRecord('customrecord_artist', null, filters, columns);
        return _.map(searchResults, function(result) {
          return {
            internalid: result.getValue('internalid'),
            name: result.getValue('name'),
            genre: result.getValue('custrecord_genre')
          };
        });

      },

Note the following differences:

  • id is not a parameter for this function and so we don't need code to handle it.
  • We're using an Underscore helper to produce an array out of the JSON search results.

Note, however, that we haven't yet added Underscore as a dependency in SuiteScript > Artist.Model.js, so do this now (underscore is the name of the module, add _ as its name in the function's parameters). Save and deploy again. Visit #artists and you should see a neatly formatted table containing the new artist records that you created.

Create a Form for Creating a New Record

In a previous section we hardcoded the way of creating new records, but now we're going to give users a way of creating their own artist records. First, we're going to create a link on the artist list template to this form (we'll code the form later). In artist_list.tpl add the following below the <h1> tag:

<a href="/artists/new">Create New Artist</a>

Now let's create a view and a template for this form. Create Artist.Edit.View.js under the JavaScript folder, and artist_edit.tpl under Templates. In the view, put some boilerplate stuff:

define('Artist.Edit.View',
  [
  'Backbone',
  'artist_edit.tpl'
  ],
  function (Backbone, artist_edit_template) {
    return Backbone.View.extend({
      template: artist_edit_template,
      events: {
        'submit form': 'saveForm'
      }
    });
  }
)

Note that we have added events to this: this listens for things that happen, namely the <form> element being submitted. In the template, start building the form:

<h1>Edit Artist Details</h1>
<form>
    <label>Name</label><input id="name" name="name" type="text">
    <label>Genre</label><input id="genre" name="genre" type="text">
    <button type="submit">Add</button>
</form>

If you remember, in order for the view to be shown when we want to, we need to update the router to point to that view. Open Artist.Router.js and add Artist.Edit.View as a dependency, EditView as a parameter, and then update our newArtist function so that it uses the data submitted in the form, instead of our hardcoded values. Replace the code in the function with this:

      var view = new EditView({application: this.application});
      view.showContent();

Stop and start gulp local (we've added a new template, and Gulp won't watch it until we do). Go to artists/new on your local site and you should see the form load. It won't do anything just yet, if you try to submit.

Connect the Model to the Form

One thing you can look at is suitecommerce > Backbone.FormView > JavaScript > Backbone.FormView.js. This file helps with using forms in Backbone. There is a saveForm function which sends the data to the model and, if you remember, we added a call to saveForm in our view. So go back to Artist.Edit.View and add in Backbone.FormView as a dependency and FormView as a parameter. Then add an initialize function to the return statement as follows:

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

So, in order for the application to get the data from the form, we need add a model to our newArtist function in Artist.Router.js. Change the view variable declaration to:

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

Then in Artist.Edit.View.js, initialize the model in the initialize function (below the code for this.application.:

        this.model = options.model;

So we should have enough to test. Reload your form on your local site. When you enter information and submit, there should be a POST in the network tab. When you go back to your list of artists, your new entry should be shown in the list.

Add Validation

Users can now add their own data into the records, but we need to ensure that when they submit the record that it is formatted in a way the application expects. First we're going to add frontend validation, which is useful for improving user experience.

The SCA code has functionality built in to it so that adding validation is easy. To start, add the following code to the return statement in JavaScript > Artist.Model.js:

      validation: {
        'name': {
          required: true,
          msg: 'Please enter an artist name'
        },
        'genre': {
          required: true,
          msg: 'Please enter a genre'
        }
      }

So all we've done is add a check on each field so that it contains data when a user submits the form (and show a message if they're empty). Now we need to modify the form so that the JavaScript knows to validate that form. Change artist_edit.tpl to the following:.

<h1>Edit Artist Details</h1>
<form>
    <div data-input="name" data-validation="control-group">
        <label for="name">Name</label>
        <span data-validation="control"><input type="text" name="name" id="name"></span>
    </div>
    <div data-input="genre" data-validation="control-group">
        <label for="genre">Genre</label>
        <span data-validation="control"><input type="text" name="genre" id="genre"></span>
    </div>
    <div>
        <button type="submit">Add</button>
    </div>

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

Note the additional data attributes we've added. Visit your local site again and go to the form. Now if you try to submit the form without any data in it, you will be shown error messages.

Now we need to add some backend validation: this will run when the create function in the backend model is called. Open SuiteScript > Artist.Model.js and add the following property to the SCModel.extend method:

      validation: {
        'name': {
          required: true,
          msg: 'Please enter an artist name'
        },
        'genre': {
          required: true,
          msg: 'Please enter a genre'
        }
      }

Finally, add this code to the top of the create function:

        this.validate(data);

This ensures that the data is validated before it is submitted to the application to create.

To test this, first remove (or comment out) the validation property in JavaScript > Artist.Model.js – if you don't, the frontend validation will always fire first. Then run gulp deploy. After it's finished, return to your form and submit it empty. In the alert-placeholder div below the form, you should see an error message (and no inline error messages). Afterwards, make sure you restore the frontend validation.

Delete a Record Using a Modal

Adding a record is complete, now we need to offer a way to delete a record. One interesting way we can do this is by adding a confirmation dialog, which pops up in a modal box when the user click a delete button. Firstly, modify artist_details.tpl and add a cell for a delete button:

    <td><button data-action="remove" data-id="{{internalid}}">Delete</button></td>

Next we need to modify Artist.List.View.js to add in the functionality. Firstly, add GlobalViews.Confirmation.View and jQuery as dependencies, and ConfirmationView and jQuery as properties to the define function. Then add the following as properties to Backbone.View.extend:

    events: {
      'click button[data-action="remove"]': 'removeArtist'
    },

    removeModel: function(options) {
      var model = options.context.collection.get(options.id);
      model.destroy();
    },

    removeArtist: function(e) {
      e.preventDefault();
      var view = new ConfirmationView({
        title: 'Remove Artist',
        body: 'Are you sure you want to remove this artist?',
        callBack: this.removeModel,
        callBackParameters: {
          context: this,
          id: jQuery(e.target).data('id')
        },
        autohide: true
      });
      this.application.getLayout().showInModal(view);
    }

So, firstly, we're adding in some code that connects the clicking of the delete button with a JavaScript function. Before we code that function, however, we have to code a helper function that removes the model from the collection. After that, we then create the function that creates the modal box and then processes the confirmation. However, there's one final part: actually sending a call to the server to delete the record. In SuiteScript > Artist.Model.js add the following line to the remove function:

        nlapiDeleteRecord('customrecord_artist', id);

One final thing to add. When a user makes a modification (such as deleting a record) we want the list to update to reflect their action. Put the following in the initialize function in Artist.List.View.js:

      var self = this;
      this.collection.on('reset sync add remove change destroy', function() {
        self.render();
      });

What this small bit of code does is listen for modifications to the model (such as calls to reset, sync and destroy) and re-renders the collection view.

Do gulp deploy and push it up to your server. Once that's done, go back to your local site and click a Delete button. After confirming in the model box that you want to delete the record, refresh the page. You'll no longer see that record in the list.

Update a Record

The final operation that we can perform with the API is updating an existing record. By now we already have most of the code we need to do this already, so it's just a case of reusing and remixing this code. First let's start by changing our router so that we no longer have separate functions for creating and updating records. In Artist.Router.js, change the route for a new artist to the following:

      'artists/new': 'artistDetails',

This now means that we can remove the newArtist function. After that, add jQuery as a dependency and jQuery as a parameter to the define functionn. Then add a new function called artistDetails with the following code:

    artistDetails: function (id) {
      var model = new Model();
      var promise = jQuery.Deferred();
      var self = this;

      if (!id) {
        promise.resolve();
      } else {
        model.fetch({
          data: {
            internalid: id
          }
        }).done(function () {
          promise.resolve();
        });
      }
      promise.done(function () {
        var view = new EditView({model: model, application: self.application});
        view.showContent();
        view.model.on('sync', function (model) {
            Backbone.history.navigate('artists', {trigger: true});
        });
      });
    }

So this has multiple components:

  • We're using jQuery.Deferred to create a 'factory' to handle our callbacks.
  • Note that we're setting a variable called self. This ensures that we maintain the correct context throughout the function.
  • We've made the function dual use by using a conditional statement by checking if it was passed an ID, if it wasn't then we know that we're creating a new record (if we were, we're updating).
  • After doing all the stuff we already know about, there's then a bit of code that will navigate us back to the artists route.

Now let's create the link for editing a record. Open artist_details.tpl and add in the following:

     <td><a href="/artists/{{internalid}}">Edit</a></td>

This will create a link to the third route in our list. Now, we need to pass the template the values from the model (using the view). So in Artist.Edit.View.js add the following to the return statement:

      getContext: function() {
        return {
          isNew: this.model.isNew(),
          id: this.model.get('internalid'),
          name: this.model.get('name'),
          genre: this.model.get('genre')
        }
      }

A quick thing we can do is prepopulate the existing values in the template for the update operation, so open artist_edit.tpl. On the inputs, add the value parameter to each of the input and use handlebar variables to put the name and genre of the artist if they exist, for example value="{{genre}}".

Now there's one vital part missing: a change to the backend model – after all, we haven't put any code in to process the updating of the record. The code for creating and updating records is largely similar. In SuiteScript > Artist.Model.js, change the update function to the following:

      update: function(id, data) {
        this.validate(data);
        var record = nlapiLoadRecord('customrecord_artist',id);
        record.setFieldValue('name', data.name);
        record.setFieldValue('custrecord_genre', data.genre);
        return nlapiSubmitRecord(record);
      },

As we've made changes to the backend, we'll need to push this up to the server; run gulp deploy again. When that's finished, visit your site and edit a record. It should redirect you back to the artists page and show the change that you made.

Summary

In this article we covered a number of areas that improved our module. We learnt how to add a link to the My Account menu so that users could go directly to the artists list page. We then replaced the hardcoded way we had for creating records and replaced it with a form. A helper function in the SCA code was used to quickly link the form to the model, so that data could flow between the two. After that we added validation to both the front and backends of the form so that we can ensure that empty forms are not being submitted to the application – this used built-in SCA functionality. Finally we moved onto the two remaining API operations: deleting and updating, which included combining the functions for creating and updating records.

You can download a copy of the files as they should be here SCA-First-Custom-Module-Part3.zip.

The next article goes over some aesthetic parts of updating a site, such as design aspects and using SCSS.

Further Reading