Develop a Document Field Integration

Introduction

Goal

Develop a document field integration using the UI Extension client library.

Background

A document field integration functions as a field in a document type. It appears in a document editing template just as any other field and can be used to set the field's value and/or that of any other field(s) in the document. Content type developers provide the field caption and hint. The field is configured as a custom integration and is shown in an iframe and provides the UI to set the value(s).

The content editor and document field communicate using the UI Extension client library.

API

Functions

ui.document.get()
Returns a promise that resolves with information about the current document. The resolved object has the following properties:

PropertyDescriptionExample value
idThe UUID of the document handle node.
displayNameThe display name of the document."My Document"
localeThe locale of the document."en"
modeThe mode of the editor that shows the field. Either "view", "edit" or "compare"."view"
urlNameThe URL name of the document."my-document"
variant.idThe UUID of the document variant node."9a3f1f5c-5302-43c4-9bec-584e810ffa2f"

ui.document.field.getValue()
Returns a promise that resolves with the current string value of a field. It's up to the field integration to interpret the string, e.g. parse it as JSON.

Without any parameters, the value of the field itself is returned.

Optionally, a 'path' string array parameter can be passed, pointing to a field or a nested subfield within the current document. For multiple-valued fields, use zero-based indices to access specific values.

Examples:

  • ui.document.field.getValue();
  • ui.document.field.getValue('brxsaas:title');
  • ui.document.field.getValue('brxsaas:mymultiple', 1);
  • ui.document.field.getValue('brxsaas:mycompound', 'brxsaas:name');
  • ui.document.field.getValue('brxsaas:mymultiplecompound', 1, 'brxsaas:name');

👍

When accessing other field values in the same compound field, you may use "." to specify the name of the current compound field where both the other field and your document field integration field belong. You can also skip the compound field index for convenience like in the following examples:

Equivalent to ui.document.field.getValue('brxsaas:mycompound', 'brxsaas:name'); if the current compound field name is brxsaas:mycompound:
ui.document.field.getValue('.', 'brxsaas:name');

Equivalent to ui.document.field.getValue('brxsaas:mymultiplecompound', 1, 'brxsaas:name'); if current compound field name is brxsaas:mymultiplecompound and it is located at the second. The zero-based index is automatically detected for convenience.
ui.document.field.getValue('.', 'brxsaas:name');

Note: Whether the compound field is multiple or not, the API call is the same because the internal backend API resolves the details of the current compound field automatically. This helps avoid the complexity in implementation, especially when the document field integration module cannot easily determine the compound field's name and index where it belongs.

ui.document.field.setValue()
Updates the string value of the field. Returns a promise that resolves when the value has been set. The string can contain up to 100K characters to avoid storing unreasonable amounts of data (e.g. when an integration gets compromised). Larger strings will be ignored and return a rejected promise.

Note that validation of the set value is not reflected in the returned promise. A resolved promise means the value reached the surrounding editor. Any client-side validation (e.g. the 'required' check) may still trigger the field to become invalid.

ui.document.field.getComparedValue()
Only available when the "mode" of the document is "compare". Returns a promise that resolves with the string value of a field value to compare.

Without any parameters, the value of the field itself is returned.

Optionally, a 'path' string array parameter can be passed, pointing to a field or a nested subfield within the current document. For multiple-valued fields, use zero-based indices to access specific values.

👍

In the same way as ui.document.field.getValue() APIs are shown above, you may use "." to specify the name of the current compound field where both the other field and your document field integration field belong. You can also skip the compound field index for convenience, too.

ui.document.field.setHeight()
Sets the height of the iframe that surrounds the document field integration to the given number of pixels. Valid values are from 10 to 2000. When calling the function with the value 'auto', the iframe is set to the height of the iframe. This gives the option to show the iframe without scrollbars. When the size of the iframe content changes, the height of the iframe is automatically adjusted. When calling the function with the value 'initial', the iframe is set to the initial height from the integration configuration.

ui.document.open()
Opens a document by UUID in the content perspective.

Returns a promise that resolves when the document is opened.

PropertyDescriptionExample value
idThe UUID of the document handle node."c580ac64-3874-4717-a6d9-e5ad72080abe"

ui.document.navigate()
Navigates a document path in the content perspective.

Returns a promise that resolves when the document is opened.

PropertyDescriptionExample value
pathThe absolute JCR path of the document handle node."/content/documents/brxsaas/content/sample-document"

Functions (Experience Manager Only)

📘

The following functions are available as of ui-extension-saas version 0.3.1 and can only be used within the context of the editor in the Experience manager channel preview. In the editor in the Content app, these functions will log an error message in the Javascript console.

