Build a Testimonials Module: Part 3

This is the third and final part of a series taking you through the process of adding a testimonials module to your site. If you haven't already, you should read part one and part two first.

In this part we're going to cover:

  1. Using composite and child views to show stars for the rating and a confirmation message
  2. Using plugins and wrapping existing functions to add a carousel to the homepage without editing the homepage's source code
  3. Using collection views and asynchronously loading to pull the data from the server and then populate the carousel with reviews

In short, we're focusing on getting the testimonials after they've been submitted and approved, displaying them, and improving the user's experience while creating and viewing the testimonials.

Child Views and Confirmation Messages

We're also going to work with child views. Essentially these are like views within views; a way of displaying something specific within another view. So, for example, we're going to be adding in a way of ranking things using stars: these stars will be provided using a child view.

Show Success on Submit with a Popup

First things, first, though: let's look at improving the user's experience a little bit. It's customary, and with good reason, to show positive feedback after a user submits a form. You'll note that when we submit a testimonial, the only we know it's been successful is if we check the backend and see if it's turned up (or we keep the Network tab open in dev tools). Let's do this.

To illustrate the principle, we're going to add something very simple: we're going to make a deliciously annoying popup appear on a successful submission of a testimonial. Don't worry, we'll change this later, but for now let's revel in its simplicity and annoyingness.

In Testimonials.Form.View.js, add jQuery as a dependency. Then add the following code to the initialize function:

this.model.on('sync', jQuery.proxy(this, 'showSuccess'));

 

We've set up an event handler so that when the model's data is synced with the NetSuite backend it runs the showSuccess function. We don't have that yet, so add the following to the return statement:

, showSuccess: function showSuccess() {
    alert('You did it!');
    this.model.clear();
    this.render();
  }

 

Now, go to your local site and submit a testimonial. After it successfully submits you should see your lovely popup congratulating you on your mighty feat.

So the first line of the new function creates the alert, but you'll note the next two lines: we clear the model and then re-render it. These two lines reset the form so that they can use it again, should they wish. Neat.

Before we move on, let's make a small change. Instead of the using the hardcoded confirmation message, let's swap it for a property value. Add underscore as a dependency and then add a new property to the return statement:

, successMessage: _('Thank you for your testimonial. We\'ll review and publish it soon.').translate()

 

Note that we have to escape the apostrophe so it doesn't interfere with the code. Then, in the showSuccess function, replace the alert code with:

alert(this.successMessage);

If you refresh the testimonials page and enter a testimonial. It should still work.

Using the Global Messaging System

Included in SCA is the ability to pop neat, little, confirmation messages up on the screen. As you'll know, this is clearly superior to a JS popup.

The first thing we need to do is add a line to the testimonials template. We need to add an element to which the message will be sent to. In testimonials_form.tpl add the following line below the header element:

<div data-confirm-message></div>

 

We don't need to put anything more in because what we're going to do next is add a dependency to our view that pulls in a child view to the template. This view uses its own template and Sass to make it functional and look good. This not only makes it easy to implement but it also ensures consistency throughout the site.

With that in mind, let's add the child view to our view. In the view, add GlobalViews.Message.View as a dependency, then replace the showSuccess function with the following:

, showSuccess: function showSuccess() {
this.showConfirmationMessage(this.successMessage, true);
}

 

Then just add a new property at the top of to the return statement:

showSuccessMessage: false

 

This just sets a default. Anyway, we've done the implementation now. Refresh the page, submit a testimonial and on success you should see something like:

Great stuff, well done!

Child View for Rating with Stars

We're also going to add a way of using stars as a way selecting and displaying a rating. Head back to the template. Much like for the confirmation message, we're going to add a child view for the stars to load in. Delete the entire control group for the rating system, we don't need it anymore. Then replace it with the following:

<div data-view="Testimonial.StarRating" data-validation="control-group" name="rating" class="testimonial-form-global-star-rating"></div>

 

Now we need to make a series of changes to our view to utilize the stars. for this, I'm going to provide you with the code wholesale and then tell what changes we've made:

