Working with Javascript

  Written by The Jahia Team
 
Developers
   Estimated reading time:

Using JavascripLoader (package.json, webpack etc.)

Commerce IO Javascript Loader is used to load javascript dependencies. That allows us to be flexible when accessing javascript (JS) modules from various components (i. e. productList, menu etc.) and at the same time make sure that we resolve correct versions of required JS modules across different DX modules. In a nutshell the module will generate a SystemJS file with required bundles, maps and paths, which will be used for resolution of JS modules (see Loading React components in the “JSP section”). This module does not need to be configured in any way, simply add it to your site and it will take care of your js dependencies.

To use javascript loader your module needs to be correctly setup. You can look at commerce-io-front-end for an example setup. Let’s look into it in detail.

Package.json file is used to specify needed JS dependencies, which can be installed via yarn command, which naturally simply creates node and node_modules folders at the root level with required files. Note that this step is performed automatically upon module deployment (more on that later).

There are two webpack config files at the root level. Webpack-config.js allows you to specify what your components are by simply providing a path to a React component. You can add plugins as you wish. As you can see js modules will be generated under /src/main/resources/javascript/bundles. Webpack-vendors.config.js lists all dependencies and is used by the javascript loader; every dependency that you add using yarn or npm must be reflected here. Note that upon deployment of the module you will see a list of resolved dependencies in your DX console.

There is also a package.json file in the src/main/javascript, which is require by the JavaScript loader, it includes module name for javascript loader and a list of JS dependencies. Note that that name, in this case @jahia/commerce-io-front-end, will be used in jsp views to get modules (more on that later).

Gulp is used to create two tasks, one for each webpack file, these tasks are triggered via maven frontend plugin as can be observed in pom.xml. For gulp configuration see gulpfile.js file.  

Creating Components with ReactJS

CommerceIO utilizes ReactJS framework to create powerful components. React components are bootstrapped in Jahia views using SystemJS to load dependencies. By bootstrapping the components in the jsp views we are able to pass the current context as well as any properties of the Jahia Component to our React component. This level of flexibility allows us to have access to any necessary information we may require.

By using React we are able to create dedicated reusable components, where the functionality is split between parent and children components.

A great example is the Product List Component.

https://demo.commerceio.jahia.com/home/summer/surf.html

Components

  • Product List
    • is used to display a grid of available products. It uses GraphQL to retrieve a list of products.
  • Pagination
    • is then used to add navigation between available pages(lists) of products if there are more products than can be displayed on a single page.
  • Product
    • dedicated component that takes care of the rendering of the actual product.

By using React it is very easy to separate our code by functionality (and rendering) into dedicated components that are designed to handle specific functionality independently.

Using GraphQL with Apollo in React components

It makes sense to look at the most complex React component with GraphQL to cover as many caveats and technical issues as possible with a solid example on hand, so we look at productList component, which can be found in src/main/javascript/app_productList.

To interact with GraphQL we use Apollo library which allows us to set up a client and also use handy functions such as graphql to feed our queries. The client setup is rather basic, with some caching features added. To learn more about Apollo visit this page.

const client = (props => {
  const link = new HttpLink({
      uri: props.contextualData.servletContext + '/modules/graphql'
  });

  let cache = new InMemoryCache({
      dataIdFromObject: product => product.sku
  });

  return new ApolloClient({
      link: link,
      cache: cache
  });
});

The queries themselves are pretty straight forward and need little explanation. If you didn’t work with GraphQL before check out this link for a quick tutorial. These are the ones used for product list component.

const PRODUCTLIST_QUERY = gql`query ProductList($connection: String!, $index: String!, $offset: Int!, $limit:Int!, $path: String!, $config: InputFacetConfig){
  cioProducts(connection:$connection, index:$index,
      category: $path, offset:$offset, limit:$limit, config:$config) {
      sku
      name
      mountedPath
      summary
      vanityUrl
      price{
          formattedValue
      }
      images {
          format
          imageType
          url
      }}
}`;

const PRODUCTINFO_QUERY = gql`query ProductInfo($siteKey: String!, $language: String!, $productCodes: [String]!){
  cioProductsInfo(siteKey: $siteKey, language: $language, productCodes: $productCodes) {
      sku
      inStock
      priceInfo {
          currencyIso
          price
          formattedPrice
      }
  }
}`;

Note that these queries are composed for comvenience. Our component therefore relies on two GET queries and responds to data from both. PRODUCTINFO_QUERY is what you could call a subquery as it relies on result from PRODUCTLIST_QUERY. This reliance is apparent at line 305 where we attempt to get visible product from the first query.

