Build a widget

Widgets allow you to create graphic elements for your storefront. There are several steps to creating a widget, including planning, associating layouts, creating components and registering.

This section takes the concepts discussed earlier and walks you through the building of a widget. The following is an example of a widget that allows a shopper to see a currency symbol for a specific currency, or the CurrencySelector widget. The widget example shows you how to get data from a REST API call for your initial display and then get additional data from another API function that you can use to update your display.

This example application has a single currency selector widget that presents a dropdown list of all of the available currencies as returned by the list-currencies JavaScript endpoint.

Select a currency example

This example has an event listener that listens for selections from this list. When a selection occurs, the ID of the selected currency is passed to the getCurrency REST endpoint to get specific details. When that information is received, it is displayed in an auxiliary panel, showing you the currency symbols for the available currencies. For example:

Currency example - result

Steps to create a widget

Creating new widgets allows you to customize your website's interface, as well as provide additional functionality to your shoppers. Before you begin to develop new widgets, it is best to plan out your requirements.

Note: Before you create a widget, ensure that you have correctly configured your OSF workspace. Refer to Set Up a Development Environment for information on setting up and configuring your OSF workspace.

It is important that you have identified the task that your widget is going to perform. Additionally, identify if your widget needs data to be added to the Redux store, and if that data needs to be available during server-side rendering (SSR).

Ideally, a component should only perform one task. When creating a widget, try and make your components smaller, which will make your code reusable across different pages. It is also recommended that you create functional components, which consume less than a class-based component.

Once you have determined what and how your widget will work, perform the following tasks:

  1. Create the plug-in directory structure needed for your new widget, as described in Review widget file structure.
  2. Create the initial widget index and metadata JavaScript files, as described in . These files will be modified as you continue to add other plug-in components.
  3. Add the widget to your application as described later in this section.
  4. Add your widget to your home page layout as described in Identify the layout.
  5. Create and generate the CSS files that your widget will use as described in Set the widget style sheet.
  6. Display localized strings, if necessary, which is described in Create locale information.
  7. If you have determined that your data needs to be available during SSR, add a new selector to the widget to access the data, as described in Create a selector.
  8. To obtain the necessary data for the widget to run, create fetchers, as described in Write a fetcher.
  9. To access an endpoint, write an action, as described in Write an action.
  10. Create an endpoint, as described in Create an endpoint.
  11. If you need to deploy a global widget that has no user interface, create a subscriber as described in Create a subscriber.

Create a widget directory structure

Your widget needs to follow a specific file structure. Create the file structure for you widget similar to that described in Review widget file structure. When you create a new widget, you will have to create these directories. If you clone and then modify an existing widget, these directories may already exist.

For our example, create the widget in the /plugins/components/currency-selector/ directory. This directory will contain the widget you create for the currency selection.

Create widget files

As described in Understand widget composition, all widgets contain, at the very least, both an index.js file and a meta.js file. Note that these are different than the index.js and meta.js files that reside in the /plugins directory.

Using the currency selector example, when you write your widget, you must add it to the components directory in the /plugins/components/currency-selector directory that you just created. For example, the following /plugins/components/currency-selector/index.js file defines the widget:
import React from 'react';
const CurrencySelector = () => {
  const currencies = [
    {repositoryId: 'c1', displayName: 'Franc', currencyCode: 'FRA'},
    {repositoryId: 'c2', displayName: 'Pound', currencyCode: 'GBP'},
    {repositoryId: 'c3', displayName: 'Yen', currencyCode: 'JPY'}
  ];
  const selectedCurrency = currencies[0];

  return (
    <div>
      {currencies && (
        <div>
         <Dropdown
            id="CurrencySelector-list"
            name="CurrencySelector-list"
            label="Currencies:"
            aria-label="Currencies:"
            value={selectedCurrency && selectedCurrency.repositoryId}
            onChange={event => {
              console.log(`selected ${event.target.value}`);
            }}
            onBlur={() => {}}
          >
            <option value="">Select a currency...</option>
            {currencies.map(currency => (
              <option key={currency.repositoryId} value={currency.repositoryId}>
                {currency.displayName}
              </option>
            ))}
          </Dropdown>
         </div>
       )}
      {selectedCurrency && (
        <div>
          Selected currency: {selectedCurrency.displayName} ({selectedCurrency.currencyCode})
        </div>
       )}
    </div>
  );
};
export default CurrencySelector;
Widget code has associated metadata, so whenever you create a module that implements your widget, you must also create a parallel metadata object. The following is an example of an undefined /plugins/components/currency-selector/meta.js file:
/**
  * Metadata for the currencySelector component.
  */
 export default {};

