Working with Javascript

November 14, 2023

Using Javascript Loader (package.json, webpack etc.)

Commerce IO Javascript Loader is used to load Javascript dependencies. This enables Commerce IO to be flexible when accessing Javascript (JS) modules from various components, for example productList and menu, and to resolve correct versions of required JS modules across different Jahia modules. The module generates a SystemJS file with required bundles, maps and paths, which are used for resolution of JS modules (for more information, see Loading React components in JSP). You do not need to configure this module, 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 the following files in the commerce-io-front-end as examples of how to setup your module.

The Package.json file specifies required JS dependencies, which can be installed using a YARN command. The install 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. 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 when you deploy the module, you will see a list of resolved dependencies in your Jahia console.

The package.json file, in the src/main/javascript directory, which is required by the JavaScript loader. The file includes the module name for javascript loader and a list of JS dependencies. Note that the module name, in this case @jahia/commerce-io-front-end, will be used in JSP views to get modules (for more information, see Loading React components in JSP).

Gulp is used to create two tasks, one for each webpack file. These tasks are triggered through a Maven frontend plugin that you can find in pom.xml. For more information on Gulp configuration, see the gulpfile.js file.  

Creating Components with ReactJS

Commerce IO utilizes the 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, Commerce IO can pass the current context as well as any properties of the Jahia Component to the React component. This level of flexibility provides Commerce IO with access to any necessary information we may require.

Using React, Commerce IO can create dedicated reusable components, where the functionality is split between parent and children components.

For an example, see the Product List Component (https://demo.commerceio.jahia.com/home/summer/surf.html).

Components

  • Product List
    Displays a grid of available products. The component uses GraphQL to retrieve a list of products.
  • Pagination
    Adds navigation between available pages (lists) of products when 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.

With React, code is separated by functionality (and rendering) into dedicated components that are designed to handle specific functionality independently.

Using GraphQL with Apollo in React components

This example provides details about the productList component, which is a complex React component that uses GraphQL. You can find the component at src/main/javascript/app_productList.

To interact with GraphQL, Commerce IO uses the Apollo library which enables setting up a client and provides access to functions such as GraphQL to feed queries. The client setup is rather basic, with some caching features added. To learn more about Apollo, see the Apollo Client Introduction 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 standard and require little explanation. If you are not familiar with GraphQL, you can check out this GraphQL tutorial. These queries are used for the 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 convenience. The Commerce IO component relies on two GET queries and responds to data from both queries. PRODUCTINFO_QUERY acts as a subquery as it relies on results from PRODUCTLIST_QUERY. You can see this at line 305 where the component attempts to get visible product data 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 the Commerce IO fetch policy is set to “network-only” but you can configure it as you wish and utilize GraphQL caching for better performing queries.

The PRODUCTLIST_QUERY uses the updateQuery method to enable Commerce IO to fetch more results. You can find the fetch more mechanism at line 146.  Here, the fetchMore method of the query is used and the query is updated with new data, and a flag is set to mark that there is no more data.

Loading React components in JSP

This section also uses the productList component and the /src/main/resources/ciont_productList/html as examples. The component is a basic JSP and you can use tags as you normally would. Note that the jahia-npm-resource tag is picked up by a Javascript Loader filter, replaced with a SystemJS configuration for @jahia/commerce-io-front-end module, and passed as a name. Remember that the 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.

In  line 33, the 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
  • The mountpoint language details object, which contains information about specific store name, its URL, required catalog name and version.

At line 36, the productList component import is performed using SystemJS. Note that the contextualData object is created and passed down to the module. It will be available in every subcomponent via withContext() HOC. If you look at the productList component, you can see that it is placed in an element with id “productList”, which is also the last HTML element in the JSP.

The following code shows the product list JSP.

<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

Commerce IO uses react-router-dom to manage navigation and maintain route history. Commerce IO maintains the state of the app by using the history object, keeping track of the locations visited within the Commerce IO app and updating the rendering accordingly.

React router provides a Link Component that allows Commerce IO 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). This is done by wrapping the exported component in a function call(withRouter(component)) that performs the transformation and allows Commerce IO to receive other parameters (for example, history object) for use in its component.

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