const ComposedComponent = compose(
  graphql(PRODUCTLIST_QUERY, {
      options(props) {
          return {
              variables : {
                  connection: props.contextualData.esConnectionName,
                  index : props.contextualData.esIndexName + "_alias_" + props.contextualData.locale,
                  path : props.category,
                  offset : 0,
                  limit : QUERY_SIZE,
                  //Remove id property from facets before using it in query
                  config : { search: props.search, facets:_.omit(JSON.parse(JSON.stringify(props.facets)), 'id', 'searchOnValue') }
              },
              fetchPolicy: 'network-only'
          }
      },
      name: "fetchProducts"
  }),
  graphql(PRODUCTINFO_QUERY, {
      options(props) {
          return {
              variables : {
                  siteKey: props.contextualData.siteKey,
                  language: props.contextualData.locale,
                  productCodes: props.fetchProducts.cioProducts === undefined || props.fetchProducts.loading ? [] : getVisibleProducts(props)
              },
              fetchPolicy: 'network-only'
          }
      },
      name: "fetchProductsInfo"
  })
)(ProductList);

Note that our fetch policy is set to “network-only” but you can configure it as you wish and utilize GraphQL caching for better performing queries.

Another interesting aspect is how PRODUCTLIST_QUERY uses “updateQuery” method to allow us to fetch more results. You can find fetch more mechanism at line 146.  Here “fetchMore” method of the query is used and the query is updated with new data, we also set a flag to mark that there is no more data.

Loading React components in jsp

I will continue using productList component as example and so in this section I look at /src/main/resources/ciont_productList/html. One thing to note is that it is a basic jsp and you can use tags as you normally would, the not so usual part is the jahia-npm-resource tag which get picked up by a filter in javascript loader and replaces the tag with a SystemJS configuration for @jahia/commerce-io-front-end module which is passed as a name. Remember that that name is defined in one of the package.json files.

CSS resources are loaded as you would normally load them in your jsp files as well.

At line 33 moutpointInfo taglib is used to get details about current mountpoint. The following information is returned:

  • Mountpoint id
  • Mountpoint name
  • Elasticsearch connection name
  • Elasticsearch index
  • SAP Hybris password, username and URL
  • Mountpoint path
  • As well as mountpoint language details object, which contains information about specific store name, its URL, required catalog name and version.

At line 36 productList component import is done via SystemJS. Note that contextualData object is created and passed down to the module. In the end it will be available in every sub component via withContext() HOC. If you look at the productList component you will discover that it is placed in an element with id “productList”, which is also the last HTML element in the jsp.

Here’s what the product list jsp actually looks like.

<jahia-npm-resource name="@jahia/commerce-io-front-end"></jahia-npm-resource>
<template:addResources type="css" resources="hfn.css"/>
<template:addResources type="javascript" resources="i18n/commerce-io-front-end-i18n_${renderContext.UILocale.language}.js" var="i18nJSFile"/>
<c:if test="${empty i18nJSFile}">
  <template:addResources type="javascript" resources="i18n/commerce-io-front-end-i18n.js"/>
</c:if>
<c:set var="mountPointInfo" value="${hybriStore:mountPointInfo(renderContext)}" />
<script>
  Promise.all([System.import('@jahia/commerce-io-front-end/app_productList/main')]).then(function (m) {
      var contextualData = {
          servletContext: "${url.context}",
          siteName: "${renderContext.site.name}",
          siteKey: "${renderContext.site.name}",
          mountPointName: "${mountPointInfo["mountPointName"]}",
          esConnectionName: "${mountPointInfo["connectionName"]}",
          hybrisURL: "${mountPointInfo["uri"]}",
          esIndexName: "${mountPointInfo["indexName"]}",
          searchPath: "${fn:substringAfter(currentNode.properties.category.node.path,   mountPointInfo["mountPointName"])}",
          elementId: "${currentNode.identifier}",
          itemsPerRow: parseInt("${currentNode.properties["itemsPerRow"].long}"),
          pageSize: parseInt("${currentNode.properties["pageSize"].long}"),
          searchValue: "${currentNode.properties["searchValue"].string}",
          editMode: "${renderContext.editMode}",
          locale: "${renderContext.site.language}"
      };
      m[0].default(contextualData);
  })
</script>
<div id="productList" data-module-info=""></div>

Routing

CommerceIO makes use of react-router-dom in order to manage navigation and maintain route history. We can maintain the state of the app by using the history object, keeping track of the locations visited within our app and update the rendering accordingly.

React router provides us with a Link Component that allows us to remain within the app without refreshing the browser window when navigating the catalog.

Components that want to manipulate or use the routing history object, should be HOC(High Order Components) we do this by wrapping the exported component in a function call(withRouter(component)) that performs this transformation allowing us to receive other parameters(i.e: history object) that we can use in our component.

Routing allows our components to maintain our application’s state while navigating through the app’s context.