Add your widget to your application

OSF provides a number of default widgets that you can use when creating your own widget. To add a default widget and make it available to the system, edit the index.js and the meta.js files in the component directory, for example, /plugins/component/index.js. This index.js file exports a number of functions whose names correspond to the name of your application's widgets. Each function returns a dynamic import that resolves to a module whose default export is the React component that initiates the widget.

By default, when the system generates an application, it exports of all of the default widgets, which are available to you should you choose. Your application may not require all of the default widgets.

For example, to create the currency-selector widget, you would add the following to the /plugins/components/index.js file:
 // Export a reference to our _CurrencySelector widget.
export const _CurrencySelector = () => import('./currency-selector');

Whenever you create a new widget, you must add a reference to it.

Add the following to the /plugins/components/meta.js file:
export {default as _CurrencySelector} from './currency-selector/meta';

Note: Preceding the names of your new components with an underscore is the recommended naming convention for custom widgets, as it will prevent naming collisions with default widgets. For example, _CurrencySelector.

Identify the layout

Before creating the widget, set the layout page that the widget will use by setting the my_app/assets/pages/home.json file. Set the layout page as follows:
{
   "title": Home Page",
   "address": "/home",
   "shortName": "home",
   "defaultPage": true,
   "layout": [
     {
       "type": "main",
       "width": 12,
       "components": [
         "_CurrencySelector"
       ]
     }
   ]
 }

Note that the _CurrencySelector has been added as a component.

You initially create your layouts locally, and then upload them to the Design page of the administration interface.

Set the widget style sheet

Our example, the CurrencySelector widget, uses a default CSS style sheet that creates a CSS class called CurrencySelector. This file, which is defined in the /plugins/components/currency-selector/styles.css file, contains standard CSS styling:
/**
 * CurrencySelector CSS definitions.
 */

 .CurrencySelector {
   padding: 10px;
 }
 .CurrencySelector__Label {
   padding-right: 10px;
 }
 .CurrencySelector__SelectedCurrencyInfo {
   padding-top: 10px;
 }

If you create or use any CSS that is specific to a user interface component, that CSS should be stored within the widget's styles.css file. Note that UI components should be plain CSS, which will enable you to apply styles on your UI components. It is also recommended that you use flex or grid CSS layouts.

Once you have created the CSS, you must update the Currency Selector widget to use the CSS:
  1. Import the contents of the styles.css file into a string constant named css.
  2. Wrap the widget's React element in a Styled component, with an id property named after the widget (in this case, CurrencySelector) and a css property that contains the imported CSS. This inserts the CSS into an HTML style element before rendering the widget.
  3. Use class names from the CSS by setting the React className property on individual React elements.

Note: Always import or export individual constants, classes or functions separately and not by using wildcards. The following example shows how to take the <div> of the className and put it inside the Styled component. It gives it a unique ID, and then pulls in the CSS file as a plain string named CSS. This lets you set it as a property on the Styled component. That causes the CSS from that style sheet to be added to the DOM and makes it available for you to use. Then set the class name of the <div> to the CurrencySelector class that was identified in the style sheet. This is a portion of the example /plugins/components/currency-selector/index.js file:

import React from 'react';
import Styled from '@oracle-cx-commerce/react-components/styled';
import css from './styles.css';
 
