Add widgets to Rich Text Editor (RTE)

Learn how to add widgets to the Rich Text Editor (RTE) in the AEM Content Fragment Editor.

Transcript
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.

AEM UI extended
Extension point
Content Fragment Editor
Rich Text Editor Widgets

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 with id, 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 the useEffect 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 using guestConnection object provides RTE instruction to execute. In this case insertContent 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;
recommendation-more-help
4859a77c-7971-4ac9-8f5c-4260823c6f69