Customizing the iTwin Viewer - "The Basics"

Tutorial result example

Introduction

This tutorial will take you through the first steps of customizing your iTwin Web Viewer. First you will learn how to add a new user interface component. Later you will customize that component to change the background color of your viewer.

Info

Skill level:

Basic

Duration:

30 minutes

Prerequisites

This tutorial assumes that you already have:

  • Your own local source for the iTwin Web Viewer based on the template @itwin/web-viewer
    • Instructions for that can be found here
  • Configured your local source to open the "House Model" sample iModel.
    • Instructions to use this sample iModel can be found here.

1. Add new iTwin Web Viewer interface component

The iTwin Web Viewer viewer template generates several files. To start with, let's take a look at the App.tsx file. This is where you should start in customizing your iTwin Viewer.

To start with App.tsx contains a single react functional component fittingly called App. The App component is responsible for:

  1. Authenticating the user
  2. Rendering the Viewer component

At the bottom of App.tsx you can see the return statement where the Viewer component is configured. Let's focus on that for now.

Return statement of App.tsx where the Viewer component is configured


jsx
return (
  <div className="viewer-container">
    {!accessToken && (
      <FillCentered>
        <div className="signin-content">
          <ProgressLinear indeterminate={true} labels={["Signing in..."]} />
        </div>
      </FillCentered>
    )}
    <Viewer
      iTwinId={iTwinId}
      iModelId={iModelId}
      authClient={authClient}
      viewCreatorOptions={viewCreatorOptions}
      enablePerformanceMonitors={true} // see description in the README (https://www.npmjs.com/package/@itwin/web-viewer-react)
    />
  </div>
);

App is just a react component. Like any react component, it returns JSX to tell react how to create HTML for the browser to render. Let's start off by adding some custom code to our JSX. We can render a "Hello World" span above the viewer by simply creating the element above the component. Note that this needs to be surrounded in a div per the single parent rule for react.

Viewer component with "Hello World" span


jsx
<div style={{ height: '100%' }}>
<span>"Hello World"</span>
  <Viewer
    iTwinId={iTwinId}
    iModelId={iModelId}
    authClient={authClient}
    viewCreatorOptions={viewCreatorOptions}
    enablePerformanceMonitors={true} // see description in the README (https://www.npmjs.com/package/@itwin/web-viewer-react)
  />
</div>

2. Your first UI Widget

So far, we haven't done anything to change the way the viewer works. We've only just added a new span element above the viewer. To add our "Hello World" span into the viewer, we need to pass the uiProviders prop to the Viewer component.

The uiProviders prop is typed to require an array of objects that implements the UIItemsProvider interface. Passing in the array will allow us to extend the Viewer with custom UI components. To do that, we need to define our MyFirstUiProvider class so that it implements the UiItemsProvider interface. Our new provider will tell the Viewer to include our "Hello world" span within the view.

Passing uiProviders prop to Viewer component


jsx
<Viewer
    iTwinId={iTwinId}
    iModelId={iModelId}
    authClient={authClient}
    viewCreatorOptions={viewCreatorOptions}
    enablePerformanceMonitors={true} // see description in the README (https://www.npmjs.com/package/@itwin/web-viewer-react)
    uiProviders={[new MyFirstUiProvider()]}
  />

Create a new file called MyFirstUiProvider.tsx with contents shown in a code snippet.

Let's review that code. We've defined our new MyFirstUiProvider class. In the new class we've defined public readonly id which is required to distinguish between different providers. Then notice that we've defined just one function called provideWidgets. This function will be called several times as the Viewer is building up the user interface. We will return an empty array except for when the location is equal to StagePanelLocation.Right and section is equal to StagePanelSection.Start. In that case, we will return a single widget that will supply our "Hello World" span.

Our helloWidget consists of three attributes:

  1. id - used to uniquely identify the widget
  2. label - description label for our widget
  3. getWidgetContent() - returns our custom UI component

"MyFirstUiProvider.tsx" file content


typescript
import {
  StagePanelLocation,
  StagePanelSection,
  StageUsage,
  UiItemsProvider,
  Widget,
} from '@itwin/appui-react';

export class MyFirstUiProvider implements UiItemsProvider {
public readonly id = 'MyFirstProviderId';

public provideWidgets(
    _stageId: string,
    stageUsage: string,
    location: StagePanelLocation,
    section?: StagePanelSection
  ): ReadonlyArray<Widget> {
    const widgets: Widget[] = [];
    if (
      stageUsage === StageUsage.General &&
      location === StagePanelLocation.Right &&
      section === StagePanelSection.Start
    ) {
      const helloWidget: Widget = {
        id: 'HelloWidget',
        label: 'Hello',
        content: <span>"Hello World"</span>
      };
      widgets.push(helloWidget);
  }
  return widgets;
}
}

At this point we need to import MyFirstUiProvider at the top of file App.tsx.

MyFirstUiProvider import


typescript
import { MyFirstUiProvider } from "./MyFirstUiProvider";

Finally, let's clean up the span and div that we added directly into the App component earlier.

Return statement in App.tsx should look like this:


typescript
return (
  <div className="viewer-container">
    {!accessToken && (
      <FillCentered>
        <div className="signin-content">
          <ProgressLinear indeterminate={true} labels={["Signing in..."]} />
        </div>
      </FillCentered>
    )}
    <Viewer
      iTwinId={iTwinId}
      iModelId={iModelId}
      authClient={authClient}
      viewCreatorOptions={viewCreatorOptions}
      enablePerformanceMonitors={true} // see description in the README (https://www.npmjs.com/package/@itwin/web-viewer-react)
      uiProviders={[new MyFirstUiProvider()]}
    />
  </div>
);

2.1 Result

Now we have our "Hello World" span displaying in a panel within the Viewer component. It should look like this:

HelloWorldWidget

3. Beyond Hello World

Saying hello to the world can be fun but we need to get past that. For this next step we'll swap out our trivial helloWidget with something a little more interactive: a ToggleSwitch. Eventually this toggle will control the background color, so we'll name our new widget backgroundColorWidget. Instead of returning a span we'll return a ToggleSwitch.

Start by navigating back to MyFirstUiProvider.tsx and adding an import for ToggleSwitch at the top of the file.

ToggleSwitch import


typescript
import { ToggleSwitch } from '@itwin/itwinui-react';

Next switch out the helloWidget with the new backgroundColorWidget.

Switch out the helloWidget with the new backgroundColorWidget


typescript
if (
stageUsage === StageUsage.General &&
location === StagePanelLocation.Right &&
section === StagePanelSection.Start
) {
const backgroundColorWidget: Widget = {
    id: 'BackgroundColorWidget',
    label: 'Background Color Toggle',
    content: <ToggleSwitch />
};
widgets.push(backgroundColorWidget);
}

Notice the only significant difference is that getWidgetContent is now returning a ToggleSwitch. It doesn't do anything interesting yet, but it should look like this:

Background Color Toggle

4. Changing the background color

For this last step, let's put our new toggle to work. We want the toggle to control the background color in the view of our house iModel. When the toggle is on, we'll override the background color to "skyblue". When the toggle is off, we'll change the background color back to its original color.

To do this, we need to pass the onChange prop to the ToggleSwitch component.

Passing the onChange prop to the ToggleSwitch component


typescript
return (
<ToggleSwitch
  onChange={(e) => {
    if (MyFirstUiProvider.toggledOnce === false) {
      MyFirstUiProvider.originalColor =
        IModelApp.viewManager.selectedView!.displayStyle.backgroundColor.tbgr;
      MyFirstUiProvider.toggledOnce = true;
    }

    const color = e.target.checked
      ? ColorDef.computeTbgrFromString("skyblue")
      : MyFirstUiProvider.originalColor;

    IModelApp.viewManager.selectedView!.overrideDisplayStyle({
      backgroundColor: color,
    });
  }}
/>
);

Since we're using two new static variables here, we need to add this to to our MyFirstUiProvider class at the beginning of our definition.

Adding toggledOnce and originalColor variables to MyFirstUiProvider class


typescript
export class MyFirstUiProvider implements UiItemsProvider {
public readonly id = 'HelloWorldProvider';
public static toggledOnce: boolean = false;
public static originalColor: number;

The first condition checks for only the first trigger of the toggle using boolean toggledOnce. If true, we need to store the original color in static variable MyFirstUiProvider.originalColor. We are using the global singleton IModelApp to get to the viewManager that can provide the current backgroundColor. We also need to flip variable MyFirstUiProvider.toggledOnce to true to make sure we only store the original color once.

Notice we're using the function overrideDisplayStyle() on the currently selected view. To get the view, we use the same global singleton IModelApp to get to the viewManager.

Our completed MyFirstUiProvider.tsx file should look similar to the one shown in a code snippet.

Completed MyFirstUiProvider.tsx file


typescript
import { ColorDef } from '@itwin/core-common';
import { IModelApp } from '@itwin/core-frontend';
import {
StagePanelLocation,
StagePanelSection,
StageUsage,
UiItemsProvider,
Widget,
} from '@itwin/appui-react';
import { ToggleSwitch } from "@itwin/itwinui-react";

export class MyFirstUiProvider implements UiItemsProvider {
public readonly id = 'HelloWorldProvider';
public static toggledOnce: boolean = false;
public static originalColor: number;

public provideWidgets(
  _stageId: string,
  stageUsage: string,
  location: StagePanelLocation,
  section?: StagePanelSection
): ReadonlyArray<Widget> {
  const widgets: Widget[] = [];
  if (
    stageUsage === StageUsage.General &&
    location === StagePanelLocation.Right &&
    section === StagePanelSection.Start
  ) {
    const backgroundColorWidget: Widget = {
      id: 'BackgroundColorWidget',
      label: 'Background Color Toggle',
      content: <ToggleSwitch
        onChange={(e) => {
          if (MyFirstUiProvider.toggledOnce === false) {
            MyFirstUiProvider.originalColor =
              IModelApp.viewManager.selectedView!.displayStyle.backgroundColor.tbgr;
            MyFirstUiProvider.toggledOnce = true;
          }
          const color = e.target.checked
            ? ColorDef.computeTbgrFromString("skyblue")
            : MyFirstUiProvider.originalColor;

          IModelApp.viewManager.selectedView!.overrideDisplayStyle({
            backgroundColor: color,
          });
        }}
      /> 
    };
    widgets.push(backgroundColorWidget);
  }
  return widgets;
}
}

5.1 Result

Result when the toggle is on:

Background blue

Result when the toggle is off:

Background original