define('Testimonials.Form.View'
, [
    'testimonials_form.tpl'
  , 'GlobalViews.Message.View'
  , 'GlobalViews.StarRating.View'
  , 'Backbone'
  , 'Backbone.FormView'
  , 'Backbone.CompositeView'
  , 'jQuery'
  , 'underscore'
  ]
, function TestimonialsFormView(
    testimonialsFormTpl
  , GlobalViewsMessageView
  , GlobalViewsStarRatingView
  , Backbone
  , BackboneFormView
  , BackboneCompositeView
  , jQuery
  , _
  )
{
  'use strict';

  return Backbone.View.extend({

    showSuccessMessage: false

  , template: testimonialsFormTpl

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

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

  , events: {
      'submit form': 'saveForm'
    , 'rate [data-toggle="rater"]': 'rate'
    }

  , initialize: function initialize() {
      this.model.on('sync', jQuery.proxy(this, 'showSuccess'));
      this.initializeModel();
      BackboneCompositeView.add(this);
      BackboneFormView.add(this);
    }

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

  , childViews: {
      'Testimonial.StarRating': function TestimonialStarRating() {
        return new GlobalViewsStarRatingView({
          showRatingCount: false
        , isWritable: true
        , value: this.model.get('rating')
        , label: 'Rating'
        , name: 'rating'
        });
      }
    }

  , showContent: function showContent() {
      var self = this;
      this.options.application.getLayout().showContent(this).done(
        function afterShowContent() {
          self.$('[data-toggle="rater"]').rater();
        }
      );
    }

  , initializeModel: function initializeModel() {
      this.model.clear().set('rating', 0);
    }

  , showSuccess: function showSuccess() {
      this.initializeModel();
      this.showContent();
      this.showConfirmationMessage(this.successMessage, true);
    }

  , rate: function rate(e, rater) {
      this.model.set(rater.name, rater.value);
    }

  , successMessage: _('Thank you for your testimonial. We\'ll review and publish it soon.').translate()
  });
});

 

Here's what's happening:

  1. Two new dependencies — we have a global view already packaged in SCA for star ratings, so you can use that easily and simply. We're also adding in the composite view which will be used to display the stars.
  2. Bindings — we've removed the bindings to the rater. We're still going to validate the field, but we can't do it this way anymore. More on this later.
  3. Events — because of the different way we're handling the system for rating, we now need to add in an event for when a rating is entered. We're going to use this to trigger the rate function later.
  4. Initialize — two new things here: we've add in a call to a function that will initialize the model and then some code to initialize the composite view.
  5. Child Views — as with the confirmation message, we're going to load in the star rating system as a child view. We need to declare some properties for it and you'll note that we're pulling the rating from the model.
  6. Show Content — by default, every view runs a showContent function. However, if you want to run a customized version of it then you can declare it in your view and then add what you want. Here, we've added in a function to run after the application is done rendering the content. This function associates the stars rating field with the star rating functionality: check out GlobalViews.RatingByStar.View.js in GlobalViews and Bootstrap.Rate.js in BootstrapExtras to see how the code works.
  7. Initialize Model — we're going to use this twice. It simply resets the form and resets the rating to 0. This happens when the module is initialized and after we've successfully submitted a testimonial.
  8. Show Success — we've modified it so that it runs initializeModel and the modified showContent.
  9. Rate — finally, once we have the rating, this function sends the data to the model so it can be passed on to the backend. You'll note that this is the function that is called when the rate event is triggered.

So there have been quite a few changes here. Next we need ot make some changes to the frontend model. In JavaScript > Testimonials.Model.js, replace the rating code with the following:

, rating: function rating(value) {
    var intValue = parseInt(value, 10);
    var isValid = intValue > 0 && intValue < 6;

    if (!isValid) {
      return _('Rating is required').translate();
    }
  }

 

This is a pretty simple validation check: parse the value as an integer and then check that it's greater than 0 and less than 6. If it's not valid, then we return an error message.

Now that we've made the requisite changes to the template, view and model, we can now refresh the testimonials page on our local site and try out the changes.

Test success and failure to check that the rating system does, in fact, work and that the validation triggers when some of the fields are left empty. When you're done, we can move on to the next part: displaying our testimonials.

List Testimonials on the Homepage

What better place to let people know how great our site is by advertising it on the homepage?

The goal is to retrieve the testimonials from the backend, then display them on a page using a carousel. The way we're going to do this teaches an important lesson: you can enhance/modify existing views by not editing them directly. We're going to wrap existing functions and use plugins to get them to appear.

Enhance an Existing View

