In this post I'll show you how to customize the output of renderings from the mock Layout Service in Sitecore JavaScript Services (JSS) when you're working in disconnected mode.

If you're interested in customizing the output of renderings from the Layout Service in connected mode, check out my other post.

Layout Service Rendering Data

The data that the Layout Service sends to your JSS app looks something like this (I've omitted a lot of properties for brevity):

{
  "sitecore": {
    "context": {
      "...omitting": "for brevity..."
    },
    "route": {
      "...omitting": "for brevity...",
      "placeholders": {
        "jss-main": [
          {
            "uid": "{2C4A53CC-9DA8-5F51-9D79-6EE2FC671B2D}",
            "componentName": "ContentBlock",
            "dataSource": "available-in-connected-mode",
            "fields": {
              "heading": {
                "value": "Welcome to Sitecore JSS"
              },
              "content": {
                "value": "<p>JSS rocks!</p>\n"
              }
            }
          }
        ]
      }
    }
  }
}

The items in the placeholders object on line 8 are what get served up to your JSS components, like this ContentBlock component from the JSS React template:

const ContentBlock = ({ fields }) => (
  <React.Fragment>
    <Text tag="h2" className="display-4" field={fields.heading} />

    <RichText className="contentDescription" field={fields.content} />
  </React.Fragment>
);

This component is rendering fields, but you could easily consume the uid, componentName, and dataSource properties returned from the Layout Service like below:

const ContentBlock = ({ fields, rendering }) => (
  <React.Fragment>
    <Text tag="h2" className="display-4" field={fields.heading} />

    <RichText className="contentDescription" field={fields.content} />
    <ul>
      <li>uid: {rendering.uid}</li>
      <li>componentName: {rendering.componentName}</li>
      <li>dataSource: {rendering.dataSource}</li>
    </ul>
  </React.Fragment>
);

What if you want to include some other data on your renderings that comes from the server, but not from data source fields? For example, if you wanted to return a list of members from the Atlanta Sitecore User Group as part of your ContentBlock renderings like this:

{
  "uid": "{2C4A53CC-9DA8-5F51-9D79-6EE2FC671B2D}",
  "componentName": "ContentBlock",
  "dataSource": "available-in-connected-mode",
  "fields": {
    "heading": {
      "value": "Welcome to Sitecore JSS"
    },
    "content": {
      "value": "<p>JSS rocks!</p>\n"
    }
  },
  "atlSugMembers": [
    "George \"Sitecore George\" Chang",
    "Martin \"Sitecore Artist\" English",
    "Anastasiya \"JavaScript Ninja\" Flynn",
    "Varun \"Too Many Questions\" Nehra",
    "Craig \"From ATL\" Taylor",
    "Amy \"Sitecore Amy\" Winburn"
  ]
}

With a bit of code you can extend this rendering data however you like.

TL;DR: Show Me the Code!

Dive straight in here on GitHub: https://github.com/coreyasmith/jss-extensible-json-renderings.

The Example

For this post, we'll update the disconnected Layout Service to add ATLSUG members to components with a specific name (e.g., ContentBlock).

This post assumes that you're working with JSS Tech Preview 4. This may not be possible on earlier versions of JSS, and the technique may be simpler and/or quite different in the final version that ships later this year.

Extend Renderings in Disconnected Mode

Most JSS apps are going to begin their life in disconnected mode, which serves up content from yml (or json) files through a Node.js proxy server that runs a mock Layout Service. Fortunately the JSS disconnected mode server is customizable. The customization steps below should work for the Angular, React, and Vue sample apps, though I've only tested for the React app.

Create Custom Disconnected Server

In the JSS sample apps, the disconnected mode server is created in scripts\disconnected-mode-proxy.js with the createDefaultDisconnectedServer method from the sitecore-jss-dev-tools package. This method accepts an options parameter that lets you extend a bit of the disconnected server's functionality: afterMiddlewareRegistered, onListening, onError, and onManifestUpdated. Unfortunately none of these allow us to customize the mock Layout Service that the disconnected mode server uses, so we'll have to create a custom version of createDefaultDisconnectedServer.

disconnected-mode-proxy.js

Open up scripts\disconnected-mode-proxy.js and make a few changes.

First, change this:

const { createDefaultDisconnectedServer } = require('@sitecore-jss/sitecore-jss-dev-tools');

to this:

const { createCustomDisconnectedServer } = require('./create-custom-disconnected-server');

Then, under const config = require('../package.json').config; add the following:

const { getRouteRenderings } = require('./layout-service/get-route-renderings');
const { addAtlSugMembers } = require('./layout-service/add-atlsug-members');

Add this to the proxyOptions object:

customizeRoute: (route, rawRoute, currentManifest, request, response) => {
  const routeRenderings = getRouteRenderings(route);
  addAtlSugMembers(routeRenderings, rawRoute.layout.renderings);
  return route;
}

Last, change createDefaultDisconnectedServer(proxyOptions); at the bottom to createCustomDisconnectedServer(proxyOptions);.

As described above, this swaps out createDefaultDisconnectedServer with a createCustomDisconnectedServer factory function that we will create below. createCustomDisconnectedServer passes a customizeRoute function to the mock Layout Service, inside of which we will get all renderings from the generated route and add ATLSUG members to the appropriate renderings.

When done, disconnected-mode-proxy.js should look like this: https://github.com/coreyasmith/jss-extensible-json-renderings/blob/master/src/Project/JssRocks/app/scripts/disconnected-mode-proxy.js.

create-custom-disconnected-server.js

Create a file called scripts\create-custom-disconnected-server.js like this:

var express = require("express");
var path = require("path");
var {
  ManifestManager,
  createDisconnectedLayoutService,
  createDisconnectedDictionaryService
} = require("@sitecore-jss/sitecore-jss-dev-tools");

const createCustomDisconnectedServer = options => {
  var app = options.server;
  if (!app) {
    app = express();
  }

  // the manifest manager maintains the state of the disconnected manifest data during the course of the dev run
  // it provides file watching services, and language switching capabilities
  var manifestManager = new ManifestManager({
    appName: options.appName,
    rootPath: options.appRoot,
    watchOnlySourceFiles: options.watchPaths,
    requireArg: options.requireArg
  });
  return manifestManager
    .getManifest(options.language)
    .then(function(manifest) {
      // creates a fake version of the Sitecore Layout Service that is powered by your disconnected manifest file
      var layoutService = createDisconnectedLayoutService({
        manifest: manifest,
        manifestLanguageChangeCallback: manifestManager.getManifest,
        customizeRoute: options.customizeRoute
      });

      // creates a fake version of the Sitecore Dictionary Service that is powered by your disconnected manifest file
      var dictionaryService = createDisconnectedDictionaryService({
        manifest: manifest,
        manifestLanguageChangeCallback: manifestManager.getManifest
      });

      // set up live reloading of the manifest when any manifest source file is changed
      manifestManager.setManifestUpdatedCallback(function(newManifest) {
        layoutService.updateManifest(newManifest);
        dictionaryService.updateManifest(newManifest);
        if (options.onManifestUpdated) {
          options.onManifestUpdated(newManifest);
        }
      });

      // attach our disconnected service mocking middleware to webpack dev server
      app.use(
        "/assets",
        express.static(path.join(options.appRoot, "../assets"))
      );
      app.use(
        "/data/media",
        express.static(path.join(options.appRoot, "../data/media"))
      );
      app.use("/sitecore/api/layout/render", layoutService.middleware);
      app.use(
        "/sitecore/api/jss/dictionary/:appName/:language",
        dictionaryService.middleware
      );

      if (options.afterMiddlewareRegistered) {
        options.afterMiddlewareRegistered(app);
      }

      if (options.port) {
        app.listen(options.port, function() {
          if (options.onListening) {
            options.onListening();
          } else {
            console.log(
              "JSS Disconnected-mode Proxy is listening on port " +
                options.port +
                "."
            );
          }
        });
      }
    })
    .catch(function(error) {
      if (options.onError) {
        options.onError(error);
      } else {
        console.error(error);
        process.exit(1);
      }
    });
};

exports.createCustomDisconnectedServer = createCustomDisconnectedServer;

This exports a createCustomDisconnectedServer function that is exactly the same as createDefaultDisconnectedServer except for one small difference: the options parameter passes customizeRoute to the mock Layout Service on line 30. The mock Layout Service accepts a customizeRoute function that takes the following parameters: route, rawRoute, currentManifest, request, and response. rawRoute is the raw data from your yml files, and route contains the transformed route data that the mock Layout Service is going to serve to your app. This method allows you to make changes to the route just before it's returned from the server.

Get Renderings from route

The rawRoute parameter passed to customizeRoute from the mock Layout Service contains a nice flat structure of all the renderings for the route. The route parameter, however, contains a nested tree of all the renderings for the route. This is a bit difficult to work with, so the first thing you should do in customizeRoute is grab all of the renderings from route and flatten them. Create a file called scripts\layout-service\get-route-renderings.js in your app like so:

const getRouteRenderings = route => {
  return getRenderingsFromPlaceholders(route.placeholders);
};

const getRenderingsFromPlaceholders = placeholders => {
  var allRenderings = Object.values(placeholders).reduce(
    (renderings, placeholder) => {
      const placeholderRenderings = getRenderingsFromPlaceholder(placeholder);
      return { ...placeholderRenderings, ...renderings };
    },
    {}
  );
  return allRenderings;
};

const getRenderingsFromPlaceholder = placeholder => {
  const allRenderings = placeholder.reduce((renderings, rendering) => {
    renderings[rendering.uid] = rendering;

    if (rendering.placeholders) {
      const nestedRenderings = getRenderingsFromPlaceholders(
        rendering.placeholders
      );
      return { ...nestedRenderings, ...renderings };
    }

    return renderings;
  }, {});
  return allRenderings;
};

exports.getRouteRenderings = getRouteRenderings;

This exports a method called getRouteRenderings that will turn this:

{
  "placeholders": {
    "jss-main": [
      {
        "uid": "1"
        "componentName": "ComponentWithPlaceholder"
        "placeholders": {
          "jss-nested": [
            {
              "uid": "2"
              "componentName": "ComponentWithoutPlaceholder"
            }
          ]
        }
      }
    ]
  }
}

into this:

{
  "1": {
    "uid": "1",
    "componentName": "ComponentWithPlaceholder",
    "placeholders": {
      "jss-nested": [
      {
        "uid": "2"
        "componentName": "ComponentWithoutPlaceholder"
      }
    ]
  },
  "2": {
    "uid": "2",
    "componentName": "ComponentWithoutPlaceholder"
  }
}

This is a lot easier to work with than the placeholders structure in route. If you want to change something in the rendering with uid 2, just do: renderings["2"].hello = "world". Or iterate over all of the renderings in the route with Object.values(renderings).forEach(). No more traversing a potentially deeply-nested tree. Nice.

Add ATLSUG Members to Renderings

Create a file called scripts\layout-service\add-atlsug-members.js like this:

const atlSugMembers = [
  'George "Sitecore George" Chang',
  'Martin "Sitecore Artist" English',
  'Anastasiya "JavaScript Ninja" Flynn',
  'Varun "Too Many Questions" Nehra',
  'Craig "From ATL" Taylor',
  'Amy "Sitecore Amy" Winburn'
];

const applicableRenderings = ["ContentBlock"];

const addAtlSugMembers = (routeRenderings, rawRouteRenderings) => {
  const atlSugMemberRenderings = applicableRenderings.map(r => r.toLowerCase());
  const addAtlSugRenderingIds = rawRouteRenderings
    .filter(r => atlSugMemberRenderings.includes(r.renderingName.toLowerCase()))
    .map(r => r.uid);

  addAtlSugRenderingIds.forEach(renderingId => {
    routeRenderings[renderingId].atlSugMembers = atlSugMembers;
  });
};

exports.addAtlSugMembers = addAtlSugMembers;

This exports a method that adds an atlSugMembers property to any rendering that is in the applicableRenderings list with ATLSUG members.

Admittedly, it's not necessary to look through rawRouteRenderings to do this particular transformation. I just wanted to demonstrate the rawRouteRenderings to routeRenderings mapping technique. The renderings in rawRouteRenderings will expose any property you define in your .yml files, so you could do something like this in your route:

jss-main:
- componentName: ContentBlock
  addAtlSugMembers: true
  fields:
    content: <p>Level 1 ContentBlock.</p>

Then look up renderings IDs in rawRouteRenderings that have the addAtlSugMembers property set to true and do your transformation based on the presence of that property rather than rendering name. In this scenario, renderings in routeRenderings won't have an addAtlSugMembers property because the Layout Service ignores it.

Display ATLSUG Members

Now you can start consuming ATLSUG members in your ContentBlock rendering like this:

const ContentBlock = ({ fields, rendering: { atlSugMembers } }) => (
  <React.Fragment>
    <Text tag="h2" className="display-4" field={fields.heading} />

    <RichText className="contentDescription" field={fields.content} />

    {atlSugMembers && (
      <ul>
        {atlSugMembers.map(member => (
          <li>{member}</li>
        ))}
      </ul>
    )}
  </React.Fragment>
);

atlSugMembers will be a property available on the rendering prop thanks to the customizations made to the disconnected server above.

Conclusion

Hopefully now you feel comfortable customizing route data from the mock Layout Service. This won't help you in connected mode, though, so check out my post on extending rendering data in connected mode.

Once you get all of this code in place, further rendering customizations are as easy as plugging a function into customizeRoute in scripts\disconnected-mode-proxy.js.

Big kudos to the JavaScript Services team (Kam Figy, Alex Shyba, Adam Weber, and Nick Wesselman) for making the disconnected Layout Service so flexible and for all of their help.

Sitecore JavaScript Services Team

Thanks to mon ami Jean-Fran├žois L'Heureux for reviewing the post.