Denali and Mont Blanc: Develop Your First SuiteCommerce Advanced Customization
Caution: this content was last updated over 9 years ago
Once you have your SuiteCommerce Advanced site and developer tools set up, it's time to get started. This article will guide you through the process of making changes to your site by adding functionality to the My Account section of a site. While the functionality itself will be somewhat arbitrary, it will teach:
- Using the developer tools, including Gulp.
- Performing basic tasks in the NetSuite app, to help familiarize you with the web interface.
- Understanding the folder structure and where files go.
- Getting to grips with single-page web architecture, particularly the libraries that we use in SCA: Backbone and Underscore.
- Interacting with NetSuite's Shopping and Commerce APIs to perform CRUD (create, read, update and delete) operations.
- Making aesthetic changes to the site's design.
The process for doing this takes several hours and the steps for doing is split up into multiple parts. This part covers the initial work for creating a module, getting "Hello World!" to appear on your site, and the basics of creating a view, router and template.
Module Summary
The module we will be adding captures the name of an artist and the genre of music that they perform. Ultimately this will be able to be entered through a form in a shopper's My Account; thus the shopper will need to log in, navigate to the page, and then create/read/update/delete record data. We will also be making aesthetic changes such as styling using Bootstrap, adding a link to the navigation, using modals, and adding a crumbtrail.
Creating the Record Type in NetSuite
Before we can develop something to interact with a record we must first create a new record type. Log in to the web interface for your site and navigate to Customization > Lists, Records, & Fields > Record Types > New. Add the type as follows:
- Name: Artist
- ID: _artist
- Show ID: (ticked)
- Access Type: Use Permission List
- Permissions Tab
- Role: Customer Center
- Level: Full
- Restrict: Viewing and Editing.
Make sure you click Add in the Permissions tab to add that permission, and then click Save to add the record type. You should leave all other settings to their default values.
Note that we use an underscore as a prefix to the custom record ID, this is because the app will append "customrecord" to the start of the ID when it is created. This convention is not mandatory but it's recommended to help you keep track of all the custom records (and custom fields and custom lists) that you create.
The permissions tab configuration will ensure that the customers will only be able to view and edit their own records and not the records of any other customer.
When the page reloads, click the New Field button in the Fields subtab. We need to add in a field to the Artist record. Add it as follows:
- Label: Genre
- ID: _genre
- Type: Free-Form Text
It would be better to make the genre field a list, but for the sake of simplicity we're going to use a standard text field. Save the field.
Preparing Your Local File Structure
In the local version of your site's files (that you created as part of your dev tools setup) is a folder called Modules; in this folder are two more folders: one, suitecommerce, that contains the proprietary code that makes up your site, and the second, third_parties, contains the libraries. frameworks and other code that SCA sites depend on. Within the Modules folder create a third and name it something like custom (or, say, your site's name) – this is where your site-specific code will live.
For our Artist module, we need to replicate the standard module structure. Under your custom folder create the following structure:
- Artist\@dev
- JavaScript
- SuiteScript
- Templates
Note the dev suffix – typically modules use numbers instead, but we're not versioning this module so we're picking an arbitrary value instead.
Within the Artist\@dev folder put a ns.package.json file. This file tells Gulp to look in the specified directories for your module's code. The easiest way to do this is to copy this file from an existing module. When you've done this open the new file and modify it so that it has the following structure:
{
"gulp": {
"javascript": [
"JavaScript/*.js"
],
"ssp-libraries": [
"SuiteScript/*.js"
],
"services": [
"SuiteScript/*.Service.ss"
],
"templates": [
"Templates/*.tpl"
]
}
}
After saving the file return to the top level of your SCA directory and edit distro.json. In the JSON for the modules key add in an entry to the bottom of attribute-value pair list for the Artist module. For example:
{
"name": "SuiteCommerce Advanced 2.0",
"version": "2.0",
"folders": {...},
"modules": {
"suitecommerce/Account": "2.0.0",
"suitecommerce/Address": "2.0.0",
...
"third_parties/underscore.js": "1.7.0",
"custom/Artist": "dev"
...
}
Save the file.
Hello World
At this point you've created an empty module that will be included in the Gulp tasks, such as adding it to the distribution and deploying to your site. Next we're going to add some code to it so that something appears on the frontend – we will add backend code later. First we start with a module file.
A module file lives in the module's JavaScript folder, taking the form of [module name].js. The SCA single page app uses this file to initialize a router for the module. For testing purposes, however, let's just put a simple console.log()
in it so we can see if it's working. Create Artist.js and put the following in it:
//@module Artist
define('Artist', function() {
'use strict';
return {
mountToApp: function(application) {
console.log('Hello World!');
}
}
});
Now that we have a JS component, we need to execute it in one of three applications that make up SCA (Shopping, MyAccount and Checkout). Open distro.json again. What we edited before only declared our new module, we now need to specify where we're going to run it.
Scroll down to tasksConfig
and, within that, to the MyAccount task (SC.MyAccount.Starter
). In the dependencies
array, add an entry for Artist
at the bottom.
Save the files and run gulp
. This will build your code and deploy it to your site. Once the deploy finishes, go to your site in your browser and log into a customer account (if you don't have a customer account already, register and then go to My Account). When you're in, open the console – you should see your "Hello World!" message.
Create a View, Router and Template
In Backbone, views are JS files that are used to reflect the application's data models and listen to events and react accordingly. The simplest way to think of them is as the code that loads the template to be rendered on the frontend. In JavaScript folder create Artist.List.View.js. For now we are going to keep it simple, so just put the following in it:
define('Artist.List.View', [
'Backbone'
], function(Backbone) {
return Backbone.View.extend({
});
});
Note that we have used define
; this is part of a library SCA uses called RequireJS, which allows us to manage our module structure effectively and manage the dependencies that each module has. The first argument in a define
statement is an array of dependencies. In this statement we are putting Backbone as a dependency and then passed as parameters to the function.
This is going to be used to show a list of artists. To get to the view, you'll need to create a router. Routers are used in Backbone to direct users through the use of hash tags (#) and slashes (/). Thus, in order to show customers a list of artists we will need to create a router to direct them to Artist.List.View.js. Create Artist.Router.js in your JavaScript folder. Its contents looks similar to the view:
define('Artist.Router', [
'Backbone'
], function(Backbone) {
return Backbone.Router.extend({
routes: {
'artists': 'artistList',
'artists/new': 'newArtist',
'artists/:id': 'artistDetails'
}
});
});
So we've added three routes: one for listing the artist, another for adding a new artist, and a final one for viewing a particular artist's details. The values attached to the attributes we just specified are functions that we need to create. But first, for the artistList
function to work, we will need to add Artist.List.View.js
as a dependency. Modify the define
statement to the following:
define('Artist.Router', [
'Backbone',
'Artist.List.View'
], function(Backbone, ListView) {
So now our router has all things it needs to direct the user to a particular view (it know it depends on our view file and has the power to call it) so now we just need to define the artistList
function that will be called when a user visits #artists in the browser. Below routes
add the following:
artistList: function() {
var view = new ListView();
view.showContent();
}
This will render the view when the hashtag is used. Remember, though, we don't have anything in our view – we will need a template. Within your Templates create artist_list.tpl and, for now, just put in the following code:
<h1>Hello world!</h1>
Now we need to modify the view so that it knows to show the template. Modify Artist.List.View.js to add the template as a dependency and parameter to the define
statement, and then add the template to return
statement. Your code should look like the following:
define('Artist.List.View', [
'Backbone',
'artist_list.tpl'
], function(Backbone, artist_list_tpl) {
return Backbone.View.extend({
template: artist_list_tpl
});
});
Then, we need to plug the router into the application – this goes in Artist.js. Modify it so it looks like this:
define('Artist',
[
'Artist.Router'
],
function (Router) {
'use strict';
return {
mountToApp: function(application) {
return new Router(application);
}
}
}
);
Finally, we need to initialize the application in the router. In Artist.Router.js create an initialize
function within the return
function. This also needs to be added as a parameter to the artistList function.
return Backbone.Router.extend ({
initialize: function(application) {
this.application = application;
},
routes: {...},
artistList: function() {
var view = new ListView ({application: this.application});
view.showContent();
}
});
Everything is ready for another test, so run gulp local
to deploy it to your local server. When that finishes, log back in to your local site's My Account. You can access your local site by appending -local to the name of SSP application in the URL. For example, after you log in change /my_account.ssp to /my_account-local.ssp. After that, append #artists to the end of the URL. You should see your "Hello world!" in the page.
Note that if you see a blank page when accessing your local server, it may be because the scripts are being blocked because you're using a secure protocol with insecure resources. You will need add an exception to allow the scripts to run; in Chrome you can do this by clicking the silver shield in the address bar.
Create a Model and a Collection
Now that we have a basic scenario completed. Let’s move on to data — for that we’re going to look at what Backbone terms ‘models’ and ‘collections’.
Models in Backbone are the heart of a JavaScript application and contain the interactive data and most of the logic that handles that data. However, SCA does it slightly differently: all of the data and logic that would be contained in this file lives in separate, SuiteScript-powered files. So, in the model file, we'll need to direct it to this file.
In the JavaScript folder, create Artist.Model.js and add the following to it:
define('Artist.Model',
[
'Backbone',
'underscore'
],
function (Backbone, _) {
return Backbone.Model.extend({
urlRoot: _.getAbsoluteUrl('services/Artist.Service.ss')
});
}
);
We'll create the file it points to later. Next we'll create a collection. Collections are simply an ordered set of models and generally handle server requests. Create Artist.Collection.js in your JavaScript folder and add the following:
define('Artist.Collection',
[
'Backbone',
'Artist.Model'
],
function (Backbone, Model) {
return Backbone.Collection.extend({
model: Model,
url: _.getAbsoluteUrl('services/Artist.Service.ss')
});
}
);
We then add this to Artist.Router.js:
- Add the model and collection as dependencies.
- Add the collection as a property to the
artistList
function. - Add code to fetch data from the server and then show it; we already have
view.showContent()
– this will need to be moved into this code.
Thus, your code will look like the following:
define('Artist.Router', [
'Backbone',
'Artist.List.View',
'Artist.Model',
'Artist.Collection'
], function (Backbone, ListView, Model, Collection) {
return Backbone.Router.extend ({
initialize: function(application) {
this.application = application;
},
routes: {
'artists': 'artistList',
'artists/new': 'newArtist',
'artists/:id': 'artistDetails'
},
artistList: function () {
var view = new ListView({application: this.application});
var collection = new Collection();
collection.fetch().done(function() {
view.showContent();
});
}
});
});
Run gulp local
and check your local site again. When it refreshes, you should see an "internal error" message. If you look in the network tab of your browser's dev tools, you'll also see a 404 error for Artist.Service.ss. This means your code worked but the SuiteScript service wasn't found (which it won't, because we haven't created it yet).
Create a SuiteScript Service
SuiteScript services typically handle the create, read, update and delete (CRUD) operations of a module. A quick way to start is to copy and paste an existing service. Create Artist.Service.ss under the SuiteScript folder and then paste into it a copy of the service used in the Address module. Then perform a search and replace, changing Address to Artist.
If you look in this file you will notice a number of things. Firstly, there is a check to see if the user is logged in (which is required in the My Account section). Secondly, if that is true, it then gets the CRUD operation that user is requesting. Thirdly, there is a switch
statement to handle what is returned, which in turn executes the requested operation based on the result. Lastly, there is some code to handle erroneous results. In all, the code pattern is typical across the services in the SCA application – this is because the logic specific to the module is going to be contained in a second file called Artist.Model.js, contained under the SuiteScript folder. While it has the same name as the other model file, it performs different functions: namely it interacts with the NetSuite API and contains SuiteScript, which is not something the Backbone model can do. Thus, it is very much a backend component.
Create Artist.Model.js in your module's SuiteScript folder. Inside of it, put in some skeleton code to handle the CRUD operations and the list operation:
define('Artist.Model',
[
'SC.Model'
],
function (SCModel) {
return SCModel.extend({
name: 'Artist',
get: function(id) {
},
list: function() {
return [
{internalid: 2, name: 'John', genre: 'Rock'},
{internalid: 3, name: 'Christine', genre: 'Rock'},
{internalid: 1, name: 'Mick', genre: 'Blues'},
{internalid: 4, name: 'Lindsey', genre: 'Rock'},
{internalid: 5, name: 'Stevie', genre: 'Pop'}
];
},
create: function(data) {
},
update: function(id, data) {
},
remove: function(id) {
}
});
}
)
For now, we're going to hardcode the list
function to return an array of values – we'll change this to an API call later. We need to update our distro.json file: we have told Gulp to include the frontend files, now we must tell it to include the backend files. After the JavaScript and Sass tasks, we must add to the list of dependencies for ssp-libraries
. Add Artist.Model
to the end of the list. Now, as this is backend code, you will have to deploy this to your server. Run gulp deploy
and when that finishes, stop and start gulp local
again. Once that's finished, go to your local site and log in. After visiting #artists, go to the network tab in your browser's dev tools console. After selecting Artist.Service.ss, you should see the hardcoded values returned in the preview.
Use the Collection and Template to Show Data
So we know our code works because we're seeing it being pulled into the console. The next steps will be using the collection to show this in the page. The first thing we need to do is import the helper for the collection view. Add Backbone.CollectionView
, Backbone.CompositeView
and Artist.Details.View
as dependencies to Artist.List.View.js
, and then add CollectionView
, CompositeView
and ArtistDetailsView
as parameters to the function.
After that, add an initialize
function and childViews
property to the return
statement:
initialize: function (options) {
CompositeView.add(this);
},
childViews: {
'Artist.Collection': function() {
return new CollectionView({
'childView': ArtistDetailsView,
'collection': this.collection,
'viewsPerRow': 1
});
}
},
So we've now introduced two new concepts: collection views and composite views. We use collections when we want to render a repeating list of models (items) that have the same view – we're doing this for the artists and their internal IDs. In other words, we need a view for showing each item and the final product is a collection view. Composites are also used for a repeating list of models but when you want to wrap them in another view for display purposes, which is what we will use to show a list (but you could, for example, use a composite to show a table). We've called the child view ArtistDetailsView
, which doesn't exist yet but will contain styling for how we're going to show each item of the collection.
The viewsPerRow
property is also added as it defines how many times to render the view per row; e.g., if we were using tables to display the items then setting this to 1 means a single item will be shown per <td>
.
When it comes to populating areas in templates with collections of data, you use HTML data
attributes on page elements. We've just created the Artist.Collection
function, so in artist_list.tpl add the following code:
<h1>Artists</h1>
<table>
<thead>
<tr>
<th>Internal ID</th>
<th>Artist</th>
<th>Genre</th>
</tr>
</thead>
<tbody data-view="Artist.Collection"></tbody>
</table>
Note that we've created a template that's going to display a table, with the collection being used to populate the body of the table.
Back in Artist.Router.js swap the order of var view
and var collection
in the artistList
function so that var collection
is first. Then pass collection: collection
as a value to ListView
. Thus, the artistList
function has now been passed the collection. Your function will look like this:
artistList: function () {
var collection = new Collection();
var view = new ListView({collection: collection, application: this.application});
collection.fetch().done(function() {
view.showContent();
});
},
Now we need to pass these to our view. Open Artist.List.View.js and go the initialize
function. This function now needs to accept options
as parameters, which will expose the collection and application to the list view. Modify the function to the following:
initialize: function (options) {
CompositeView.add(this);
this.application = options.application;
this.collection = options.collection;
},
So we've mentioned using a child view, so now we have to define it. Under the JavaScript folder create Artist.Details.View.js. It follows the standard boilerplate that we've been using so far, but also references to a template for the child view (which we haven't created yet) and also a function that returns an object with the data from the model to the view. Put the following in the file:
define('Artist.Details.View',
[
'Backbone',
'artist_details.tpl'
],
function (Backbone, artist_details_template) {
return Backbone.View.extend({
getContext: function () {
return {
'name': this.model.get('name'),
'genre': this.model.get('genre'),
'internalid': this.model.get('internalid')
}
},
template: artist_details_template
});
}
)
The code mentions a template, so we have to create it now. Under the Templates directory, create artist_details.tpl. We've already created a collection that has a table in it, so this template will be used to process and build the rows in the table. In the template put the following:
<tr>
<td>{{internalid}}</td>
<td>{{name}}</td>
<td>{{genre}}</td>
</tr>
SCA uses Handlebars for templating. This allows us to use curly braces to substitute in values that we want to appear on the frontend. We now have enough code to test this again, so run gulp local
(if it's already running, stop and start it again) and the load the local version of the view in your browser again. Remember: if you have to log in again, make sure you change the URL to go back to the local version afterwards. You should see something like the following:
Get Records
Great! We’ve made progress by using static data but now is the time to move on and use real data.
Next, 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.
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):
- Type – the internal ID of the record type we are accessing.
- ID – the internal ID of the saved search. (We're not using a saved search, so we're going to set this as 'null'.)
- 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.
- 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
function. 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.
Conditional Changes in a Template
That’s the data part of the tutorial over, now let’s focus on making styling changes to our module — making it sure it looks good and that the user has a good experience.
One thing we can change is put in some conditional changes into our template. We can use the isNew
method we set in our getContext
function to show different text depending on whether the user is creating a new artist record or updating an existing one. In artist_edit.tpl change the h1 tag to the following:
<h1>{{#if isNew}}New{{else}}Edit{{/if}} Artist Details</h1>
Go to your local site and try adding and updating records, you should see different heading text depending on the context of the operation. You could now, for example, put in similar logic for the submit button text.
Add a Menu Item
So we have the ability to create, read, and delete the records, but we don't have an intuitive way for users to access these records on the frontend other than by going directly to the URL. Let's add a way to navigate to these pages in the menu. The menu is dynamic, so we don't need to write the HTML ourselves – we just need to write some JavaScript.
Open Artist.js and add a new parameter to the top of the main return
statement as follows:
MenuItems: {
parent: 'settings',
id: 'artistslist',
name: 'Artists List',
url: 'artists',
index: 0
},
Our code added the following properties:
- parent – the menu item the link will live under.
- id – the HTML data-id parameter added to the link.
- name – the label (text) shown on the link.
- url – the URL to which the user will be sent to.
- index – the position in the list it will appear.
After gulp local
has compiled your code, visit your local My Account page again. You'll see, under the Settings parent menu item, a link to our Artists List page.
Add Breadcrumbs
If you click around the My Account section on your site you'll see that there are breadcrumbs above the top heading of every page. These easily added and are controlled by the view for that page. So, in Artist.List.View.js add in the following to the main return
statement:
getBreadcrumbPages: function() {
return [{text: 'Artists', href: '/artists'}]
},
Once gulp local
updates, go to the list of artists on your local site and you'll see the breadcrumbs appear. We can repeat this process for Artist.Edit.View.js. Note, however, that we need different text depending on the context, so we can add in a conditional into our JavaScript as follows:
getBreadcrumbPages: function() {
if (this.model.isNew()) {
return [
{text: 'Artists', href: '/artists'},
{text: 'New', href: '/artists/new'}
]
} else {
return [
{text: 'Artists', href: '/artists'},
{text: 'Edit', href: '/artists/' + this.model.get('internalid')}
]
}
}
If you notice, we've added in two entries for the return
statement: this simply adds in an extra entry for the breadcrumbs. Now if you go to your local site and go to the form, you'll see the changes you've made.
Auto-Expanding Navigation
One small user experience improvement we can make is to the navigation: when a user visits a page related to the artist functionality, we can auto-expand the menu to show where the user is in context to the entire My Account section. To do this, add the following code to the return
statements in Artist.List.View.js and Artist.Edit.View.js:
getSelectedMenu: function() {
return 'artistslist'
}
getSelectedMenu
and getBreadcrumbPages
are examples of a number of helper functions that are built into the SCA code to make this sort of thing easy.
Prepare for Sass
At the moment our form looks pretty poor, so we should add some styling to it. SCA uses Sass for styling, which is a scripting language for CSS and allows us to extend it with new syntax, such as variables, mixins and nesting. The Sass is compiled by Gulp and outputted as CSS, which is then used in the browser.
Like JavaScript and templates, each module has its own Sass directory. To start you'll need to do the following:
- Create a directory called Sass at the same level as the module's JavaScript and Templates directory.
- Add an entry to the module's ns.package.json file as follows:
- In distro.json add
"Artist"
to the list of modules that have Sass to be included in myaccount.css (search the file for this). In other words, this means adding it as a dependency.
There are a number of SCA-specific conventions to do with Sass that we need to remember:
- When creating Sass files, the Sass files are modularized: one Sass file per template file. Furthermore, the names of the Sass files match the template they're styling, prefixed by an underscore.
- Within the template files, all classes are prefixed with the template name but using hyphens instead of underscores. So, for example, we're going to add some styling to artist_list.tpl so we'll be adding classes starting with
artist-list-
. - There exists a large number of helper classes built in to SCA which you can use by using the
@extend
keyword in your code blocks. These live in the Sass files in the Modules > BaseSassStyles > Sass directory.
Style the List Template
At the moment, our artist list looks pretty bad – we should make it look better. In the Sass directory, create _artist-list.scss and put in it the following:
.artist-list-header {
@extend .list-header;
position: relative;
margin-bottom: $sc-base-margin * 3;
display: inline-block;
width: 100%;
}
.artist-list-title {
@extend .list-header-title;
float:none;
@media (min-width: $screen-sm-min) {
float: left;
}
}
.artist-list-header-button-new {
@extend .list-header-button;
margin-top: $sc-base-padding * 4;
position: absolute;
top:25;
z-index:1;
right: 0;
@media (min-width: $screen-sm-min) {
margin-top: 0;
z-index:0;
top: 0;
margin-bottom: $sc-base-margin * 3;
}
}
.artist-list-results-container
{
@media (min-width: $screen-md-min) {
.artist-list-results-table-header-row-id {
width: 20%;
}
.artist-list-results-table-header-row-artist {
width: 30%;
}
.artist-list-results-table-header-row-genre {
width: 30%;
}
.artist-list-results-table-header-row-actions {
text-align: center;
}
}
}
.artist-list-results-table {
@extend .recordviews-table;
}
.artist-list-results-table-header {
@extend .recordviews-row-header;
border-top: 1px solid $sc-color-theme-light;
}
This is a lot of CSS, we'll go through it in a minute. However, you'll notice that we've put in a lot of classes; this means that we have to update our list template so that they are marked up with these classes. In artist_list.tpl, change the HTML to the following:
<section class="artist-list">
<header class="artist-list-header">
<h1 class="artist-list-title">Artists</h1>
<a href="artists/new" class="artist-list-header-button-new">Create New Artist</a>
</header>
<div class="artist-list-results-container">
<table class="artist-list-results-table">
<thead class="artist-list-results-table-header">
<tr class="artist-list-results-table-header-row">
<th class="artist-list-results-table-header-row-id">Internal ID</th>
<th class="artist-list-results-table-header-row-artist">Artist</th>
<th class="artist-list-results-table-header-row-genre">Genre</th>
<th class="artist-list-results-table-header-row-actions" colspan="2">Actions</th>
</tr>
</thead>
<tbody data-view="Artist.Collection"></tbody>
</table>
</div>
</section>
Restart gulp local
and go to your local site. When you visit the list page, it should look slightly better – so what happened? If you look at the Sass file again, you'll see that for a lot of our classes simply specify @extend .[class name]
. The @extend
directive is used in Sass where one class should all the styles of another class, as well as its own styles. In regular CSS, this means that you would attach multiple classes to the element your styling and build up the styles in the HTML. The downside to this is that you always have to remember to chain classes together when templating, meaning a maintenance headache and non-semantic style concerns. @extend
avoids these problems by telling Sass that one selector should inherit the styles of another selector.
@extend
works by inserting the extending selector wherever the extended selector is. For example, we have an extending class called .artist-list-results-table
that is extending .recordviews-table
; this means that wherever the extended class is used, the extending class is inserted. If there are other styles that we have declared for the extending class, they are outputted separately.
Style the Details Template
The list and table itself are now styled, but the individual rows and cells of the table are not – this is because the details template is separate (and the Sass for it is separate too). Replace the code in artist_details.tpl with the following:
<tr class="artist-details-results-table-row">
<td class="artist-details-results-table-row-id">
<span class="artist-details-results-table-row-id-label">Internal ID: </span>
<span class="artist-details-results-table-row-id-value">{{internalid}}</span>
</td>
<td class="artist-details-results-table-row-name">
<span class="artist-details-results-table-row-name-label">Name: </span>
<span class="artist-details-results-table-row-name-value">{{name}}</span>
</td>
<td class="artist-details-results-table-row-genre">
<span class="artist-details-results-table-row-genre-label">Genre: </span>
<span class="artist-details-results-table-row-genre-value">{{genre}}</span>
</td>
<td class="artist-details-results-table-row-action-edit">
<a class="artist-details-results-table-row-action-edit-button" href="/artists/{{internalid}}">Edit</a>
</td>
<td class="artist-details-results-table-row-action-delete">
<button class="artist-details-results-table-row-action-delete-button" data-action="remove" data-id="{{internalid}}">Delete</button>
</td>
</tr>
You'll notice that this is quite a significant change to the template:
- The class names mirror the same pattern as the list template.
- You'll also see that the class names build up gradually as you go deeper in nested elements; this is one of the benefits of Sass – every class is meaningful.
- We've added in labels to each of the cells. We're going to hide these for most responsive views but we're going to add in some styling later that collapses the tables on narrower views and we'll need these labels for the values.
So now we need to add the styling for these rows. Create _artist-details.scss and put the following in it:
.artist-details-hide-label {
@media (min-width: $screen-md-min) {
display: none;
}
}
.artist-details-results-table-row {
@extend .recordviews-row;
}
.artist-details-results-table-row:hover {
@extend .recordviews-row:hover;
}
.artist-details-results-table-row-id-label {
@extend .artist-details-hide-label;
}
.artist-details-results-table-row-name-label {
@extend .artist-details-hide-label;
}
.artist-details-results-table-row-genre-label {
@extend .artist-details-hide-label;
}
.artist-details-results-table-row-action-edit-button {
@extend .button-small;
@extend .button-tertiary;
@media (max-width: $screen-md-min) {
margin-bottom: $sc-small-margin;
}
}
.artist-details-results-table-row-action-delete-button {
@extend .button-small;
@extend .button-primary;
background-color: $sc-color-error;
border-color: darken($sc-color-error,20);
@media (max-width: $screen-md-min) {
margin-bottom: $sc-small-margin;
}
}
.artist-details-results-table-row-action-delete-button:hover {
background-color: saturate(lighten($sc-color-error, 4), 4);
}
If you stop and start gulp local
again, and go to your local site, you will see the changes. Also, if you resize the width of the window, you'll see that the styling changes as you make it larger and smaller. You'll notice in the above code that we added some styling that only applies when the window's width is less/more than a 'breakpoint'. Breakpoints (such as $screen-md-min
) are defined by Bootstrap, a responsive frontend framework. Bootstrap provides a lot of ready-to-use styling and helpers to aid us in styling pages quickly and consistently.
Another thing you'll notice is that, for example, we've used the saturate
and lighten
functions; these, and other useful functions, are available via Sass and enable us to make ad-hoc changes to colors – rather than hardcoding values throughout our CSS, we just have one file (SuiteCommerce > BaseSassStyles > Sass > variables > _colors.scss).
Style the Edit Template
Now that we've styled two templates already, there is just one left and that is the form where users can add or edit artist details. Edit artist_edit.tpl and replace it with the following:
<section class="artist-edit">
<header class="artist-edit-header">
<h1 class="artist-edit-header-title">{{#if isNew}}New{{else}}Edit{{/if}} Artist Details</h1>
</header>
<form class="artist-edit-form">
<fieldset>
<small>Required <span class="artist-edit-form-label-required">*</span></small>
<div class="artist-edit-form-name" data-input="name" data-validation="control-group">
<label class="artist-edit-form-label" for="name">Name <span class="artist-edit-form-label-required">*</span></label>
<span data-validation="control"><input class="artist-edit-form-input" type="text" name="name" id="name" value="{{name}}"></span>
</div>
<div class="artist-edit-form-genre" data-input="genre" data-validation="control-group">
<label class="artist-edit-form-label" for="genre">Genre <span class="artist-edit-form-label-required">*</span></label>
<span data-validation="control"><input class="artist-edit-form-input" type="text" name="genre" id="genre" value="{{genre}}"></span>
</div>
</fieldset>
<div class="artist-edit-form-submit">
<button class="artist-edit-form-submit-button" type="submit">{{#if isNew}}Add{{else}}Update{{/if}}</button>
</div>
<div data-type="alert-placeholder"></div>
</form>
</section>
As with the templates before, we've added more semantic elements to it and marked it up with classes that follow a clear pattern: while you're not required to follow this guideline, do what's best for your site.
Next you need to create Sass > _artist-edit.scss and put the following in it:
.artist-edit {
@extend .box-column-bordered;
}
.artist-edit-header {
margin-bottom: $sc-medium-margin;
}
.artist-edit-form-name, .artist-edit-form-genre, .artist-edit-form-submit {
@extend .control-group;
}
.artist-edit-form-label {
@extend .input-label;
}
.artist-edit-form-input {
@extend .input-large;
}
.artist-edit-form-label-required {
@extend .input-required;
}
.artist-edit-form-submit-button {
@extend .button-primary;
@extend .button-medium;
}
This styling is pretty basic but does the job. As we've added a new file, we'll need to restart gulp local
again, and when you refresh your local site you should see the form styled simply, but nicely. When creating things like lists and forms, it is worth investigating the structure of similar modules (i.e., their templates and Sass).
Code samples are licensed under the Universal Permissive License (UPL), and may rely on Third-Party Licenses