GETTING STARTED WITH CONTROLLERS, MODELS AND DECORATORS (SFCC)

Controllers were introduced as an update of pipelines and, eventually, they replaced them completely. They are one of the most important parts of the Salesforce Commerce Cloud project because they control the flow of data in our application. On the other hand, models fetch data from a server and provide it as a JSON object that will be used to render a page. In this article, we will show you how you can build on your existing RefArch site (or SFRA) to add new functionality with controllers and models.

Prerequisites

You need to have an access to a sandbox and set up your workspace. It can be Visual Studio Code or Eclipse. In a blog post about Eclipse, there is a detailed explanation on how to add a cartridge. Here we will be using Salesforce Commerce Cloud SFRA as a base cartridge, and we will extend it with the functionality we need. Note: you need access to Salesforce Commerce Cloud GitHub in order to download it.  

 

What are controllers?

Controllers are scripts that run server-side and handle storefront requests. Their main function is to manage the flow of data in our application and create ViewModels to process each storefront request as a route and generate an appropriate response. For example, in a storefront application, clicking on the login button or opening a category triggers a controller that renders a page. JavaScript and Salesforce B2C Commerce script are used to write controllers, and right now they must comply with Rhino 1.7R5 JavaScript, including ECMAScript 5. An extension of a controller file can be either .ds or .js, but we should only use .js since it is preferable to use it in a new architecture. Controllers must be placed in the controllers folder at the top level of the cartridge, as it is shown in the image below. If we want to use controller methods, we must export them. Otherwise, they won’t be able to handle storefront requests.

Image removed.

 

To create our Hello.js controller all we had to do was right-click on the controllers folder and add a new file called Hello.js to it. After that, we could add our own business logic inside of it. To do that we need to learn a little bit more about controllers.

 

Controller routes and endpoints

When we go to a specific URL endpoint on the storefront we activate a route. A simple example of this would be in our Hello.js controller.

'use strict';

const server = require('server');

server.get('Show', function (req, res, next) {

    res.render('helloWorld');

    next();

});

If we use the same code inside of it as shown in the image above, our Hello-Show route will be created when we visit the storefront URL https://instance-name.demandware.net/on/demandware.store/Sites-RefArch-Site/Hello-Show.

 

This new route will be created with the code that is inside server.get because, as we can see, it refers specifically to Show. We can have as many routes as we need. To invoke them using a storefront URL they all follow the convention controller Name-routeName which in our case was Hello-Show. 

 

In case your storefront URLs are not in format like the one I have used as an example, you can change that by going to Merchant Tools > Site Preferences > Storefront URL and disable Enable Storefront URLs. This will make storefront use these legacy URLs that will show you this controller-route format when you navigate to any page on storefront. In case we want to use pretty urls for our controller, we can go to Merchant Tools > SEO > URL Rules and click on the Pipeline URLs tab. There we have to enable Append Trailing Slash to Pipeline URLs and Perform Automatic Redirect. That way we can customize our controller-route URLs with the help of alias. For example, we can change our route Hello-Show to be displayed as practice, so now our URL would look like this:

https://instance-name.demandware.net/s/RefArch/practice

Routes can have several different access rights. This is a filtering function provided by Commerce Cloud. Depending on what we need our route for we can choose between:

     - get: used when we want to filter get requests

     - http: used when we want to filter http requests

     - https: used to filter https requests

     - include: used for remote includes

     - post: used to filter post requests

If our request doesn’t match filter condition, we will be shown an error message ‘Params do not match route’.

Do note that we have to export our routes to make them available to the storefront. To do that we simply need to use:

module.exports = server.exports();

In controllers we use require to import script modules or B2C Commerce packages that our controller needs. The best practice regarding performance is to use a lazy loading technique, that is, use require only when you need to.

 

Middleware and req, res, next() in routes 

In SiteGenesis controllers we had guard functionality that helped us filter requests and specify the level of access. Now it has been replaced with middleware in RefArch (or SFRA). Middleware allows us to add functions as a chain, and each function receives req, res, and next() as arguments in that order. 

For Request we use a short version req. This argument contains a server request that stated the execution. In the req object we can find user input information like the user's login and locale information, or session information. The req argument parses query string parameters and stores them inside a req.querystring object.

We use a short version res for Response. This argument contains functionality for returning data back to the client. For example, if we wanted to set cache to expire 24 hours from now, we would use: res.cacheExpiration(24). Or if you want to get current viewData from the response object, you can use res.getViewData.

To inform the server that we have completed the middleware step, and that it can execute the next step in the chain we use next(). 

We can use multiple of these middleware functions to avoid having to rewrite them.