Before we jump in, let's take a look at a useful feature of Backbone. When we wanted to add a form, we created an entirely new view, put our stuff in it, and then deployed it. But what happens when you want to modify an existing one? Well, sure, you could create a copy, put your changes in, and then override it. But there's a more efficient, cleaner way of doing it: wrapping.

To start, let's add in a view that we'll use to display the carousel. Create Testimonials.Carousel.View.js in JavaScript and, for now, put the following in it:

define('Testimonials.Carousel.View'
, [
    'Backbone'
  , 'testimonials_carousel.tpl'
  ]
, function TestimonialsCarouselView(
    Backbone
  , testimonialsCarouselTpl
  )
{
  'use strict';

  return Backbone.View.extend({
    template: testimonialsCarouselTpl
  , initialize: function initialize(){
      console.log('Carousel initialized');
    }
  })
});

 

We're also going to quickly add a template to render when they view has been initialized. Create testimonials_carousel.tpl and put the following in it:

<h1 style="text-align:center;">I'm not a carousel, but I will be one in the near future!</h1>

 

Now we need to modify Testimonials.js so that we have some code to plug the carousel into the view. Like I did previously, I'm going to suggest a wholesale removal and replacement of code and then explain the changes. Replace the code in Testimonials.js with this:

define('Testimonials'
, [
    'Testimonials.Router'
  , 'Testimonials.Carousel.View'
  , 'Home.View'
  , 'Backbone.CompositeView'
  , 'PluginContainer'
  , 'underscore'
  ]
, function (
    Router
  , CarouselView
  , Home
  , BackboneCompositeView
  , PluginContainer
  , _
  )
{
  'use strict';

  return {

    plugCarouselIntoView: function plugCarouselIntoView(View, application, afterSelector) {

      if (!View.prototype.visitChildren) {
        View.prototype.initialize = _.wrap(View.prototype.initialize, function wrap(fn) {
            fn.apply(this, _.toArray(arguments).slice(1));
            BackboneCompositeView.add(this);
        });
      }

      View.prototype.preRenderPlugins = View.prototype.preRenderPlugins || new PluginContainer();

      View.prototype.preRenderPlugins.install({
        name: 'Testimonials.Carousel'
      , execute: function execute($el) {
          $el.find(afterSelector)
            .after('<div data-view="Testimonials.Carousel"></div>');
        }
      });

      View.addExtraChildrenViews({
        'Testimonials.Carousel': function wrapperFunction() {
          return function() {
            return new CarouselView({
              application: application
            });
          };
        }
      });

    }

  , mountToApp: function(application) {

      this.plugCarouselIntoView(Home, application, '.home-slider-container');

      return new Router(application);
    }

  }
});

 

So let's talk about what's going on here:

Dependencies

We've got five new dependencies:

  1. Carousel view — naturally we include this because it's the thing we want to plug into something.
  2. Home view — the thing we're plugging it into.
  3. Backbone composite view — if you remember, composite views are used when you have a repeating list of models but you want to wrap them in another view for display purposes. What we want to do is grab a bunch of testimonials, and display them on the homepage. A composite view is perfect for this.
  4. Plugin container — this contains a group of useful functions that allow us to 'inject' code at certain points in the application's processing.
  5. Underscore — the way we wrap existing functions requires the use of Underscore.

So with those new files added, we can look at the code itself. We've added the plugCarouselIntoView function, which does the job it's named after. Let's look at the particulars of it.

Prepare for Composite View

The first part is to check whether the view we're going to plug into is already a composite view; if it isn't, then we need to modify it. To do that, we're using the visitChildren method, which we include in Backbone.CompositeView.js. The function itself recursively goes through all the child views. We use it and, if it finds none, converts the current view to a composite view.

Initialize and Install the Pre-Render Plugin

In conjunction with PluginContainer.js, Backbone.View.render.js allows us to execute code at various stages of the processing of templates. We offer four hooks:

  1. Pre-compile — before the template function is executed and generates an HTML string
  2. Post-compile — after the template function has executed and generated an HTML string
  3. Pre-render — before the template string is appended to the DOM
  4. Post-render — after the template string has been appended to the DOM

So first we have to initialize it and then install it. install is a method in PluginContainer.js that adds it to the array of plugins to be executed. We're telling it to execute it pre-render. It's named Testimonials.Carousel, which is what will be the data-view parameter on the element in the rendered page. And then we tell it what to execute, which is to take the element, find the selector we want (afterSelector) and then append an element for the child view to populate.

