Post Featured Image

TIL Thursday: How to Make a View Available Globally

This blog post is applicable only to sites running SuiteCommerce, or SuiteCommerce Advanced 2018.2 or newer.

This isn't going to be a particularly long blog post, but I just wanted to highlight a new method added to the base component in 2018.2: registerView(). By extension, it is available in any component that extends it (eg the layout, cart, PDP, and PLP components).

The essence of the method is that it makes it much easier to add views arbitrarily to templates. By registering a view to a component, you make that view available anywhere that component is available. After you run the code, you just need to modify the theme templates of the places you want the view to appear and that's it.

We recommend that when you use this, you try to stick to views that don't rely too heavily on the context of their parent views; however, if you do require a specific parent view, then you should not run into difficulties as long as you're careful.

Use Cases

When we implemented this functionality, we anticipated two broad use cases:

  1. Make it easier to move generic SuiteCommerce elements around in your theme
  2. In customer (your) extensions, you can register a child view and allow that child view to render in a companion theme, specifically in the locations you want

How We Use It

Regarding point one, you should have read in the 2018.2 release notes that we made enhancements to the site search bar. Well, the enhancements make use of this new method: by decoupling the site search elements (the button to reveal it, and the keyword input box), we enable designers to put them where they want with minimum fuss. Our technical writers added a document explaining how you can do this.

Indeed, if you take a look at SiteSearch > JavaScript > SiteSearch.js, you'll see a prime example of how to use registerView:

define('SiteSearch', ['SiteSearch.View','SiteSearch.Button.View'], function(SiteSearchView, SiteSearchButtonView) {
  'use strict';
  return {
    mountToApp: function(application) {
      var layout = application.getComponent('Layout');
      layout.registerView('SiteSearch', function() {
        return new SiteSearchView({ application: application });
      });
      layout.registerView('SiteSearch.Button', function() {
        return new SiteSearchButtonView({ application: application });
      });
    }
  };
});

We use it twice to register two new views to the layout component. Accordingly, wherever the layout component is available (which is globally) you can modify templates (of an extension or a theme) and add data-view="SiteSearch" or data-view="SiteSearch.Button" to generate a keyword search input or site search button respectively.

For example, if you take a look at our base theme in Header > Templates > header.tpl you will see this:

<div class="header-site-search" data-view="SiteSearch" data-type="SiteSearch"></div>

How You Can Use It

What this means is that if you suddenly find yourself wanting to add site search to some arbitrary view/template combo you're working on, you can just do something like this:

<section class="mycoolmodule-info-card">
    <span class="mycoolmodule-info-card-content">{{message}}</span>
    <p>Testing placing the search bar in the header</p>
    <div data-view="SiteSearch.Button"></div>
    <div data-view="SiteSearch"></div>
</section>

Which, if we place just below the logo, looks like this:

Clicking the icon opens the search box for input.

Very simple views can be added via an entry point file in an extension. For example, if I want a "Hello World!" type message to be available anywhere, then I could do something like this:

define('HelloWorld'
, [
    'HelloWorld.View.js'
  ]
, function
  (
    HelloWorldView
  )
{
  'use strict';

  return {
    mountToApp: function mountToApp (container)
    {
      var Layout = container.getComponent('Layout');

      if (Layout)
      {
        Layout.registerView('HelloWorld', function HelloWorld ()
        {
          return new HelloWorldView
        });
      }
    }
  }
})

That's my entry point file in my extension.

Then I just need to create a view and template. My view looks like this:

define('HelloWorld.View'
, [
    'helloworld.tpl'
  ]
, function
  (
    helloworld_tpl
  )
{
  'use strict'

  return Backbone.View.extend({
    template: helloworld_tpl
  })
});

And then my template:

<p>Hello World!<p>

Finally, to render it, just edit any template and put the following in it:

<div data-view="HelloWorld"></div>

Of course, if you want to add some complexity to it, you can pass it options in the constructor, such as the container object.

Why Would I Use This Instead of addChildView()?

If you are a single-site developer, you may not need to worry about it too much. However, the prime benefit is that when it comes to customizing the look-and-feel of your site, it creates a clean separation between your JavaScript and your templates.

Let's take an example: Dan the developer has created a widget using addChildView() and has written it so that it renders on the home page. Denise the designer adds a new element in a home page template so that it shows. They push up their changes and activate them. Some time later, the business wants to change its location: Dan must now change the JavaScript so that it renders somewhere else, and Denise must update the theme. Again, both the JavaScript and theme must be uploaded and activated again.

However, if they use registerView() instead, Denise is the only person who must make a change in order to change its location.

There's also a second benefit: if you're an extension developer then you can ship your extension without forcing your functionality to appear in specific places. Or, to put it another way, customers are not restricted to the specific places you define in your extension; nor do they need to make changes to their JavaScript files to use it. All they would need to do is update their theme's templates!

To be clear: we're not saying that this is a replacement for addChildView(), instead it complements it by providing flexibility in situations where addChildView() can't.

  • addChildView() — I, the JavaScript developer, know exactly where this child view should go, and it's unlikely to move (eg because it is context-specific)
  • registerView() — this view could go in a number of places, sometimes more than one, and it's generic enough to work almost anywhere, so I'm going to leave that up to the theme developer

So, for example, we certainly wouldn't use registerView() for a checkout-only child view.

Can I Use Model Data?

Let's say you have a situation where you've used a service to get some data from NetSuite and you want to put that into the view and also have it available globally, will this work?

The answer is: only if that service has been called at the time.

Typically, we anticipate that you will use this method in an entry point file. These files are called when the modules are first loaded. Models, however, are typically only created when a particularly route has been hit (ie a shopper visits a particular page). If the user has hit that page and you then, at the point of creating your model, register the view then you can pass it along. However, should the user refresh while on another page or if the model gets tidied up, it will disappear.

In short: this is probably a bad idea and goes against the design decision of it. It's really for things that are generic and are meant to be implemented by designers without having to worry about the data.

Can I Use the Plugin Container To Dynamically Generate the View Placeholders?

This was a question I had and so I asked a colleague who worked on it if it would make her sad if I did this and she said that it definitely would.

I know what you're thinking: the PluginContainer module is super cool functionality from yesteryear that let us, for example, add a widget without overriding a module. Its power is that it lets us run some jQuery before/after compilation or before/after rendering a view. Specifically in our use cases, we could potentially use it to modify a templates' HTML to add in a container for our new child view without having to touch the theme at all.

However, as the developer pointed out to me: the point of this method is as a service to the theme. If you're going to use something like this, then you're better off using addChildView(). Furthermore, as the plugin container functionality is not part of the extensibility API, we can't really guarantee its stability or availability going forward. It's risky to use, especially in something like an extension.

Summary

One of the key tenets of our approach to extensibility is that code is separated and modularized, and that functionality is easy to package up and move about as required. The registerView() method is available in the Layout component, and is therefore available globally. It enables you to make parts of your site, specifically views, available globally — this is perfect for re-usable chunks of code that don't care for a specific context.

In our example, we looked at the site search features, which we split up into two: one view to show the toggle button for showing/hiding the keyword search box, and the other for the search itself. This decouples the functionality from its original location, meaning that theme designers can decide to put these wherever they like into their theme without having to change the JavaScript.

This method is not appropriate for functionality that requires data specific to a particular location on the site. For example, if we wanted to add a view to the checkout, it will still be appropriate to use the addChildView() method.