When we register a route we have two options to make a route that has no middleware functions or make one that has middleware functions. In case we want to make a route that doesn’t have any middleware function, we do what we have previously done. Here we have Home-ErrorNotFound as an example of a route with middleware.

server.get('ErrorNotFound', function (req, res, next) {

    res.setStatusCode(404);

    res.render('error/notFound');

    next();

});

We will use a middleware function or functions more frequently. Here, we will build on our previous Hello.js example, and it will now look something like this.

'use strict';



const server = require('server');

const cache = require('*/cartridge/scripts/middleware/cache');



server.extend(module.superModule);



server.replace(

    'Show',

    consentTracking.consent,

    cache.applyDefaultCache,

    function (req, res, next) {

        res.render('/home/practiceHome');

        next();

});

In this case, we have consentTracking.consent and cache.applyDefaultCache as middleware functions. Keep in mind that they always execute as a chain. Each one of these middleware functions does something different. For example, consentTracking.consent is called the first time you visit a page to display consent to tracking so your page is GDPR compliant. While cache.applyDefaultCache caches homepage for a specified amount of time, cache offers some other options regarding duration of caching time you require. In SiteGenesis you would use the <iscache> tag in ISML to control cache. However, now it is done in a controller, which makes it more efficient. 

 

They work in such a way that the server module emits events at every stage of execution, and you can subscribe or unsubscribe to them from a given route. To override the middleware chain, we use an event emitter to remove the event listener, and generate a new one. If we have to change or remove a step in the middleware chain, it is best to replace an entire route. 

 

SFRA doesn’t support all the events used in JavaScript so we will focus only on five of them that are supported.

  • route:Complete is emitted when all the steps in the middleware chain are executed. After that, since the server is subscribed to it, it will render ISML or JSON to the client.
  • route:beforeComplete is emitted just before route:Complete, that is, after all the steps in chain have been completed. We use it to store data that was submitted by the user to the database. Most often it is used in forms.
  • route:Redirect is emitted before res.redirect executes.
  • route:Start is emitted before the first step in the middleware chain.
  • route:Step is emitted before each step in the middleware chain.
server.post('Submit', function (req, res, next) {

    this.on('route:BeforeComplete', function (req, res) {

        let form = server.forms.getForm('practice');

        if (!form.valid) {

            res.setStatusCode(500);

        }

        res.json({ form: server.forms.getForm('practice') });

    });

    next();

});

Here we can see how one of these events would be used in a controller. In this particular case, it is used for a form submission. We call it with this.on after which we specify which event we want to use and our own business logic in that event. 

 

How to extend the functionality of our controllers and models

You can often have multiple layers of cartridges that overlay each other. This allows us to import from the previous cartridge and overlay it. Commerce Cloud offers us a chaining mechanism to make this simple, which allows us to access modules we want to override. A controller can extend or override another controller with the same part and name without having to copy code from one to the other. This results in reusability that SiteGenesis controllers didn't have. 

The global property of module.superModule offers access to the latest cartridge path module that has the same path and name as the current module.

 

<Image removed.

The image above shows us what locating a model or controller would look like in the cartridge path that was used in this example. Based on how SFRA is made it would first look in this custom cartridge. Since we can see here that it has called modele.superModule, we would go one level down to the LINK cartridge where we can see it has also called modele.superModel so we would have to look at the level below to Plugin cartridge and finally we would have to look at base cartridge. This would then go in reverse order; it would first execute base functionality, and then extend it with one before it and so on until it gets to the top. 

 

We have three main methods at our disposal to extend or overwrite a route:

     - append: to extend functionality by executing after the superModule route

     - prepend: to extend functionality by executing before the superModule route

     - replace: overrides the superModule route completely

 

Let’s say we make a new controller called Home.js in the controllers' folder and it looks something like this. 

'use strict';



const server = require('server');



server.extend(module.superModule);



server.prepend('Show', function (req, res, next) {

    let viewData = res.getViewData();

    viewData.param = 'Here we are using prepend';

    res.setViewData(viewData);

    next();

});



module.exports = server.exports();

Here we are inserting functionality before the Show route using server.prepend. The most important thing about this piece of code is server.extend(module.superModule) because it allows us to import the functionality from a controller and extend or override it. Lets first focus on module.superModule - it imports functionality from a controller that has the same name on the right of the current cartridge in the cartridge path. While server.extend inherits the current server object and extends it with new routes from supermodule, in our case it would add all routes Home.js file. Finally, don’t forget to export your functionality or else it won’t be available at storefront. We could use append or replace in the same way we have used prepend.

 

Models

