AEM Edge Delivery - HTML Container in Universal Editor for Arbitrary HTML


AEM implementations (Cloud or AMS) generally have a HTML Container Component to add Arbitrary Html by authors. Although not a best practice in EDS (and not SEO friendly), if you are desperate, you can try the following solution. It converts the html into Base64 Format while saving and decodes it back to html while rendering...

Demo | Github


HTML Container Block


Expanded


Rendering


Solution


1) For detailed instructions on setting up Universal Editor Extension and Publishing to your Org check this post, steps below load it from a locally running app so https://localhost:9080 

2) Open terminal, login to aio and create an app. While going through the prompts make sure you select the Universal Editor Extension Template and Add a Custom Renderer... 

                              > aio login

                              > aio app init eaem-ue-html-container

3) Add the following code in eaem-ue-html-container\src\universal-editor-ui-1\web-src\src\components\ExtensionRegistration.js for a custom field/renderer implementation

import { Text } from "@adobe/react-spectrum";
import { register } from "@adobe/uix-guest";
import { extensionId } from "./Constants";
import metadata from '../../../../app-metadata.json';

function ExtensionRegistration() {
  const init = async () => {
    const guestConnection = await register({
      id: extensionId,
      metadata,
      methods: {
        canvas: {
          getRenderers() {
            return [
              {
                extension: extensionId,
                dataType: 'eaem:html-container',
                url: '/index.html#/eaem-html-container'
              }
            ];
          },
        },
      },
    });
  };
  init().catch(console.error);

  return <Text>IFrame for integration with Host (AEM)...</Text>
}

export default ExtensionRegistration;


4) Add the following code in src\universal-editor-ui-1\web-src\src\components\HtmlContainerField.js which has the main extension code for showing a TextArea to add HTML, convert to base64 while saving...

import React, { useState, useEffect } from 'react'
import { attach } from "@adobe/uix-guest"
import {
  Provider,
  defaultTheme,
  View,
  Flex,
  TextArea,
  Text,
  Button
} from '@adobe/react-spectrum'

import { extensionId } from "./Constants"

/**
 * Encodes a UTF-8 string to base64.
 */
const encodeToBase64 = (value) => {
  if (!value) return '';
  return window.btoa(unescape(encodeURIComponent(value)));
}

const decodeFromBase64 = (value) => {
  if (!value) return '';
  try {
    return decodeURIComponent(escape(window.atob(value)));
  } catch (e) {
    return value;
  }
}

export default function HtmlContainerField () {
  const [guestConnection, setGuestConnection] = useState()
  const [htmlValue, setHtmlValue] = useState('');
  const [label, setLabel] = useState('HTML Code');
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    (async () => {
      const connection = await attach({ id: extensionId })
      setGuestConnection(connection);

      const model = await connection.host.field.getModel();

      if (model.label) {
        setLabel(model.label);
      }

      const storedValue = await connection.host.field.getValue() || '';

      setHtmlValue(decodeFromBase64(storedValue));
      setLoading(false);

      document.body.style.height = '650px';
    })()
  }, [])

  const handleChange = (newValue) => {
    setHtmlValue(newValue);
    guestConnection?.host.field.onChange(encodeToBase64(newValue));
  }

  const openExpandedEditor = () => {
    const width = Math.min(1400, window.screen.availWidth - 40);
    const height = Math.min(1000, window.screen.availHeight - 40);
    const left = (window.screen.availWidth - width) / 2;
    const top = (window.screen.availHeight - height) / 2;

    window.eaemHtmlContainerBridge = {
      label,
      value: htmlValue,
      onChange: handleChange,
    };

    const popup = window.open(
      `${window.location.origin}${window.location.pathname}#/eaem-html-container-editor`,
      'eaemHtmlContainerEditor',
      `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`
    );

    if (!popup) {
      // eslint-disable-next-line no-alert
      alert('Please allow pop-ups for this site to expand the editor.');
    }
  }

  return (
    <Provider theme={defaultTheme} colorScheme='dark' height='100vh'>
      <View padding='size-200' UNSAFE_style={{ overflow: 'hidden' }}>
        <Flex direction="column" gap="size-100">
          <TextArea
            label={label}
            value={htmlValue}
            onChange={handleChange}
            isDisabled={loading}
            width="100%"
            height="500px"
            placeholder="Enter HTML code..."
          />
          <Flex justifyContent="end">
            <Button variant="secondary" isDisabled={loading} onPress={openExpandedEditor}>Expand</Button>
          </Flex>
        </Flex>
        <Text UNSAFE_style={{ display: 'block', marginTop: '8px', fontSize: '11px', opacity: 0.7 }}>
          Content is stored as base64-encoded HTML.
        </Text>
      </View>
    </Provider>
  )
}