const CurrencySelector = () => {
  const currencies = [
    {repositoryId: 'c1', displayName: 'Franc', currencyCode: 'FRA'},
    {repositoryId: 'c2', displayName: 'Pound', currencyCode: 'GBP'},
    {repositoryId: 'c3', displayName: 'Yen', currencyCode: 'JPY'}
  ];
  const selectedCurrency = currencies[0];
 
  return (
    <Styled id="CurrencySelector" css={css}>
      <div className="CurrencySelector">
        {currencies && (
          <div>
            <span className="CurrencySelector__Label">Currencies:</span>
 
            <select
              value={selectedCurrency && selectedCurrency.repositoryId}
              onChange={event => {
                console.log(`selected ${event.target.value}`);
              }}
              onBlur={() => {}}
            >
              <option value="">Select a currency...</option>
 
              {currencies.map(currency => (
                <option key={currency.repositoryId} value={currency.repositoryId}>
                  {currency.displayName}
                </option>
              ))}
            </select>
          </div>
        )}
 
        {selectedCurrency && (
          <div className="CurrencySelector__SelectedCurrencyInfo">
            Selected currency: {selectedCurrency.displayName} ({selectedCurrency.currencyCode})
          </div>
        )}
      </div>
    </Styled>
  );
};
export default CurrencySelector;
Note that the /plugins/components/currency-selector/meta.js file should contain the following:
import * as de from './locales/de.json';
import * as en from './locales/en.json';
 
export default {
  resources: {
    de,
    en
  }
};

For additional information on CSS performance, refer to Remove unnecessary CSS.

Create locale information

You can add styling and localized strings to your widget. The widget obtains localized text from a resource bundle that identifies the language of the shopper's browser.

Your application can store the necessary code in the /locales directory under your widget’s component directory. For example, /plugins/components/currency-selector/locales. Note that all shared locale strings should be put into this common resource directory, however, component-specific locales should never be added to the common directory. CSS classes will also identify how to display your widget based upon the device used to view the site. For example, you may want to create mobile-specific CSS and desktop-specific CSS. These CSS classes can be differentiated using a @media query to identify different resolution and size images.

This directory should contain resource bundles for all of the supported locales. These resource bundles contain strings. For example, the English locale, or the en.json file, contains the following for the CurrencySelector widget:
{
  "labelCurrencies": "Currencies:",
  "labelSelectACurrency": "Select a currency...",
  "labelSelectedCurrency": "Selected Currency:"
}

Since resources are passed into widgets as normal props, it is best to use a naming convention that differentiates resource props from other props. It is recommended that you reuse labels, and avoid duplication.

And the German locale, or the /plugins/components/currency-selector/locales/de.json file, contains the following:
{
  "labelCurrencies": "[de]Currencies:[de]",
  "labelSelectACurrency": "[de]Select a currency...[de]",
  "labelSelectedCurrency": "[de]Selected Currency:[de]"
}
So that resources can be passed in as component props, replace your widget's meta.js file to be similar to the following:
import de from './locales/de.json';
import en from './locales/en.json';
 
/**
 * Metadata for the CurrencySelector component.
 */
export default {
  // Include references to all of our resource strings in all supported locales.
  // This will enable the component to access any resource string via its props,
  // using the locale that is currently in effect.
  resources: {
    de,
    en
  }
};

Replace the hard-coded strings in your widget with resource prop values:

import React from 'react';
import Styled from '@oracle-cx-commerce/react-components/styled';
import css from './styles.css';
 
