Add widgets to Rich Text Editor (RTE)
Learn how to add widgets to the Rich Text Editor (RTE) in the AEM Content Fragment Editor.
In this video, we will learn how to integrate a custom UI in the Rich Text Editor using Widgets functionality. This powerful feature allows you to integrate custom UI created using the JavaScript framework of your choice. Widgets can be seen as special models that are opened by pressing the open curly bracket key within the RT Editor. They enable the insertion of dynamic content that relies on external systems or may change depending on the current context. To illustrate this, let’s begin with a demonstration of the RT Widgets functionality. I have already set up the local version of the UI extension application in my AEM cloud service environment. As you can observe on the screen, when I press the open curly bracket key within the RT, a context menu is displayed. By selecting the Discount Code List option from this menu, the custom UI is opened. Within the modal window, I can easily find, select and insert the current adventure specific discount code. This capability allows for seamless integration of a custom UI regardless of its complexity to insert external content that can dynamically adapt based on the current context. By utilizing the RT widgets, we have made the process of integrating custom UI components into RT easy and powerful. Now, let’s dwell into the technical details and explore the implementation code. To get started, I created a project named AEM UI Extension CF Editor using the Adobe Developer Console and the App Builder template. On my local machine, I used the AIO CLI to initialize the project. During initialization process, I selected the AEM Content Fragment Editor Extensibility template. You can easily verify this by checking the project’s package.json file. Now, let’s discuss the implementation details and code. In the Extension Registration component, we make use of the getWidgets method from the RT extension point to add the widget. The URL attribute value represents a relative URL path for loading the custom UI. The custom UI for the discount code hash route is developed using the Adobe React Spectrum framework. To learn more about React Spectrum, visit react-spectrum.adobe.com. Moving on, we need to add the discount codes route in the main React component. In app.js, you can find the hash route definition associated with the discount codes component. Let’s now review the discount codes component. Within the adventure discount codes variable, we have hardcoded the adventure name and its corresponding discount code. Ideally, these values would be retrieved from an external application such as order management system, product information management system, or an app builder action. The guest connection is initialized using the useEffect hook and is managed as component state. It facilitates communication with the AEM host and specifically enables the insertion of the discount code into the RT at the current caret position. Using the useState hook, we manage the component state as well.
The custom UI is rendered using React Spectrum’s combo box and button components. These components also have associated handler functions to perform actions. Let’s zoom in on the add discount code handler function. By utilizing the guest connection object, we provide the RT instructions for execution. Within the instructions array, we use the insert content type and provide an HTML code snippet to insert the selected adventure’s discount code. With these implementation steps, we have successfully integrated the custom UI into the RT using Widget’s functionality. Thank you.
To add the dynamic content in the Rich Text Editor (RTE), the widgets functionality can be used. The widgets help to integrate the simple or complex UI in the RTE and the UI can be created using the JS framework of your choice. They can be thought as dialogs that are opened by pressing {
special key in the RTE.
Typically, the widgets are used to insert the dynamic content that has an external system dependency or could change based on the current context.
The widgets are added to the RTE in the Content Fragment Editor using the rte
extension point. Using rte
extension point’s getWidgets()
method one or many widgets are added. They are triggered by pressing the {
special key to open the context menu option, and then select the desired widget to load the custom dialog UI.
This example shows how to add a widget called Discount Code List to find, select, and add the WKND adventure-specific discount code within an RTE content. These discount codes can be managed in external system like Order Management System (OMS), Product Information Management (PIM), home grown application or an Adobe AppBuilder action.
To keep things simple, this example uses the Adobe React Spectrum framework to develop the widget or dialog UI and hard-coded WKND adventure name, discount code data.
Extension point
This example extends to extension point rte
to add a widget to the RTE in the Content Fragment Editor.
Example extension
The following example creates a Discount Code List widget. By pressing the {
special key within the RTE, the context menu is opened, then by selecting the Discount Code List option from the context menu the dialog UI is opened.
The WKND content authors can find, select, and add current Adventure-specific discount code, if available.
Extension registration
ExtensionRegistration.js
, mapped to the index.html route, is the entry point for the AEM extension and defines:
- The widget definition in
getWidgets()
function withid, label and url
attributes. - The
url
attribute value, a relative URL path (/index.html#/discountCodes
) to load the dialog UI.
src/aem-cf-editor-1/web-src/src/components/ExtensionRegistration.js
import React from "react";
import { Text } from "@adobe/react-spectrum";
import { register } from "@adobe/uix-guest";
import { extensionId } from "./Constants";
// This function is called when the extension is registered with the host and runs in an iframe in the Content Fragment Editor browser window.
function ExtensionRegistration() {
const init = async () => {
const guestConnection = await register({
id: extensionId,
methods: {
rte: {
// RTE Widgets
getWidgets: () => [
{
id: "discountcode-list-widget", // Provide a unique ID for the widget
label: "Discount Code List", // Provide a label for the widget
url: "/index.html#/discountCodes", // Provide the "relative" URL to the widget content. It will be resolved as `/index.html#/discountCodes`
},
],
}, // Add a comma here
},
});
};
init().catch(console.error);
return <Text>IFrame for integration with Host (AEM)...</Text>;
}
export default ExtensionRegistration;
Add discountCodes
route in App.js
In the main React component App.js
, add the discountCodes
route to render the UI for the above relative URL path.
src/aem-cf-editor-1/web-src/src/components/App.js
...
<Routes>
<Route index element={<ExtensionRegistration />} />
<Route
exact path="index.html"
element={<ExtensionRegistration />}
/>
{/* Content Fragment RTE routes that support the Discount Codes Widget functionality*/}
<Route path="/discountCodes" element={<DiscountCodes />} />
</Routes>
...
Create DiscountCodes
React component create-widget-react-component
The widget or dialog UI is created using the Adobe React Spectrum framework. The DiscountCodes
component code is as below, here are key highlights:
- The UI is rendered using React Spectrum components, like ComboBox, ButtonGroup, Button
- The
adventureDiscountCodes
array has hardcoded mapping of adventure name and discount code. In real scenario, this data can be retrieved from Adobe AppBuilder action or external systems like PIM, OMS or home grown or cloud provider-based API gateway. - The
guestConnection
is initialized using theuseEffect
React hook and managed as component state. It is used to communicate with the AEM host. - The
handleDiscountCodeChange
function gets the discount code for the selected adventure name and updates the state variable. - The
addDiscountCode
function usingguestConnection
object provides RTE instruction to execute. In this caseinsertContent
instruction and HTML code snippet of actual discount code to be inserted in the RTE.
src/aem-cf-editor-1/web-src/src/components/DiscountCodes.js
import {
Button,
ButtonGroup,
ComboBox,
Content,
Divider,
Flex, Form,
Item,
Provider,
Text,
defaultTheme
} from '@adobe/react-spectrum';
import { attach } from '@adobe/uix-guest';
import React, { useEffect, useState } from 'react';
import { extensionId } from './Constants';
const DiscountCodes = () => {
// The Adventure Discount Code list
// In this example its hard coded, however you can call an Adobe AppBuilder Action or even make an AJAX call to load it from 3rd party system
const adventureDiscountCodes = [
{ id: 1, adventureName: 'BALI SURF CAMP', discountCode: 'BALI2023' },
{ id: 2, adventureName: 'BEERVANA IN PORTLAND', discountCode: 'PORTFEST' },
{ id: 3, adventureName: 'NAPA WINE TASTING', discountCode: 'WINEINSPRING' },
{ id: 4, adventureName: 'RIVERSIDE CAMPING', discountCode: 'SUMMERSCAPE' },
{ id: 5, adventureName: 'TAHOE SKIING', discountCode: 'EPICPASS' },
];
// Set up state used by the React component
const [guestConnection, setGuestConnection] = useState();
// State hooks to manage the component state
const [discountCode, setDiscountCode] = useState(null);
// Asynchronously attach the extension to AEM, we must wait or the guestConnection to be set before doing anything in the modal
useEffect(() => {
(async () => {
const myGuestConnection = await attach({ id: extensionId });
setGuestConnection(myGuestConnection);
})();
}, []);
// Handle the `discountCodeList` Dropdown change
const handleDiscountCodeChange = (key) => {
if (key) {
//console.log(`DiscountCode Key: ${key}`);
//console.log(`DiscountCode Value: ${adventureDiscountCodes[key-1].discountCode}`);
//Get discount code value using selected key/index
let discountCodeValue = adventureDiscountCodes[key - 1].discountCode;
//update the `discountCode` state
setDiscountCode(discountCodeValue);
}
};
// Add the selected Adventure's Discount Code into the RTE
const addDiscountCode = () => {
if (discountCode) {
// Use `guestConnection.host.rte.applyInstructions` method and provide RTE instruction to execute.
// The instructions are passed as an array of object, that has `type` and `value` keys
guestConnection.host.rte.applyInstructions([{ type: "insertContent", value: `<strong>Discount Code: ${discountCode}</strong>` }]);
}
};
// Adobe React Spectrum (HTML code) that renders the Discount Code dropdown list, see https://react-spectrum.adobe.com/react-spectrum/index.html
return (
<Provider theme={defaultTheme}>
<Content width="100%">
<Flex width="100%">
<Form width="50%">
<Text>Selected Discount Code: <strong>{discountCode}</strong></Text>
<p />
<Divider size="M" />
<ComboBox
name="discountCodeList"
label="Type or Select an Adventure name"
defaultItems={adventureDiscountCodes}
onSelectionChange={handleDiscountCodeChange}>
{item => <Item>{item.adventureName}</Item>}
</ComboBox>
<p />
<ButtonGroup align="right">
<Button variant="accent" onPress={addDiscountCode} autoFocus>Add</Button>
<Button variant="secondary" onPress={() => setDiscountCode(null)}>Clear</Button>
</ButtonGroup>
</Form>
</Flex>
</Content>
</Provider>
);
}
export default DiscountCodes;