👍

Try out the ChatGPT Text Generator integration available from the Content Marketplace for an excellent use case of the functions below!

ui.document.setFieldValue(path, value)
Updates the specified field's value.

Parameters:

  • path is a string referring to a document field in the following format: {namespace}:{propertyname} If the field is a field group you can set a nested field using a path like the following: {namespace}:{propertyname}.{namespace}:{childpropertyname}.
  • value is a string to set the specified field's value to. To set boolean, integer, or float you must convert to string first.

Examples:

  • ui.document.setFieldValue('brxsaas:title, 'false')
  • ui.document.setFieldValue('brxsaas:group.brxsaas:item', '111')

ui.document.getFieldValue(path)
Gets the specified field's value.

Returns a promise that resolves with the current string value of a field. It's up to the field integration to interpret the string, e.g. parse it as JSON.

  • path is a string referring to a document field in the following format: {namespace}:{propertyname}. If the field is a field group you can set a nested field using a path like the following: {namespace}:{propertyname}.{namespace}:{childpropertyname}.

Examples:

  • ui.document.getFieldValue('brxsaas:title')
  • ui.document.getFieldValue('brxsaas:group.brxsaas:item')

ui.document.getValues()
Gets object with all field values of the current document.

Returns a promise that resolves with a JSON string containing key/value pairs representing the all document fields and their current values.

Example:

  • ui.document.getValues()

"Hello World" Code Example

👍

For simplicity, this is a plain HTML/JavaScript example. Document field integrations can be developed using your JS framework and tools of choice, such as Angular, Vue, React, etc.

This "Hello World" example of a document field integration renders a button and sets the field value to "Hello Button" when the user clicks the button.

The HTML for the integration renders the button and loads the client library and the script for the integration:

<!doctype html>
<html>
  <head>
    <title>Document Field Integration Example</title>

    <script type="text/javascript" src="https://unpkg.com/@bloomreach/[email protected]/dist/ui-extension.min.js"></script>
    <script type="text/javascript" src="index.js"></script>
  </head>
  <body>
    <p>Value: <span id="fieldValue"></span></p>
    <button id="setFieldValue">Set Field Value</button>
  </body>
</html>

The script registers the extension, shows the field value, enables the button, and adds an event listener to the button to set the field value when the user clicks the button:

document.addEventListener('DOMContentLoaded', async () => {
  try {
    const ui = await UiExtension.register();
    const brDocument = await ui.document.get();
    const value = await ui.document.field.getValue();
    
    showFieldValue(value);
    initSetFieldValueButton(ui, brDocument.mode);
    
  } catch (error) {
    console.error('Failed to register extension:', error.message);
    console.error('- error code:', error.code);
  }
});

function initSetFieldValueButton(ui, mode) {
  const buttonElement = document.querySelector('#setFieldValue');
  if (mode !== 'edit') {
    buttonElement.style.display = 'none';
    return;
  }

  buttonElement.style.display = 'block';
  buttonElement.addEventListener('click', async () => {
    try {
      var value = 'Hello Button'; 
      ui.document.field.setValue(value);
      showFieldValue(value);
    } catch (error) {
      console.error('Error: ', error.code, error.message);
    }
  });
}

function showFieldValue(value) {
  document.querySelector('#fieldValue').innerHTML = value;
}

Advanced Code Example (Experience Manager Only)

This advanced example extends the previous "Hello World" example with three additional buttons, some with form fields, that demonstrate the functions to access other fields in the document.

🚧

This example can only be used in the editor in the Experience manager app. In the Content app, the buttons will cause error messages to be logged in the Javascript console.

The HTML for the integration renders the buttons and form fields and loads the client library and the script for the integration:

<!doctype html>
<html>
<head>
    <title>Document Field Extension Example</title>

    <script type="text/javascript"
            src="https://unpkg.com/@bloomreach/[email protected]/dist/ui-extension.min.js"></script>
    <script type="text/javascript" src="index.js"></script>

    <!--style>
        html {
            font-family: 'Open Sans', Roboto, 'Helvetica Neue', sans-serif;
        }

        input {
            margin: 8px 0;
        }

        section {
            border-bottom: 1px dotted #000;
            padding-bottom: 8px;
            margin-bottom: 16px;
        }

        code {
            white-space: pre;
        }

        .container {
            margin-bottom: 8px;
        }
    </style-->
</head>
<body>

<section>
    <div class="container">
        Field value: <span id="fieldValue"></span>
    </div>
    <button id="setFieldValue">Set Field Value</button>
</section>

<section>
    <div class="container">
        <label for="documentFieldPathInputA">
            Document field path:
        </label>
        <br>
        <input type="text" id="documentFieldPathInputA" name="documentFieldPathInputA">
        <br>
        <label for="documentFieldInput">
            Document field value:
        </label>
        <br>
        <input type="text" id="documentFieldInput" name="documentFieldInput">
        <br>
    </div>
    <button id="setDocumentValueButton">Set document field</button>
</section>

<section>
    <div class="container">
        <label for="documentFieldPathInputB">
            Document field path:
        </label>
        <br>
        <input type="text" id="documentFieldPathInputB" name="documentFieldPathInputB">
        <br>
        Document field value:
        <br>
        <code id="documentFieldValue"></code>
    </div>
    <button id="getDocumentFieldValueButton">Get document field value</button>
</section>

<section>
    <div class="container">
        Document values:
        <code id="documentValues"></code>
    </div>
    <button id="getDocumentValuesButton">Get document values</button>
</section>

</body>
</html>

The script registers the extension, enables the buttons, and adds event listeners to the buttons that access other fields in the document when the user clicks them:

// Copyright 2021-2023 Bloomreach. All rights reserved. (https://www.bloomreach.com/)

(ui => {
    document.addEventListener('DOMContentLoaded', async () => {
        try {
            ui = await UiExtension.register();
            const brDocument = await ui.document.get();
            const value = await ui.document.field.getValue();

            await ui.document.field.setHeight(330);

            showFieldValue(value);
            initSetFieldValueButton(brDocument.mode);
            initSetDocumentValueButton(brDocument.mode);
            initGetDocumentValuesButton(brDocument.mode);
            initGetDocumentFieldValueButton(brDocument.mode);
        } catch (error) {
            console.error('Failed to register extension:', error.message);
            console.error('- error code:', error.code);
        }
    });

    function initSetFieldValueButton(mode) {
        const buttonElement = document.querySelector('#setFieldValue');
        if (mode !== 'edit') {
            buttonElement.style.display = 'none';
            return;
        }
        if (buttonElement) {
            buttonElement.style.display = 'block';
            buttonElement.addEventListener('click', async () => {
                try {
                    var value = 'Hello World';
                    ui.document.field.setValue(value);
                    showFieldValue(value);
                } catch (error) {
                    console.error('Error: ', error.code, error.message);
                }
            });
        }
    }

    function initSetDocumentValueButton(mode) {
        const buttonElement = document.querySelector('#setDocumentValueButton');
        const documentField = document.querySelector('#documentFieldInput');
        const documentFieldPath = document.querySelector('#documentFieldPathInputA');
        if (mode !== 'edit') {
            buttonElement.style.display = 'none';
            return;
        }
        if (buttonElement) {
            buttonElement.style.display = 'block';
            buttonElement.addEventListener('click', async () => {
                try {
                    // set field value
                    ui.document.setFieldValue(documentFieldPath.value, documentField.value);
                } catch (error) {
                    console.error('Error: ', error.code, error.message);
                }
            });
        }
    }

    function showFieldValue(value) {
        document.querySelector('#fieldValue').innerHTML = value;
    }

    function initGetDocumentFieldValueButton(mode) {
        const buttonElement = document.querySelector('#getDocumentFieldValueButton');
        const documentFieldPath = document.querySelector('#documentFieldPathInputB');
        const documentFieldValue = document.querySelector('#documentFieldValue');
        if (mode !== 'edit') {
            buttonElement.style.display = 'none';
            return;
        }
        if (buttonElement) {
            buttonElement.style.display = 'block';
            buttonElement.addEventListener('click', async () => {
                try {
                    documentFieldValue.innerHTML =
                        JSON.stringify(await ui.document.getFieldValue(documentFieldPath.value), null, 2);
                } catch (error) {
                    console.error('Error: ', error.code, error.message);
                }
            });
        }
    }

    function initGetDocumentValuesButton(mode) {
        const buttonElement = document.querySelector('#getDocumentValuesButton');
        const documentValuesElement = document.querySelector('#documentValues');
        if (mode !== 'edit') {
            buttonElement.style.display = 'none';
            return;
        }
        if (buttonElement) {
            buttonElement.style.display = 'block';
            buttonElement.addEventListener('click', async () => {
                try {
                    documentValuesElement.innerHTML = JSON.stringify(await ui.document.getValues(), null, 2);
                } catch (error) {
                    console.error('Error: ', error.code, error.message);
                }
            });
        }
    }
})();