Custom Component

This feature is not available in production yet.

Custom Component is a regular component that allows you to specify an HTML template, CSS and lifecycle JavaScript functions that hook into the Adapptio framework.

Using this feature, you can create your own UI components, including high-performance solutions and custom logic. A custom component is able to provide data to the scope (available to other components), including a method. Also, it's able to trigger events.

Configuration Properties

  • Component Props - custom data you can pass to the component. These data are available in the lifecycle functions, and the On Props Changed function is called when props change.

  • HTML Template - an initial HTML that is rendered when the component is created. When this property changes, the component will be disposed and mounted again.

  • CSS class - a CSS class name of the div element that's wrapping the HTML template. Use this class to style your component.

  • CSS code - a custom CSS code injected into the page. This code is injected directly into the page header, and it applies globally. You have to be careful not to conflict with existing CSS, or you can break the editor or an application styling. If multiple custom components provide the same CSS code (eg. you copy/paste the component), it will be injected only once.

  • On Mount - a JavaScript function that is executed when the component is mounted to the DOM. See the lifecycle functions section below.

  • On Props Changed - a JavaScript function that is executed when Component Props change. See the lifecycle functions section below.

  • On Update Method - a JavaScript function that is executed when the update method is called via an event. See the lifecycle functions section below.

  • On Dispose - a JavaScript function that is executed when the component is being destroyed. In this function, you must unbind all event listeners, stop all timers, etc., to avoid memory leaks. See the lifecycle functions section below.

  • External JS scripts - a list of URL addresses of external JS scripts to be loaded. The On Mount function will be executed after all external scripts are successfully loaded. If multiple components require the same scripts, they will be loaded only once. You can use this feature to load 3rd party libraries.

During a server-side rendering, only the HTML template will be rendered. All other scripts will be executed on the client side.

Lifecycle Functions

The lifecycle functions are JavaScript functions executed at certain component phases.

The following variables are available in all lifecycle functions:

  • props - An object containing Component props as passed from the configuration.

  • state - A mutable object instance you can use to store any data and pass them around between individual function calls.

  • domEl - A DOM element that contains the rendered HTML template. You can use this to access child elements, bind events, etc.

  • scopeData - Current component scope data. Scope data are exposed to other components.

  • setScopeData(prevData: any|(prevData: any) => any) - A function to set new scope data. To perform an atomic update, pass a function into the prevData argument. It will receive previous scope data as the first argument. Using this, you will avoid data racing conditions.

  • triggerEvent(data: any) => any - A function you can call in order to trigger a custom event (Inspector -> Event -> Custom Event). Data you pass into the data argument will be available in the event flow as an eventData variable.

  • componentMode - A string (enum) specifying if the component is being rendered in the edit mode, read-only mode or in standard mode (preview / deployed application). Possible values are: "normal", "readonly", "edit". You can use this property to enhance an editing experience (like disabling pointer events or allowing inline editing).

Your JavaScript code is not sandboxed and runs directly on the page. If you create an infinite loop or computationally expensive operation, your browser may become unresponsive, and you might be unable to save your changes.

If this happens, reload the page. Scripts will be automatically disabled.

Anyway, try to avoid these issues by writing safe code.

On Mount

This function is called when the component is created, the initial HTML template is rendered, and DOM elements are mounted. Also, all external scripts are loaded as well.

Use this function to bind event listeners and to define shared functions. You can store any data by assigning them to some property in the state variable. See the example below.

On Props Changed

This function is called when the Component Props value changes. You can use this function to update your component's DOM or other internals based on new props.

On Update Method

A custom component provides the update method in its scope. This method can be called from regular event flows. When the method is called, the On Update Method is executed.

In addition to the variables described above, in this function, the args variable is available. This variable contains method call arguments in an array.

On Dispose

This function is called when the component is destroyed (unmounted). Your job is to perform correct clean-up in this function - unbind event listeners, stop timers, etc. This is very important in order to avoid memory leaks and performance issues.

In the editor, when any of the lifecycle function's code or HTML template changes, the component is always disposed and then mounted again.

Hello World Example

Let's create a simple custom component with HTML input.

It will receive two props: label and initialValue. When a user types in, the value will be exported to the scope, and a custom event will be triggered.

Also, when component props change, the DOM will be updated. When the initialValue changes, it will reset the field value to the initial one.

And it will handle an update method with setValue argument that will directly set the input's value.

Component Props

HTML Template

<label>
  <span class="fld-label"></span>
  <input type="text" class="fld-input" value="" />
</label>

CSS

Set CSS Class to custom-cmp-hello and the CSS Code to:

.custom-cmp-hello {
  padding: 10px;
}

.custom-cmp-hello .fld-label {
  display: block;
  font-size: 14px;
  font-weight: 500;
  margin-bottom: 4px;
}

.custom-cmp-hello .fld-input {
  padding: 8px 10px;
  border-radius: 4px;
  border: 1px solid #cccccc;
  font-size: 15px;
}

On Mount

// Get references to template elements
const labelEl = domEl.querySelector(".fld-label");
const inputEl = domEl.querySelector(".fld-input");

/**
 * @typedef {{ label: string, initialValue: string }} Props
 */

/**
 * Handle update of props
 * @param {Props} newProps
 */
state.updateProps = (newProps) => {
  state.initialValue = newProps.initialValue;

  labelEl.innerText = newProps.label;

  if (state.initialValue !== inputEl.value) {
    inputEl.value = state.initialValue;
    state.handleValueUpdate();
  }
};

/**
 * Handle update of input value
 * Provides new value into scope
 */
state.handleValueUpdate = () => {
  setScopeData((prevData) => {
    return {
      ...prevData,
      initialValue: state.initialValue,
      value: inputEl.value
    };
  });

  triggerEvent({
    name: "valueChanged",
    value: inputEl.value
  });
}

/**
 * Sets the input value and triggers update
 * 
 * @param {string} newValue
 */
state.setValue = (newValue) => {
  inputEl.value = newValue;
  state.handleValueUpdate();
}

// Bind event listener
inputEl.addEventListener("input", state.handleValueUpdate);

// Trigger initial props update
state.updateProps(props);

On Props Changed

state.updateProps(props);

On Update Method

if (typeof args[0] !== "string") {
  throw new Error("Invalid argument #1 - expected string.")
}

switch(args[0]) {
  case "setValue": {
    if (typeof args[1] !== "string") {
      throw new Error("Invalid argument #2 - expected string.")
    }

    state.setValue(args[1]);

    break;
  }
  default: {
    throw new Error("Invalid operation.");
  }
}

On Dispose

const inputEl = domEl.querySelector(".fld-input");
inputEl.removeEventListener("input", state.handleValueUpdate);

Test It

And that's it. Try to switch to the Preview Mode, type something in and watch how the scope changes. Also, you can try to display a toast message every time the value changes.

Here's an example of a demo view.

Last updated