Add the Carousel View as a Child View

View.addExtraChildrenViews creates the code for Testimonials.Carousel, which is what is executed by the pre-render plugin. You'll see that it's returning a new CarouselView and is the final part of this function. All that's left, then, is to actually plug the carousel into the view, and we add that to the mountToApp function. We run the function specifying that it should be added to the homepage view below the element with the selector .home-slider-container. Note that if you don't have this selector on your homepage, then you can swap it for whatever one you like.

Now we're in a position to test! As we added new files, you'll need to restart your local server. When you've done that, head to your homepage and you should see something like this:

Great! Now we're ready to fetch data from the server and show it!

Fetch the Collection

Collections are ordered sets of models. A simple way to think of them is to imagine models as like songs and collections are albums. Songs are individual and aren't bound to a particular album, and can pop up in numerous places. Maybe the song will be released on a band's latest album, but maybe it'll also appear on their greatest hits album. In our situation, each testimonial is going to be added to a collection of testimonials.

GET Method

You'll remember that we've already written the backend code to create testimonials, so let's add the code to get them.

Open up SuiteScript > Testimonials.Model.js and the following below the create function:

, list: function list() {
    var paginatedSearchResults;

    var filters = [
      new nlobjSearchFilter('custrecord_t_status', null, 'is', '2')
    , new nlobjSearchFilter('isinactive', null, 'is', 'F')
    ];

    var columns = [
      new nlobjSearchColumn('name')
    , new nlobjSearchColumn('custrecord_t_rating')
    , new nlobjSearchColumn('custrecord_t_entity_name')
    , new nlobjSearchColumn('custrecord_t_text')
    , new nlobjSearchColumn('custrecord_t_creation_date')
    ];

    paginatedSearchResults = Application.getPaginatedSearchResults({
      results_per_page: 20
    , columns: columns
    , filters: filters
    , record_type: 'customrecord_testimonial'
    });

    if (paginatedSearchResults.records && paginatedSearchResults.records.length > 0) {
      paginatedSearchResults.records = _.map(paginatedSearchResults.records, function mapRecord(record) {
         return {
           title: record.getValue('name'),
           text: record.getValue('custrecord_t_text'),
           createdDate: record.getValue('custrecord_t_creation_date'),
           rating: record.getValue('custrecord_t_rating'),
           writerName: record.getValue('custrecord_t_entity_name')
         };
      });
    }
    return paginatedSearchResults;
  }

 

Here's what's happening:

  1. The creation of the filters. These essentially are the search request we're sending to the API (ie, the field type). Note that for the status we've specified the ID of 2 — this is the ID of the Approved status, so change it if you need to.
  2. The creation of the columns. These are the specific fields that we want returned.
  3. The submission of the search request.
  4. The creation of an object with the returned values.

Once we've done that we need to update the service so that it has a GET method. Open up Testimonials.Service.ss and add a method to the switch:

case 'GET':
  Application.sendContent(Testimonials.list());
  break;

 

Great stuff! Let's move on.

View and Collection View

So we've got two things we need to do here: we've got to add the collection view, and then we've got to make the view and application aware of it.

In JavaScript, create Testimonials.Collection.js and put the following in it:

define('Testimonials.Collection'
, [
    'Testimonials.Model'
  , 'Backbone.CachedCollection'
  , 'underscore'
  , 'Utils'
  ]
, function TestimonialsCollection(
    Model
  , BackboneCachedCollection
  , _
  , Util
  )
{
  'use strict'

  return BackboneCachedCollection.extend({
    url: Util.getAbsoluteUrl('services/Testimonials.Service.ss')
  , model: Model
  , parse: function parse(response) {
      return response.records;
    }
  });
});

Here we're using something from BackboneExtras: Backbone.CachedCollection, which is an extension of the original Backbone.Collection. It provides the same functionality as a collection but adds in a layer of caching within Backbone, reducing load and improving performance on follow-up hits to the collection.

The only other thing of note is the parse function: as the testimonials are in the records property, we can ignore the rest of the response. Neat.

Modify the View

Next we need to update Testimonials.Carousel.View.js. Replace the initialize function with the following:

