Post Featured Image

Change Image Content Depending on Device Type

This blog uses the extension framework to deliver the customization (available since Aconcagua), but relies on technology available since Denali — therefore, it can be adapted for older sites.

A question I was asked recently was, "is it possible to show different images in the homepage carousel depending on the user's device?". The answer is: yes, of course! Here's an example I've put together to illustrate what happens at different device widths — see what happens when I resize the viewport and refresh the page:

We've talked before about the homepage — a while ago in the now-out-of-date blog post on customizing it and in the rather experimental look at implementing flexbox, but this is quite simple and relates to something that a lot seem to have on their homepage: carousels or sliders.

The homepage is a personal page to each organization. The default we have in our base theme is really just full of placeholders, and the default way of building up carousel content is a bit lacking. So, I'm assuming at this point that you're either using one of our pre-built NetSuite themes, or you've branched out and built your own.

Therefore, when you take a look at my example module, I'd strongly suggest not using it as a direct template but instead as a study aid for how I have customized my specific site.

The Mechanisms

Before diving into the code, let me give a brief overview of how I approached this problem, and what the key take-aways are.

Device Detection

The TL;DR of this is that there are some built-in utility functions in the SuiteCommerce source code — available since way back in 2015.2/Denali — that perform (from a crude point of view, I would say) device detection. We make these available in the Utils.js module, which is a very handy helper module if you're not already familiar with it, whose methods we attach to Underscore's global namespace (_) for ease of access.

What this means is that you have in your arsenal the following methods:

  • getDeviceType(widthToCheck) — if you don't pass it an integer value, it returns a string of either desktop, tablet or mobile depending on the current viewport width (otherwise it will evaluate the value you passed it)
  • isPhoneDevice(), isTabletDevice(), isDesktopDevice() — each one essentially runs the device type checker function and evaluates the string it returns, and then returns a boolean value

There are a couple others in there which could be useful, but it's generally the second set of functions that are useful for us. In other words, if we want to show images only to mobile users, we can use isPhoneDevice() as the basis for our conditional statement for mobile users, and isTabletDevice() for tablet users.

Generating Paths for New Images

OK, so we've figured out it's possible to know when a user is on a desktop, tablet, or mobile device, now we need to store and access additional images. How?

The easiest way is to mimic what we already do, and that is to create two additional configuration objects to add the configuration record.

As it stands, carousel images are treated like unmanaged resources. For anyone running a site older than Aconcagua, that term is meaningless, but for those on newer code, it means that we're not including the alternative images in our theme bundle. Instead, the administrator is expected to upload the images to the file cabinet themselves. For something like carousel images, this seems correct to me as we would want those in charge of content management to not need access to the theme developer tools to make these changes.

Once they've uploaded the images, they can provide the URLs into newly created configuration sub-tabs and fields that look exactly like the existing sub-tab for carousel images.

From there, we can copy the code for generating the correct URL paths for them.

Changing the Existing Image Paths

Finally, how do you change the code so that it shows the appropriate images? After all, it's currently set up to show images in all cases. For that, we're using the generic addToViewContextDefinition() method available on visual components in the extensibility API. Its name correctly indicates that it is primarily to be used to add new properties to a view's getContext() object, but it can also be used to replace existing ones, which is what we're going to do. (For non-extensibility folk, you could just extend the prototype of the view instead.)

With a combination of the above mechanisms, we can create something that will:

  1. Detect the user's device type
  2. Get the partial image paths from the configuration record
  3. Process them so that we can generate the full URL paths needed to load them
  4. Swap out the property in the home view's context object so that when the template renders, it'll show our chosen images

Let's take a look at the code!

Entry Point File

I'm going to skip the creation of the extension and module, and instead tell you that this customization requires two files: an entry point file for the JavaScript and a configuration file.

Here is the code for SteveGoldberg.DeviceSpecificCarousel.DeviceSpecificCarousel.js:

define('SteveGoldberg.DeviceSpecificCarousel.DeviceSpecificCarousel'
, [
    'underscore'
  ]
, function
  (
    _
  )
{
  'use strict';

  return {
    mountToApp: function mountToApp (container)
    {
      var Layout = container.getComponent('Layout')
    , Environment = container.getComponent('Environment')
    , replaceCarouselImages = _.isPhoneDevice() || _.isTabletDevice()
    , self = this;

      if (Layout && Environment && replaceCarouselImages)
      {
        var carouselImages =
        _.isPhoneDevice() ? Environment.getConfig('home.carouselImagesMobile')
      : _.isTabletDevice() ? Environment.getConfig('home.carouselImagesTablet')
      : []

        if (carouselImages.length)
        {
          Layout.addToViewContextDefinition('Home.View', 'carouselImages', 'array', function ()
          {
            return self.mapCarouselImages(carouselImages);
          });
        }
      }
    }
  , mapCarouselImages: function mapCarouselImages (urlArray)
    {
      return _.map(urlArray, function (url)
      {
        return _.getAbsoluteUrlOfNonManagedResources(url)
      });
    }
  }
});

