Customizing the Nutrient Web SDK UI

The Web Viewer supports an API for customizing various parts of the SDK’s user interface (UI). This API enables in-place UI customization of various components in the SDK.

Use cases include:

  • Fully replace the default component UI (for example, the comment thread) with your own
  • Insert a custom UI at a predefined slot in an existing component (for example, a custom header in the comment thread)
  • Replace an existing slot in a component with your own custom UI (for example, replace the default editor in the comment thread with your own UI)

The support for new customization API is currently limited to a few components. We will be expanding this in the future.

Slots

The UI customization API is largely enabled through the concept of slots. These are passed to the SDK as part of load configuration using the ui property.

NutrientViewer.load({
// ... your config
ui: {
// config for UI customization
}
});

You can think of slots as predefined placeholders in the Web SDK UI where you can place your custom UI. The slots themselves might have a default UI, which can be replaced with a custom one, or they might be empty, enabling you to insert your own UI.

To start off, we’ve introduced a few slots that can be customized. For a list of currently supported slots, you may refer to the supported slots guide. We’ll gradually expand the slots to cover more components and functionalities.

Consider the following example of the comment thread component:

Default comment thread UI

The comment thread component is a slot that can be fully replaced with a custom UI. Besides being a slot itself, it also comprises (nested) slots within.

Concepts

Consider an example where you want to fully replace the entire comment thread UI with your own custom implementation. Specify the commentThread slot in the ui configuration.

NutrientViewer.load({
// ... your config
ui: {
commentThread: ...
}
});

To provide a custom UI, a slot accepts a function that returns an object with a render method. The render method should return a DOM Node. Example:

NutrientViewer.load({
// ... your config
ui: {
commentThread: (instance, id) => {
return {
render: (params) => {
const div = document.createElement("div");
// customize the div as needed
// ...
// return a DOM Node
return div;
}
};
}
}
});

Here’s the signature of the function which returns the render method:

type UIFactory = (instance, id) => {
render?: (params) => HTMLElement | null;
};

The SDK internally calls the UIFactory function once as it prepares to render the associated component. You can think of it as an initialization phase for the slot. It receives a couple of parameters - instance and id.

  • instance is the instance of Nutrient Web SDK and you may make full use of the SDK APIs available on it.
  • id is a unique identifier for the specific component being rendered into the slot. A component, such as comment thread, can have multiple instances, and the id helps to differentiate between them. id is a string value, for example 01K2HFFCTB5MZKY5H7P7XGYBGG which refers to a comment thread id.

After the UIFactory function is called, the SDK stores a reference to the returned object containing the render method.

The SDK will internally call render anytime params change and it expects a change in the particular UI. render will be called with the updated params. Following are important points to note:

  • The returned DOM Node from render will be placed into the specified slot.
  • If the specified slot is empty, the returned DOM Node will be appended to the slot DOM container.
  • If the slot already has a DOM Node, it will be replaced with the returned DOM Node from render.
  • If the slot already has a default UI, it will be replaced with the custom UI, for example, comment thread UI has an editor slot which renders the default editor UI. You may replace the UI contained in this slot with your own custom UI.
  • If the slot has no default UI, the custom UI will be inserted into the slot, for example, comment thread has a header slot which is empty by default. You can insert your own custom UI into this slot.

render may be called multiple times, so you should treat it as a pure function and avoid side effects inside it, for example, avoid changing any pre-existing variables or objects outside its scope. You should use render to return a DOM Node that represents the current state of the UI, for example, if render is called multiple times with the same params you should write the logic such that it formulates the same expected markup each time.

The params received by render contains properties related to the specific slot (such as id) that may be useful for constructing the UI. We’ll be adding more useful properties to params incrementally.

Fully customize a slot

Below is an example of how you can fully customize the comment thread UI with your own:

NutrientViewer.load({
// ... your config
ui: {
commentThread: (instance, id) => ({
render: (params) => {
// return a DOM Node
const div = document.createElement("div");
div.style.backgroundColor = "lightblue";
div.style.padding = "10px";
div.style.border = "1px solid #ccc";
div.style.borderRadius = "5px";
div.innerText = `This is a custom UI for the comment thread: ${id}`;
return div;
}
})
}
});

In the above example, you’ll notice the function passed to the commentThread slot follows the same UIFactory signature as described earlier. This will replace the default comment thread UI with the returned custom UI:

Full customization of Comment Thread UI

Nested slots

Slots form a hierarchical structure representing the tree of components available for UI customization. This enables partial customization use cases where you want to replace only a part of the component UI, while keeping the rest intact. This is especially useful where you just need to insert a custom UI at a specific location in the component.

In order to enable nested slots for a particular slot you can pass an object, instead of a function. To customize a nested slot, you can then pass a function to the nested slot key having the same UIFactory signature.

Below is an example of how you can customize the nested slots for header and footer in comment thread UI with your own:

NutrientViewer.load({
// ... your config
ui: {
// instead of UIFactory function, we pass an object to commentThread slot specifying nested slots
commentThread: {
// nested slot for header having the same UIFactory signature
header: () => {
return {
render: () => {
const div = document.createElement("div");
div.style.backgroundColor = "lightgreen";
div.style.padding = "5px";
div.innerText = "This is a custom header for the comment thread.";
return div;
},
};
},
// nested slot for footer having the same UIFactory signature
footer: () => {
return {
render: () => {
const div = document.createElement("div");
div.style.backgroundColor = "lightcoral";
div.style.padding = "5px";
div.innerText = "This is a custom footer for the comment thread.";
return div;
},
};
},
}
}
});

The above example inserts the specified custom UI into the header and footer slots respectively, while keeping the rest of the comment thread UI intact.

Customizing nested slots in Comment Thread UI

Lifecycle methods

Besides the render method, you can also define lifecycle methods in the object returned by the UIFactory function. These methods enable you to perform actions at specific points in the component’s lifecycle. They’re also useful for effects such as adding event listeners or performing cleanup.

They also receive an id parameter which can be used to identify the specific instance of the component.

Following are the currently supported lifecycle methods:

  • onMount — Called when the component is mounted. You can use this to perform any setup actions, such as adding event listeners, and analytics events.
  • onUnmount — Called when the component is unmounted. You can use this to perform any cleanup actions, such as removing event listeners.

Below is an example of how you can use lifecycle methods in the commentThread slot:

NutrientViewer.load({
// ... your config
ui: {
commentThread: (instance, id) => {
const div = document.createElement("div");
return {
render: (params) => {
// return a DOM Node
div.innerText = `This is a custom UI for the comment thread`;
return div;
},
onMount: (id) => {
console.log(`Comment thread mounted with id: ${id}`);
// You can add event listeners or perform other setup actions here
},
onUnmount: (id) => {
console.log(`Comment thread unmounted with id: ${id}`);
// You can remove event listeners or perform other cleanup actions here
}
};
}
}
});

Similarly, you can use lifecycle methods in nested slots as well. Below is an example of how you can use lifecycle methods in the header slot of the commentThread:

NutrientViewer.load({
// ... your config
ui: {
commentThread: {
header: (instance, id) => {
const div = document.createElement("div");
div.innerText = "This is a custom header for the comment thread.";
return {
render: () => div,
onMount: (id) => {
console.log(`Header mounted with id: ${id}`);
// You can add event listeners or perform other setup actions here
},
onUnmount: (id) => {
console.log(`Header unmounted with id: ${id}`);
// You can remove event listeners or perform other cleanup actions here
}
};
},
}
}
});

Conclusion

The UI customization API provides ways to fully or partially customize various parts of the Nutrient Web SDK UI with slots. Currently the supported slots are limited, but we will be expanding them in the near future.

For a list of currently supported slots, refer to the supported slots guide.