Develop Your First SuiteCommerce Advanced Module (Kilimanjaro) - Part 4: Styling and User Experience

This tutorial is the fourth and final part of a 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 part three of the tutorial, we upgraded our simple module to a fully functional, transactional multi-page module that enabled users to create, read, update and delete user preference records on their site. However, we are far from finished: there are still so many factors to module development left to cover.

In short, the experience is sub-optimal. While we added in a modal to delete records, it is still a hassle to navigate through the functionality without knowing the direct links. Furthermore, the pages themselves all look rather terrible and are in dire need of styling.

Add an Edit Record Link

We added a button in the previous tutorial that enabled users to delete records, but to improve the user experience, we should add links for adding new records, and editing existing ones.

Add a Link to the Details Template

Remember, in our router set up a route so that if a user went to #preferences/:id they would be taken to a page where they can view and edit an existing user preference of the ID they provided. Thus, it's not hard to conceive of a list page which has a button next to each entry, which has this route as its href.

Open up user_preferences_details.tpl and add a new cell before the existing button:

<td><a href="/preferences/{{internalid}}">{{translate 'Edit'}}</a></td>

Next, we need to accomodate it in to our list template.

Add a colspan to the Actions Column in the List Template

The simplest thing we can do is collapse the columns for the two buttons together.

In user_preferences_list.tpl, change the Actions header cell to:

<th colspan="2">{{translate 'Actions'}}</th>

If you spin up your local server, you can hit up the list page and see something like this:

If you click the edit links, you'll be taken straight to the page for that record.

Add an Add Record Link

Sure, we can go to /new to trigger the new record form, but let's just add a link, quickly.

In user_preferences_list.tpl, add a link near the top:

<p><a href="/preferences/add">{{translate 'Add New'}}</a></p>

After refreshing the page, your new link will appear:

Checking if a Model is New

For creating and editing a record, we share a view and template. In our text, we put slashes in (ie "Add/Update") so that regardless of which one the user was doing, the text would be appropriate. However, wouldn't it be super cool if we could use the same template but show different text depending on the situation? Well, we can.

Add a Check in the Edit View

In UserPreferences.Edit.View.js, update the getContext function with a new method:

, isNew: this.model.isNew()

This will return true when we create a new model (for the purposes of adding a new record) and return false when we view an existing record.

Use the Check in the Edit Template

If you open up the list template, user_preferences_list.tpl, you can swap out the h1 tag for this:

<h1>
    {{#if isNew}}
        {{translate 'Add User Preference'}}
    {{else}}
        {{translate 'Update User Preference'}}
    {{/if}}
</h1>

You can see that we're using the if helper to perform a check to see if it's true; depending on the outcome we show different text.

Generally speaking, Handlebars is a logicless templating framework, which means that we avoid putting logic in the template files. If you look through the full list of built-in helpers you'll see that they're all rather basic. While you can add custom helpers, we generally suggest that you avoid introducing logic into them. So, where can you put the logic?

The idea is that this is performed in a JavaScript file. In our example, we could do this in the view: rather than have the text translated and determined in the template, we could perform the check in the view and then send the appropriate (and translated) string to the template. But we're not going to in our example.

Anyway, we still need to do the same thing for the submit button too.

In user_preferences_edit.tpl, replace it with:

<button type="submit">
    {{#if isNew}}
        {{translate 'Add'}}
    {{else}}
        {{translate 'Update'}}
    {{/if}}
</button>

Which results in the following, depending on the context:

Validate Data

Before sending data to NetSuite, it's a good idea to ensure that the data is 'sane', ie that it conforms to a set of minimum requirements. Common sanity checks include that it's not empty, has a minimum or maximum length, pattern matching (eg for postal codes or credit card numbers), or that a user is logged in before they can use the service.

Validation can and should be performed on the both frontend and backend, before data is transmitted to the backend. This prevents the relatively slow process of attempting to write record data, and provides instant feedback to the user about what they've done wrong.

Luckily for us, validation has been built into the SCA application, so we can add a few snippets of code and it'll do the hard work for us.

Add Validation to the Frontend Model

The first place we can start is the place where form data is added to a model (before it's sent up the chain to the server).

Open up JavaScript > UserPreferences.Model.js and add a new property:

, validation:
  {
    'type':
    {
      required: true
    , msg: 'Please select a type'
    }
  , 'value':
    {
      required: true
    , msg: 'Please enter a value'
    }
  }

Each object matches the IDs we use for the fields.

For each, we specified that they must have a value (required) and that a message is returned if they don't have values.

Mark Up the Edit Template

The JavaScript that makes this work is already built-in, and with this object of fields and rules ready, we just need to edit the template.

The template needs two things: a place for error messages to be displayed in, and for the inputs to be marked up so that they can be targeted and highlighted when there are errors with them.

Open user_preferences_edit.tpl, and add the following at the top of the form element:

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

Error messages will be sent to this div.

Now, replace the contents of the fieldset tag with the following:

<fieldset>
    <div data-input="type" data-validation="control-group">
        <label for="type">{{translate 'Type'}}</label>
        <span data-validation="control">
            <select name="type" id="type">
                {{#each typeOptions}}
                    <option value="{{internalid}}" {{#if isSelected}}selected{{/if}}>{{name}}</option>
                {{/each}}
            </select>
        </span>
    </div>

    <div data-input="value" data-validation="control-group">
        <label for="value">{{translate 'Value'}}</label>
        <span data-validation="control">
            <input type="text" name="value" id="value" value="{{value}}">
        </span>
    </div>
</fieldset>

You can see that we've split the inputs up into groups. Each group is marked up with a data-input attribute, which we use to associate the specific validation rules with the specific inputs, as well as data-validation="control-group", which help us target the correct part of the form when a rule is broken. There is also similar markup on span elements surrounding the inputs.

Refresh the page for your local server and then go to add a new user preference. Try to submit the form without entering a value, you should see it fail:

Importantly, if you check the Network tab of your developer tools, you'll see that no call to the service is made when there are validation failures: the user is alerted immediately and the server's time is not wasted by handling improper data.

Add Validation to the Backend Model

We also add validation to the backend model. Why? Frontend JavaScript can be overridden or disabled; SuiteScript files hosted on NetSuite servers cannot. This is a 'belts and braces' type approach, where we make absolutely sure that our pants won't fall down.

Open SuiteScript > UserPreferences.Model.js and add a new property:

, validation:
  {
    'type':
    {
      required: true
    , msg: 'Please select a type'
    }
  , 'value':
    {
      required: true
    , msg: 'Please enter a value'
    }
  }

It's the same as the one used in the frontend model — that's because the same code runs on both the front and back ends.

Then, within the create and update functions, add this code to the top of each:

this.validate(data);

This ensures that the validation is applied to the data before we attempt to save it to the server.

To test this, you will need to comment out the validation object we put in the frontend model — if our data is invalid, then the frontend JavaScript will always catch it first.

When you've done that, run a full deploy up to the servers. After it's completed, hit up the Add and Update pages and see what happens when you try to submit an empty field.

If you want to learn more about the rules available, you can look at the defaultValidators variable in third_parties > backbone.validation > backbone-validation.js or take a look at the author's documentation. For use cases, just take a look at some of the models built into existing SCA modules.

Don't forget to uncomment the validation rules in the frontend model.

Add a Menu Item

At the moment, users can only get to the preferences page if they already know the URL, but that's not very good UX is it? Let's add a link in the account navigation menu.

Open up UserPreferences.js and add a new method:

, MenuItems:
  {
    parent: 'settings'
  , id: 'userpreferenceslist'
  , name: 'User Preferences'
  , url: 'preferences'
  , index: 1
  }

This configuration object specifies the following things:

  • parent: the ID for the menu item that the link will live under
  • id: a HTML ID that will added to the link when it is generated
  • name: the text shown for the link
  • url: the URL (route) that will be used for the link
  • index: the position in the list that it will appear (counting starts from 0)

When you visit the account section on your local server, expand the Settings menu and you will see your link there. Clicking it will take you through to the landing page!

Auto-Expand the Navigation

A neat little user experience improvement we can make to the interface is automatically expanding the place in the menu that we just added.

In UserPreferences.List.View.js and UserPreferences.Edit.View.js, add:

, getSelectedMenu: function ()
  {
    return 'userpreferenceslist'
  }

Note that we're returning the ID we assigned in the MenuItems object we added to the entry point file.

Add Breadcrumbs

When you go up deep within a section, you frequently see breadcrumbs at the top of the section, showing how far you are in a hierarchy. This helps ground the user, so they know exactly where they are, but also provides ways out, should they need to head back.

Open UserPreferences.List.View.js and add a new method:

, getBreadcrumbPages: function ()
  {
    return [{text: 'User Preferences'}]
  }

Now head to UserPreferences.Edit.View.js and add a similar method:

, getBreadcrumbPages: function ()
  {
    if (this.model.isNew())
    {
      return [
        {text: 'User Preferences', href: '/preferences'}
      , {text: 'New'}
      ]
    }
    else
    {
      return [
        {text: 'User Preferences', href: '/preferences'}
      , {text: 'Edit'}
      ]
    }
  }

Hopefully, the text and href options are self-explanatory. Hoever, you should note that when we provide an array of objects, it iterprets the first as the parent and each one a child to the last.

You should also know that the last object passed will never generate a clickable link (the idea being that if a user is already on the page, then they don't need a link to it in the breadcrumbs).

If you refresh your page, you should see one of these:

Prepare for Sass

So far, we have completely ignored the CSS/styling of this module, and now is the time to change that.

SuiteCommerce Advanced uses Sass for its stylesheets. Sass is a scripting language that allows us to write more complex CSS by writing what we want in the language and then have it compile into native CSS. This is all done during the deploy process.

It gives us new syntax, such as variables, mixins and nested rules, thus giving us power tools to make our jobs easier.

There's a lot of nuance and depth to it, which I'm going to skirt over in this tutorial, but you should take a look at their documentation as well past blog posts about Sass, such as the ones on best practices.

Anyway, to prepare for the code, we need to do the following:

  1. Create a Sass directory in the module root
  2. Update the module's ns.package.json file so that it's included with the module code: , "sass": ["Sass/*.scss"]
  3. Update distro.json so "UserPreferences" is included in the dependencies array for myaccount.css

Finally, before we begin, let's go over some important rules (conventions) that we employ:

  1. Generally speaking, Sass and template files have a 1:1 relationship: keep things modular
  2. Most Sass files start with an underscore — this means that they're 'partials' and are going to be included into other, larger Sass files (and aren't going to be rendered by themselves)
  3. Classes in template files are semantic and start with the template name
  4. Use existing variables and 'helper' classes that we built into the base SCA Sass (see the files in Modules > BaseSassStyles > Sass

Spin Up Your Style Guide

A cool built-in tool that we have is a live style guide. This is effectively documentation for Sass, marked up in our code and available to view and browse when working locally.

Stop your local server and then run:

gulp local styleguide

When it finishes running, you can go to http://localhost:3000/ in your browsers to view the guide. It includes all core variables and re-usable functionality. Head over to the Style section to take a look at what's available to us.

You'll see all kinda of variables: we're going to be using ones for spacing and colors, which will cut down on the amount of work we need to do, as well as make it easy, going forward, to ensure consistency throughout the site.

Style the List Template

In the Sass directory, create _user_preferences_list.scss and then add:

.user-preferences-list-header {
    @extend .list-header;
    position: relative;
    margin-bottom: $sc-padding-lv3;
    display: inline-block;
    width: 100%;
}

.user-preferences-list-title {
    @extend .list-header-title;
    float: none;
}

.user-preferences-list-table-header-actions {
    width: 25%;
}

.user-preferences-list-button-new {
    @extend .list-header-button;
    margin-top: $sc-padding-lv4;
    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-padding-lv3;
    }
}

.user-preferences-list-table {
    @extend .recordviews-table;
}

.user-preferences-list-table-header {
    @extend .recordviews-row-header;
    border-top: 1px solid $sc-neutral-shade-300;
}

There's a fair bit of CSS in here, but before we look at it, let's add the classes that they're applying styling to.

Open user_preferences_list.tpl and replace it with:

<section class="user-preferences-list">
    <header class="user-preferences-list-header">
        <h1 class="user-preferences-list-title">{{translate 'User Preferences'}}</h1>
        <a class="user-preferences-list-button-new" href="/preferences/add">{{translate 'Add New'}}</a>
    </header>
    <table class="user-preferences-list-table">
        <thead class="user-preferences-list-table-header">
          <tr>
              <th class="user-preferences-list-table-header-id">{{translate 'Internal ID'}}</th>
              <th class="user-preferences-list-table-header-type">{{translate 'Type'}}</th>
              <th class="user-preferences-list-table-header-value">{{translate 'Value'}}</th>
              <th class="user-preferences-list-table-header-actions" colspan="2">{{translate 'Actions'}}</th>
          </tr>
        </thead>
        <tbody data-view="UserPreferences.Collection"></tbody>
    </table>
</section>

OK, on the face of it, it looks like pretty standard CSS except there's some cool stuff going on.

Firstly, the extend keyword lets us take a previously defined class and use it in our new class — we can then add in additional declarations. In a way, this is like what we're doing with our JavaScript: we have base classes that form the foundations of what we want to do, and then we can build our own customizations on top. if we tried to do this with regular CSS, we would end up repeating ourselves a lot, or all our elements would have multiple classes on them.

Secondly, you can see that in one case, we are nesting a media query with a declaration. What this lets us do is easily create a duplicate declaration, but one that that will only apply when the screen is of a particular width.

Finally, you'll see a whole host of existing variables and classes being used or extended throughout. You can refer to the style guide for what these do, but it may be better to look at the source code.

As mentioned previously, a lot of variables as well as atoms and molecules (ie the building blocks of our styles) are declared within the BaseSassStyles module. However, some bigger bits are declared elsewhere. For example, the list-header classes we're using appear in the ListHeader module. Sometimes you'll need to perform a search on your source code to find where its declared (which will also net some results showing where it's already being used).

If you're using a Mac or other Unix system like I am, then grep can be used; for example:

sgoldberg-macbookpro:SCA sgoldberg$ grep -rl "list-header-title" Modules
Modules/suitecommerce/ListHeader@2.3.2/Sass/_list-header-view.scss
Modules/suitecommerce/Facets@3.2.0/Sass/_facets-facet-browse.scss
Modules/suitecommerce/ProductList@3.0.3/Templates/product_list_details_later.tpl
Modules/suitecommerce/ProductList@3.0.3/Sass/_product-list-details-later.scss
Modules/suitecommerce/BaseSassStyles@3.0.0/Sass/molecules/_list-header.scss
Modules/suitecommerce/Case@2.3.1/Sass/_case-list.scss
Modules/extensions/UserPreferences@1.0.0/Sass/_user_preferences_list.scss

Style the Details Template

Before we take a look at how this looks, we should style the template that is used to generate the rows in the table.

Replace the contents of user_preferences_details.tpl with:

<tr class="user-preferences-details-table-row">
    <td class="user-preferences-details-id">
        <span class="user-preferences-details-label">{{translate 'Internal ID'}}: </span>{{internalid}}
    </td>
    <td class="user-preferences-details-type">
        <span class="user-preferences-details-label">{{translate 'Type'}}: </span>{{type}}
    </td>
    <td class="user-preferences-details-value">
        <span class="user-preferences-details-label">{{translate 'Value'}}: </span>{{value}}
    </td>
    <td><a class="user-preferences-details-edit" href="/preferences/{{internalid}}">{{translate 'Edit'}}</a></td>
    <td><button class="user-preferences-details-delete" data-action="delete" data-id="{{internalid}}">{{translate 'Delete'}}</button></td>
</tr>

So, we're following the same pattern of semantic class names.

One new thing, however, is that we've added labels before the values we're pulling from the context object. Why would we do that if we're building a table? Well, the plan is to hide them when there's enough space to fully render the table; otherwise, we're going to show them when the screensize shrinks and we change the layout of the table.

To wit, in the Sass folder, create _user_preferences_details.scss and in put:

.user-preferences-details-hide-label {
    @media (min-width: $screen-md-min) {
        display: none;
    }
}

.user-preferences-details-table-row {
    @extend .recordviews-row;
}

.user-preferences-details-table-row:hover {
    @extend .recordviews-row:hover;
}

.user-preferences-details-label {
    @media (min-width: $screen-md-min) {
        display: none;
    }
}

.user-preferences-details-edit {
    @extend .button-small;
    @extend .button-tertiary;
    @media (max-width: $screen-md-min) {
        margin-bottom: $sc-margin-lv1;
    }
}

.user-preferences-details-delete {
    @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-padding-lv1;
    }
}

.user-preferences-details-delete:hover {
    background-color: saturate(lighten($sc-color-error, 4), 4);
}

You can see that there isn't anything particularly novel here, until we get to the bottom. I've dropped in two functions called saturate and lighten for use within our CSS.

Imagine this scenario: you've taken the steps to declare color variables that can be used throughout your stylesheets — so that you only have to declare the colors once (making it easier to update them in future) — but now you want to use a variation of that color, one more vivid and lighter than that color. What do you do?

You could manually calculate what color you need and set that as a variable, or you can use functions to calculate for you on the fly. Again, what this means that you can declare once and move on — and if you change your mind about the base color, the functions will simply perform transformations on the new color.

For a full list of functions, check out their documentation.

Stop and restart your local server (style guide is optional). When you visit the list page, you should see something like this:

Try resizing the window and seeing how the appearance changes at different widths.

Style the Edit Template

The final bit of styling we need to do relates to the form that users complete to add or edit user preferences records. You should be familiar with what we're doing by now.

Replace the contents of user_preferences_edit.tpl with:

<section class="user-preferences-edit">
    <header class="user-preferences-edit-header">
        <h1>
            {{#if isNew}}
                {{translate 'Add User Preference'}}
            {{else}}
                {{translate 'Update User Preference'}}
            {{/if}}
        </h1>
    </header>
    <form>
        <div data-type="alert-placeholder"></div>
        <fieldset>
            <small>Required <span class="user-preferences-edit-required">*</span></small>
            <div class="user-preferences-edit-control-group" data-input="type" data-validation="control-group">
                <label class="user-preferences-edit-label" for="type">
                    {{translate 'Type'}}
                    <small><span class="user-preferences-edit-required">*</span></small>
                </label>
                <span data-validation="control">
                    <select class="user-preferences-edit-select" name="type" id="type">
                        {{#each typeOptions}}
                            <option value="{{internalid}}" {{#if isSelected}}selected{{/if}}>{{name}}</option>
                        {{/each}}
                    </select>
                </span>
            </div>

            <div class="user-preferences-edit-control-group" data-input="value" data-validation="control-group">
                <label class="user-preferences-edit-label" for="value">
                    {{translate 'Value'}}
                    <small><span class="user-preferences-edit-required">*</span></small>
                </label>
                <span data-validation="control">
                    <input class="user-preferences-edit-input" type="text" name="value" id="value" value="{{value}}">
                </span>
            </div>
        </fieldset>
        <div class="user-preferences-edit-control-group">
            <button class="user-preferences-edit-submit" type="submit">
                {{#if isNew}}
                    {{translate 'Add'}}
                {{else}}
                    {{translate 'Update'}}
                {{/if}}
            </button>
        </div>
    </form>
</section>

Next, create Sass > _user_preferences_edit.scss with:

.user-preferences-edit {
    @extend .box-column-bordered;
}

.user-preferences-edit-header {
    margin-bottom: $sc-margin-lv3;
}

.user-preferences-edit-control-group {
    @extend .control-group;
    margin: $sc-margin-lv4 0 0 0;
}

.user-preferences-edit-input {
    @extend .input-large;
}

.user-preferences-edit-label-required {
    @extend .required;
}

.user-preferences-edit-submit {
    @extend .button-primary;
    @extend .button-medium;
}

This is basic styling, but like before you can see that we're relying on existing styles.

Stop and restart your local server (we added new files which it needs to track) and then visit a page to add or edit a user preference. It should look like this:

Final Thoughts

And that's it, really. There's obviously a lot more to styling than just this, but this should give you a taste of it.

Good advice for going forward with your own projects is to look at existing parts of the site that have the styling you want to reproduce on your site and then take a look at the source code that generates it.

Anyway, the important take-aways from this, I think, are two-fold:

  1. Sass is a very powerful styling powertool — the one that many designers have been waiting for. From a developer point of view, you can easily ramp up your new functionality by reproducing existing styles with useful features like variables and extend.
  2. SCA includes a lot of pre-defined variables, atoms, molecules and organisms that comprise the base theme of the site. Re-using them will save you a lot of time and code, and will also ensure that your site remains consistent throughout the entire experience.

Finally, let me say that there's a lot more to designing/styling functionality than what we covered in this tutorial. Take a look at our other blog posts on design/styling/Sass or our design hierarchy.

This is the final part of the tutorial for adding new functionality to your site.

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

You can also download a commented version of the code here: UserPreferences@1.0.0-commented.zip.