The only dependency is Underscore, which we'll be taking advantage of to get our helper functions as well its map() function.

When the module is loaded, we start by declaring the useful variables we're going to need: two extensibility API components, a flag that tests whether the user is using a phone or tablet (we could consider !_.isDesktopDevice() as a replacement), and a reference to this, which we'll need to call the map generator function once we change scope.

To begin, we start with our conditional: we check to see if we have our components and whether our 'non-desktop device' flag is true. If all of that is true, we move on.

Next, we need to determine which images we want to replace the existing ones with. To do this, we break out our device type detectors with a ternary operator (ie a shorthand 'if' function): if it's a phone device, get the property from the config record for the mobile images; if it's a tablet device, do the same but for the tablet images. (We'll talk about these later.)

Then we perform the swapping — if there are images to replace. addToViewContextDefinition() is a method available on any visual component (eg layout, PDP, PLP, cart, etc) that allows us to modify the context object a given view (by adding or overwriting a property of its getContext()). It takes four arguments:

  1. The view class name you want to modify
  2. The property name of that view's context object you want to add or modify
  3. The property type
  4. The callback for generating this property's value

We know it's the home view we're targeting, and that it is its carousel images we want to change. We're going to provide it with an array of URL paths, which we then generate in the callback function.

For the callback function, I moved the map generator to another function. Its code is identical to the one that's in the home view file. Essentially, we anticipate that the administrator will upload new banner images to the file cabinet, and then put in the paths in the configuration record.

Assuming the directory structure is, for example, Web Site Hosting Files > Live Hosting Files > SSP Applications > NetSuite Inc. - SCA 2019.1 > Development then they would likely add their images to the img folder. Thus, in their configuration record they would add the path as img/carousel-home-mobile1.jpg, img/carousel-home-mobile2.jpg and img/carousel-home-mobile3.jpg, for example.

However, this isn't appropriate for loading the images, as we'll need more: we'll need the full URL path — that's where _.getAbsoluteUrlOfNonManagedResources() comes in. If you just type it into your developer console, you'll see it resolves to your site's domain plus the SSP name. This, in combination with the configuration path, will generate the full URL, eg, https://shop.example.com/sca-dev-2019-1/img/carousel-home-mobile1.jpg.

This is where the map function comes in. mapCarouselImages() will take the simple array of images provided by the configuration record and then return a new array with the fully-qualified URLs. When we have that, we just overwrite the existing property with this array. Thus, when it comes to render the view, it has our new data and the device-specific carousel images will load. Nice.

Why Doesn't It Automatically Re-Render the Images When You Resize?

It's not programmed to. You could add a listener to the window's resize event so that it runs the code again and then re-renders the view but that's not necessarily the right thing to do and I wanted to keep the code as simple as possible. It's rare for shoppers to resize their viewport; it may happen if someone rotates their device (and the new width is within a different breakpoint), or if they splitscreen your site with another window. If you need to account for those scenarios, you can!

Keep in mind that if you do this, you'll also need to add code to add back the original/desktop carousel images.

The Configuration File

What we need to do is add two new sub-tabs to the configuration record page where administrators can add their images for mobile and tablet devices.

In the module's Configuration > SteveGoldberg.DeviceSpecificCarousel.DeviceSpecificCarousel.json file, I have:

{
  "properties":
  {
    "home.carouselImagesMobile":
    {
      "group": "layout"
    , "type": "array"
    , "title": "Carousel Images Mobile"
    , "description": "Carousel Image URLs for Mobile Devices"
    , "items":
      {
        "type": "string"
      , "title": "URL"
      }
    , "default": []
    }
  , "home.carouselImagesTablet":
    {
      "group": "layout"
    , "type": "array"
    , "title": "Carousel Images Tablet"
    , "description": "Carousel Image URLs for Tablet Devices"
    , "items":
      {
        "type": "string"
      , "title": "URL"
      }
    , "default": []
    }
  }
}

If you're already familiar with JSON configuration files then this won't look out of the ordinary. We're declaring new properties to add to an existing tab/group; its structure mimics that of the configuration file of the existing carousel images.

Deploy, Activate and Test

Save all your files and push them up to NetSuite. You'll obviously need to activate it. Once that's all done, you'll need to go the file cabinet and navigate to the img folder for your site's SSP and upload some images there (eg three for tablet and three for mobile), and then you'll need to modify the configuration record and put those values in their respective arrays (eg Layout > Carousel Images Tablet and Layout > Carousel Images Mobile).

If you save the configuration record, the only thing left to do is to test!

Resize your site's viewport and and then refresh. If it's within the viewport width of one of the device types, it will show different images. Here's my example:

Finally, keep in mind that if you don't want to maintain a slider using your own code, you can always use one of our pre-built extensions:

  • Slideshow — carousels that you can drag-and-drop and maintain from the site management tools
  • Photo Gallery — not quite a carousel, but has the same sort of purpose but in a different way, visually

Both of these offer the luxury of no developer involvement: just install the bundle and activate, and the ecommerce manager is free to free to add and manage them themself.