const CurrencySelector = ({labelCurrencies, labelSelectACurrency, labelSelectedCurrency}) => {
  const currencies = [
    {repositoryId: 'c1', displayName: 'Franc', currencyCode: 'FRA'},
    {repositoryId: 'c2', displayName: 'Pound', currencyCode: 'GBP'},
    {repositoryId: 'c3', displayName: 'Yen', currencyCode: 'JPY'}
  ];
  const selectedCurrency = currencies[0];
   return (
    <Styled id="CurrencySelector" css={css}>
      <div className="CurrencySelector">
        {currencies && (
          <div>
            <span className="CurrencySelector__Label">{labelCurrencies}</span>
             <select
              value={selectedCurrency && selectedCurrency.repositoryId}
              onChange={event => {
                console.log(`selected ${event.target.value}`);
              }}
              onBlur={() => {}}
            >
              <option value="">{labelSelectACurrency}</option>
 
              {currencies.map(currency => (
                <option key={currency.repositoryId} value={currency.repositoryId}>
                  {currency.displayName}
                </option>
              ))}
            </select>
          </div>
        )}
        {selectedCurrency && (
          <div className="CurrencySelector__SelectedCurrencyInfo">
            {labelSelectedCurrency} {selectedCurrency.displayName} ({selectedCurrency.currencyCode})
          </div>
        )}
      </div>
    </Styled>
  );
};
export default CurrencySelector;

Note that you must redeploy your application for localization to take effect as the localization mechanism requires that Design Studio text snippets have been created to contain the translated strings.

When you create shared locale resource strings, they should go into a common resource directory. However, ensure that you do not add component-specific locales into a common resource directory.

Now that you have created your widget, you can configure it to communicates with REST API endpoints.

Create a subscriber

Subscriber plug-ins are passive observers that allow you to deploy a global widget that has no user interface.

Subscribers allow you to add connections to other independent endpoints. For example, if you want to add Google Analytics to your site, use a subscribers plug-in. Subscribers do not invoke store actions, and as such, they receive read-only versions of the Redux store’s API. You can, however, use subscribers to invoke endpoints independent of the Redux store. Note that subscribers are run only on the client-side.

Use a subscriber for any feature where you want to passively observe an event. If your feature needs to update the application state, then you should create an action. Creating a subscriber is similar to creating an action or an initializer. The following example shows how to create a subscriber named my-subscriber:

  1. Create a /plugins/subscribers/my-subscriber directory to store your subscriber index and meta.js files.
  2. Create the /plugins/subscribers/my-subscriber/index.js file. For example:
    export default ({endpoint, getState, subscribeDispatch}) => {
      // Get value form the selector
      // const someValueFromTheState = someSelector(getState());
     return subscribeDispatch(action, state) => {
      // const {type} = action;
      // Track getState action
     if (type === 'getState') {
      // Add connections to other independent endpoints
      // endpoint ('someEndpointName', somePayload, state);
     }
     else if (type === 'myOtherAction') {
     // Track myOtherAction action
     }
     });
    };
  3. Create the /plugins/subscribers/my-subscribers/meta.js file. For example:
    export default {
      packageId: '@oracle-cx-commerce/subscribers',
      description: 'Add your description',
      //endpoints: ['yourEndpointName' || ''],
      //triggers: 'someActionType',
      author: 'yourUserName'
    };
  4. Export your subscriber by creating a /plugins/subscriber/index.js file that contains the following:
    export const mySubscriber = () => import('./my-subscriber');
  5. Then export your subscriber's metadata by creating a /plugins/subscribers/meta.js file that contains the following:
    export (default as mySubscriber) from './my-subscriber/meta';
  6. You must add the subscribers to your application's client.js file. For example:
    import * as actions from './plugins/actions';
     import * as components from './plugins/components';
     import * as endpoints from './plugins/endpoints';
     import * as subscribers from './plugins/subscribers';
     import {createClient} from '@oracle-commerce/react-app/client';
     createClient({
       actions,
       components,
       endpoints,
       subscribers
    }). then(({start}) => start ());
  7. Finally, you must add the subscriber information to your application's meta.js file. For example:
    import * as actions from './plugins/actions/meta';
     import * as components from './plugins/components/meta';
     import * as endpoints from  './plugins/endpoints/meta';
     import * as initializers from './plugins/initializers/meta';
     import * as subscribers from './plugins/subscribers/meta';
     import {createMeta} from '@oracle-commerce/react-app/meta';
     export default createMeta({
       actions,
       components,
       endpoints,
       initializers,
       subscribers
    });