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.
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:
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:
- Create the plug-in directory structure needed for your new widget, as described in Review widget file structure.
- 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.
- Add the widget to your application as described later in this section.
- Add your widget to your home page layout as described in Identify the layout.
- Create and generate the CSS files that your widget will use as described in Set the widget style sheet.
- Display localized strings, if necessary, which is described in Create locale information.
- 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.
- To obtain the necessary data for the widget to run, create fetchers, as described in Write a fetcher.
- To access an endpoint, write an action, as described in Write an action.
- Create an endpoint, as described in Create an endpoint.
- 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.
/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;
/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.
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.
/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
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
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.
- Import the contents of the
styles.css
file into a string constant namedcss
. - Wrap the widget's React element in a
Styled
component, with anid
property named after the widget (in this case,CurrencySelector
) and acss
property that contains the imported CSS. This inserts the CSS into an HTMLstyle
element before rendering the widget. - 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;
/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.
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.
/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]"
}
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
:
- Create a
/plugins/subscribers/my-subscriber
directory to store your subscriberindex
andmeta.js
files. - 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 } }); };
- 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' };
- Export your subscriber by creating a
/plugins/subscriber/index.js
file that contains the following:export const mySubscriber = () => import('./my-subscriber');
- 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';
- 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 ());
- 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 });