A model is the representation of data in a Model-View-Controller architecture. Models in SFRA are used as serializable JSON objects that represent B2C Commerce system objects. These models use the B2C Commerce script API to retrieve data from the platform for a functional area of the application, such as orders. The models then construct a JSON object that you can use to render a template. SFRA uses a variation of Model-View-Controller architecture in the following way. Controllers are handling information that they get from users and use it to create ViewModels. ViewModel provides the data to render pages in the application and often combines data from multiple B2C Commerce script objects. ViewModels are often interchangeably referred to as models.

Image removed.

In order to create a model you have to first create a models folder inside our cartridge folder as shown in the image above. After that we create a model file. In case you want to extend a base model, use the same name as in base. Here we will extend the account model.

function account(currentCustomer, addressModel, orderModel) {

   module.superModule.call(this, currentCustomer, addressModel, orderModel);

   if(currentCustomer.custom && currentCustomer) {

        this.account.practiceField = currentCustomer.custom.practiceField || null;

   }

}



module.exports = account;

If you look at the image above you can see we have the same function name as in the base model which accepts the same parameters. The first thing it needs to do is call the base model using model.superModule.call and pass parameters to it. Sometimes you might have lots of arguments in a model so it will be better to use module.superModule.apply(this, arguments) instead. After that we can implement the logic we want this new extended model to have and, in the end, we export that model. 

 

Make your life easier with model decorators

Model decorator gives us the ability to add behavior to an object dynamically. This is used so that we can divide a large amount of data for a model into smaller parts that are function-specific. We call these function-specific parts decorators. Then we can use one or more of these decorators to add or override the functionality of the original object. Some of the core models are extendable and configurable through the decorator pattern.

Image removed.

Decorators used for the product in the base can be seen in the models folder under decorators in the product folder. An example of a model decorator would be fullProduct.js from the base cartridge, and it uses an index of decorators. This is accomplished by using:

let decorators = require('*/cartridge/models/product/decorators/index');

module.exports = function fullProduct(product, apiProduct, options) {

decorators.base(product, apiProduct, options.productType);

decorators.price(product, apiProduct, options.promotions, false, options.optionModel);



if (options.variationModel) {

    decorators.images(product, options.variationModel, { types: ['large', 'small'], quantity: 'all' });

} else {

    decorators.images(product, apiProduct, { types: ['large', 'small'], quantity: 'all' });

}



decorators.quantity(product, apiProduct, options.quantity);

decorators.variationAttributes(product, options.variationModel, {

    attributes: '*',

    endPoint: 'Variation'

});

decorators.description(product, apiProduct);

decorators.ratings(product);

Now, instead of making these smaller pieces in every model we need, we can simply just use decorators we need. We can look at decorators as subsets of the model that allows us to extend the model. Here we can see that decorators are simple to use - we just pass to those we need our model to see. That way we have made extending our model pretty simple. This ease of use is what makes them so great, especially for larger models since we usually have more reusable parts there. Now let's take a closer look at what is inside one of these decorators.

'use strict';



module.exports = function (object, product, quantity) {

    Object.defineProperty(object, 'isOrderable', {

        enumerable: true,

        value: product.availabilityModel.isOrderable(quantity)

    });

};

When we look inside orderable.js, we can see it is not a standard function. It accepts a product and certain quantity, and it uses Object.defineProperty to define if it is available in the quantity that is requested on our object and returns it. This will later be used to render that part of the page to the user and show them if they can order that product in the quantity they need. 

One thing to note about decorators is that the <isdecorate> tag that is used in ISML has nothing to do with model decorators. 

From time to time we will need to extend a certain utility so we can use it in multiple different models. For this we use helper scripts, and, in our case, we will extend accountHelpers.js script.

'use strict';



const assign = require('server/assign');



function updateAccountFields(newAccount, account) {

    module.superModule.updateAddressFields(newAccount, account);

    newAccount.custom.practiceField = account.practiceField;

}



module.exports = assign(module.superModule, {

    updateAccountFields: updateAccountFields

});

This allows us to extend accountHelpers from base. In this case, this is simply copying some information from the account object and saving it into the system. One thing that is new to us here is this assign which allows us to extend our helper script function with additional functionality that we need. When we are using assign in our module.export we are first calling module.superModule we are importing all functionality from the base cartridge of this helper script after that we are extending it with our own functionality. After we have extended our helper script we can use it in our models by using require keyword and call it when we need to. 

 

Parting words

As we have seen controllers and models are pretty straightforward in SFCC. You can learn them quickly even if you don’t have any prior experience with this platform. Hope this has expanded your knowledge so you can start working on your project right away! Make your first steps in learning SFCC. If you are interested in further research of SFCC, check out Srđan’s blog about SFCC OCAPI and Hooks.