, initialize: function initialize() {
    this.collectionPromise = this.collection.fetch();
  }

, render: function render() {
    var self = this;
    this.collectionPromise.done(function doneCollection() {
      self._render();
    });
  }

, getContext: function getContext() {
    return {
      content: JSON.stringify(this.collection.toJSON())
    };
  }

 

So, firstly, we've given the initialize function something to do: tell the collection to fetch the data.

Next up we have the render function which waits for the loading to be completed and then, well, renders it.

Finally, we have the setting of the context object, which returns the results of the collection as a string.

Update Entry Point File and Template

The final part is to update the entry point file and print something out.

Open up Testimonials.js and add Testimonials.Collection as a dependency. Then, within the return new CarouselView statement, add:

, collection: new Collection()

 

Now one final thing: edit the template file and add in:

<pre>{{content}}</pre>

 

That's it! Now we just need to deploy our changes. Remember, we made some backend changes so you'll need to run gulp deploy --dev to push them up to the server. When that's done, restart your local server and refresh your homepage. When you do, you should see something like this:

If you don't see anything, remember that you have to go into the backend of NetSuite and approve some of your testimonials! To do this, go to Customization > Lists, Records & Fields > Record Types and then click the List link in the row for Testimonial. Edit a bunch of them and change their status to Approved.

Turn the Testimonials into a Carousel

Finally, the bit you've probably been waiting for: let's turn this lump of JSON data into a carousel!

For this we need to:

  1. Add a view and a template to display each of the testimonials
  2. Modify the carousel view to become a collection view
  3. Add the bxSlider functionality, which is included with SCA

Add a Testimonial View and Template

So the plan is to use a collection to display the testimonials. And as I said before, collections are sets of models. This means for each of the testimonials, we need to a view and a template to display the data for each of the testimonials. Let's add them now.

Create JavaScript > Testimonials.Testimonial.View.js and in it put the following:

define('Testimonials.Testimonial.View'
, [
    'testimonials_testimonial.tpl'
  , 'GlobalViews.StarRating.View'
  , 'Backbone'
  , 'Backbone.CompositeView'
  ]
, function TestimonialsTestimonialView(
    testimonialsTestimonialTpl
  , GlobalViewsStarRatingView
  , Backbone
  , BackboneCompositeView
  )
{
  'use strict';

  return Backbone.View.extend({

    template: testimonialsTestimonialTpl

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

  , childViews: {
      'StarRating': function() {
        return new GlobalViewsStarRatingView({
          model: this.model
        , showRatingCount: false
        });
      }
    }

  , getContext: function getContext() {
      return {
        createdDate: this.model.get('createdDate')
      , writerName: this.model.get('writerName')
      , title: this.model.get('title')
      , text: this.model.get('text')
      };
    }
  });
});

 

I think that this code is pretty self-explanatory by now. This new view is a composite view as it's going to have a child view: the one we use to display stars. Then we set the context by pulling in the data from the model.

Let's add a template. Create testimonials_testimonial.tpl and put the following in it:

<div class="testimonials-testimonial">
    <span><div data-view="StarRating"></div>{{translate '<b>$(0)</b>: <i>$(1)</i> <small>($(2))</small>' writerName title createdDate}}</span>
    <p>{{text}}</p>
</div>

With the view and template in place, we nearly in a position to show the testimonials formatted on the homepage. But we need to make a modification to carousel view first.

Prepare the Carousel View

Head back to Testimonials.Carousel.View.js. Replace its contents with this:

define('Testimonials.Carousel.View'
, [
    'Testimonials.Testimonial.View'
  , 'testimonials_carousel.tpl'
  , 'Backbone'
  , 'Backbone.CompositeView'
  , 'Backbone.CollectionView'
  , 'underscore'
  , 'jQuery'
  ]
, function TestimonialsCarouselView(
    TestimonialView
  , testimonialsCarouselTpl
  , Backbone
  , BackboneCompositeView
  , BackboneCollectionView
  , _
  , jQuery
  )
{
  'use strict';

  return Backbone.View.extend({

    template: testimonialsCarouselTpl

  , initialize: function initialize() {
      BackboneCompositeView.add(this);
      this.listenTo(this.collection, 'sync', _.debounce(jQuery.proxy(this.render, this)));
      this.collectionPromise = this.collection.fetch();
    }

  , childViews: {
      'Testimonials.Collection': function TestimonialCollectionChildView() {
        return new BackboneCollectionView({
          application: this.application
        , collection: this.collection
        , childView: TestimonialView
        , viewsPerRow: 1
        , childViewOptions: {
            application: this.application
          }
        });
      }
    }

  , getContext: function getContext() {
      console.log('Promise status',this.collectionPromise.state());
      return {
        isReadyToRender: this.collectionPromise.state() === 'resolved'
      };
    }
  });
});

 