5) Add the following code in src\universal-editor-ui-1\web-src\src\components\HtmlContainerModalEditor.js for expanded mode

import React, { useState } from 'react'
import {
  Provider,
  defaultTheme,
  Flex,
  View,
  TextArea,
  Heading,
  Button,
  Text
} from '@adobe/react-spectrum'

export default function HtmlContainerModalEditor() {
  const bridge = window.opener && window.opener.eaemHtmlContainerBridge;
  const [value, setValue] = useState(bridge ? bridge.value : '');

  if (!bridge) {
    return (
      <Provider theme={defaultTheme} colorScheme="dark" height="100vh">
        <View padding="size-300">
          <Text>Unable to connect to the field editor. Please close this window and try again.</Text>
        </View>
      </Provider>
    )
  }

  const handleChange = (newValue) => {
    setValue(newValue);
    bridge.onChange(newValue);
  }

  return (
    <Provider theme={defaultTheme} colorScheme="dark" height="100vh">
      <Flex direction="column" height="100vh">
        <View paddingX="size-300" paddingTop="size-200" paddingBottom="size-100">
          <Flex justifyContent="space-between" alignItems="center">
            <Heading level={3} margin="size-0">{bridge.label}</Heading>
            <Button variant="cta" onPress={() => window.close()}>Done</Button>
          </Flex>
        </View>
        <View flexGrow={1} paddingX="size-300" paddingBottom="size-300">
          <TextArea
            aria-label={bridge.label}
            value={value}
            onChange={handleChange}
            width="100%"
            height="100%"
          />
        </View>
      </Flex>
    </Provider>
  )
}


6) A sample Block Model eg. blocks\eaem-html-container\_eaem-html-container.json

{
  "definitions": [
    {
      "title": "EAEM HTML Container",
      "id": "eaem-html-container",
      "plugins": {
        "xwalk": {
          "page": {
            "resourceType": "core/franklin/components/block/v1/block",
            "template": {
              "name": "EAEM HTML Container",
              "model": "eaem-html-container"
            }
          }
        }
      }
    }
  ],
  "models": [
    {
      "id": "eaem-html-container",
      "fields": [
        {
          "component": "text",
          "name": "containerName",
          "label": "Container Name",
          "value": "",
          "valueType": "string"
        },
        {
          "component": "eaem:html-container",
          "name": "containerHtml",
          "label": "Container HTML",
          "value": "",
          "valueType": "string"
        }
      ]
    }
  ],
  "filters": []
}


7) Block rendering eg. blocks\eaem-html-container\eaem-html-container.js can have the following code to convert base64 encoded html back to rendered html...

function decodeBase64(value) {
  if (!value) return '';
  try {
    return decodeURIComponent(escape(window.atob(value)));
  } catch (e) {
    return value;
  }
}

export default function decorate(block) {
  const [nameRow, htmlRow] = [...block.children];

  const containerName = nameRow?.textContent?.trim() || '';
  if (containerName) {
    block.dataset.containerName = containerName;
  }

  const encodedHtml = htmlRow?.textContent?.trim() || '';
  const decodedHtml = decodeBase64(encodedHtml);

  block.innerHTML = decodedHtml;
}



No comments:

Post a Comment