Perhaps the biggest thing here is our use of promises. When we make AJAX calls, we have to wait for the data to be returned before we can do anything with it. Promises are deferred actions: you've defined what you want to happen when it resolves, so we have to add in code that deals with it. In the context, we added in a console.log so that we can see its status as it changes.

We also added in code for creating the collection as a child view, and you'll note that we set as a child view, the new testimonial view we just created.

All that's left is to modify the carousel template as all it currently offers us is some blythe text about how it's not what we want it to be. After all, we need this template to render the collection of testimonials. Tab to testimonials_carousel.tpl and change it to:

{{#if isReadyToRender}}
<section class="testimonials-carousel">
    <h1 class="testimonials-carousel-title">{{translate 'What our customers say'}}</h1><a href="/testimonials/new">{{translate 'Leave yours'}}</a>
    <div data-view="Testimonials.Collection"></div>
</section>
{{/if}}

 

Great! That's everything for the basics. Restart your local server and refresh your homepage. You should see something like this:

You can also check your console to see the log messages appear. Now we're just one step away from finishing.

Add the Carousel

As mentioned previously, we include code in SCA to create a carousel. This means that as long as we've set up our code properly, just adding a slider is relatively trivial. For this we use bxSlider, a third-party jQuery slider which is also responsive.

Adding a slider requires only a small modification to the collection view, and the addition of a data attribute to the element. Open up Testimonial.Carousel.View.js and add Utils as a dependency. Then add the following to the bottom of the initialize function:

var self = this;
this.on('afterCompositeViewRender', function afterViewRender() {
  _.initBxSlider(self.$('[data-slider]'), {
    slideSelector: 'div.testimonials-testimonial'
  , mode: 'fade'
  , pager: false
  , auto: false
  });
});

 

_.initBxSlider is the thing doing the work here. You'll note that we've targeting an element with the data-slider attribute, which we'll add shortly. We also told it to target div.testimonials-testimonial, which is the selector for each testimonial in testimonials_testimonial.tpl. The rest of the properties are just optional choices, which you can read more about on the bxSlider website.

Next, open up testimonials_carousel.tpl and add data-slider to the element used for the testimonials collection.

That's it, really. Refresh your local server and you should see a fully functional carousel with buttons:

It doesn't look great, but it's functional. If you want to improve the look, you can refer to our other uses of the slider, for example on the homepage, recently viewed items, and the shopping cart.

Summary

In this tutorial we added the ability for users to send in testimonials and have them displayed on the site. We covered:

  • Creating a custom record type
  • The basics of module development
  • Views and templates, including using inbuilt form functionality to make the process easier
  • Deploying, including to a local server and deploying only backend files to the server
  • Frontend and backend models, which handle our data
  • Validating the data
  • Services, which talk with the APIs to send and retrieve data
  • Using composite and child views, in particular when using the inbuilt star rating system
  • Using inbuilt carousel functionality to finalise the display of our customer testimonials

And with that, Pablo's final thoughts on this tutorial are:

  1. All SuiteCommerce Advanced code is accessible to you, so make use of it. When in doubt, there's probably another module that already does something similar to what you want to do.
  2. Embrace the framework: you'll end up writing less code. Look at key files like ApplicationSkeleton, Backbone.* and SSP libraries.
  3. Don't modify source files directly. You should always extend or override. If you don't you'll find it hard to maintain and upgrade your source code.
  4. SCA is a JS framework. Don't put JavaScript in your templates.
  5. If your page refreshes and it's not an app change, you're doing it wrong.

So where to go from here? Well, you'll probably want to style the carousel. But after that you could consider making it so that only registered users can post testimonials. Perhaps you could send an automated email a week after a customer has placed an order, asking for their feedback? Maybe go the whole way and offer a separate page just for testimonials with pagination, filters and sorting. It's up to you.

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