# Building Custom Components

Canonical page: https://www.adaptabletools.com/docs/ui-tutorial-building-custom-components

<!-- framework: vanilla -->
### Vanilla

- AdapTable Vanilla lets you extend the UI with bespoke content via `render` functions and AdapTable Forms
- Each Custom Component has its own guide under **UI Components** with vanilla DevSteps and demos

The AdapTable UI, and associated components, is designed to be fully extensible.

We expect developers to add additional, bespoke components to meet their requirements

You can add the following custom components using TypeScript and HTML returned from `render` callbacks.

| Custom Component    | UI guide                                                                             |
| ------------------- | ------------------------------------------------------------------------------------ |
| Dashboard Toolbars  | [Custom Toolbars](https://www.adaptabletools.com/docs/ui-dashboard-custom-toolbars/index.md)           |
| Tool Panels         | [Custom Tool Panels](https://www.adaptabletools.com/docs/ui-tool-panel-custom-tool-panel/index.md)     |
| Settings Panels     | [Custom Settings Panels](https://www.adaptabletools.com/docs/ui-settings-panel-custom/index.md)        |
| Column Menu Items   | [Custom Column Menu Items](https://www.adaptabletools.com/docs/ui-column-menu-custom-items/index.md)   |
| Context Menu Items  | [Custom Context Menu Items](https://www.adaptabletools.com/docs/ui-context-menu-custom-items/index.md) |
| Windows             | [Custom Popups](https://www.adaptabletools.com/docs/ui-popup-windows/index.md)                         |
| Progress Indicators | [Custom Progress Indicators](https://www.adaptabletools.com/docs/ui-progress-indicator/index.md)       |

- These pages use the **Vanilla** framework picker setting to see the `render`-based workflow
- For AdapTable React, Angular, or Vue change the Framework Picker in the header as required

<!-- /framework -->

<!-- framework: angular -->
### Angular

- AdapTable Angular lets you extend the UI with bespoke Angular components
- These use `AngularFrameworkComponent` and complement AdapTable's built-in UI

The AdapTable UI, and associated components, is designed to be fully extensible.

We expect developers to add additional, bespoke Angular components to meet their requirements

The following custom Angular components can be provided by developers:

| Custom Component    | UI guide                                                                             |
| ------------------- | ------------------------------------------------------------------------------------ |
| Dashboard Toolbars  | [Custom Toolbars](https://www.adaptabletools.com/docs/ui-dashboard-custom-toolbars/index.md)           |
| Tool Panels         | [Custom Tool Panels](https://www.adaptabletools.com/docs/ui-tool-panel-custom-tool-panel/index.md)     |
| Settings Panels     | [Custom Settings Panels](https://www.adaptabletools.com/docs/ui-settings-panel-custom/index.md)        |
| Column Menu Items   | [Custom Column Menu Items](https://www.adaptabletools.com/docs/ui-column-menu-custom-items/index.md)   |
| Context Menu Items  | [Custom Context Menu Items](https://www.adaptabletools.com/docs/ui-context-menu-custom-items/index.md) |
| Windows             | [Custom Popups](https://www.adaptabletools.com/docs/ui-popup-windows/index.md)                         |
| Progress Indicators | [Custom Progress Indicators](https://www.adaptabletools.com/docs/ui-progress-indicator/index.md)       |

## AngularFrameworkComponent

All custom UI extensions in AdapTable Angular leverage the [`AngularFrameworkComponent`](https://www.adaptabletools.com/docs/reference/angularframeworkcomponent.md) object.

It references an Angular component class via `type`, and optionally configures that instance via `onSetup`.

```ts
export type AngularFrameworkComponent<T = unknown> = {
  type: T;
  onSetup?: ({adaptableApi}: {adaptableApi: AdaptableApi}) => Partial<T>;
};
```

- The `AngularFrameworkComponent` is **automatically** invoked when its containing element becomes visible
- And it is automatically unmounted when the component is hidden

### `type` property

This is a standard Angular component class. It is automatically instantiated or destroyed by AdapTable at runtime.

### `onSetup()` function

If specified, this (optional) function is called after each component instantiation and the returned object properties are assigned to the component instance.

### Wiring in Adaptable Options

Pass an `AngularFrameworkComponent` on the `frameworkComponent` property (for example in `dashboardOptions.customToolbars`):

```ts
frameworkComponent: {
  type: SlideToggleComponent,
  onSetup: ({adaptableApi}) => ({
    onToggleChange: (value: boolean) => {
      /* use adaptableApi */
    },
  }),
},
```

- `onSetup` is **not** invoked for [Column Menu](https://www.adaptabletools.com/docs/ui-column-menu-custom-items/index.md) or [Context Menu](https://www.adaptabletools.com/docs/ui-context-menu-custom-items/index.md) custom items — because AG Grid owns that lifecycle
- Use `frameworkComponent: { type: MyMenuItemComponent }` and read `params.menuItemParams` in `agInit` instead

### Adaptable Api Injection Token

```ts
const ADAPTABLE_API = new InjectionToken<AdaptableApi>('ADAPTABLE_API');
```

Inject [Adaptable API](https://www.adaptabletools.com/docs/technical-reference-adaptable-api/index.md) in your component constructor:

```ts
constructor(@Inject(ADAPTABLE_API) private adaptableApi: AdaptableApi) {}
```

<!-- /framework -->

<!-- framework: react -->
### React

- AdapTable React lets you extend the UI with bespoke React components via `ReactFrameworkComponent`
- Custom Toolbars, Tool Panels, Settings Panels, Menu Items, Windows, and Progress Indicators are supported

The AdapTable UI, and associated components, is designed to be fully extensible.

We expect developers to add additional, bespoke React components to meet their requirements

The following custom React components can be provided by developers:

| Custom Component    | UI guide                                                                             |
| ------------------- | ------------------------------------------------------------------------------------ |
| Dashboard Toolbars  | [Custom Toolbars](https://www.adaptabletools.com/docs/ui-dashboard-custom-toolbars/index.md)           |
| Tool Panels         | [Custom Tool Panels](https://www.adaptabletools.com/docs/ui-tool-panel-custom-tool-panel/index.md)     |
| Settings Panels     | [Custom Settings Panels](https://www.adaptabletools.com/docs/ui-settings-panel-custom/index.md)        |
| Column Menu Items   | [Custom Column Menu Items](https://www.adaptabletools.com/docs/ui-column-menu-custom-items/index.md)   |
| Context Menu Items  | [Custom Context Menu Items](https://www.adaptabletools.com/docs/ui-context-menu-custom-items/index.md) |
| Windows             | [Custom Popups](https://www.adaptabletools.com/docs/ui-popup-windows/index.md)                         |
| Progress Indicators | [Custom Progress Indicators](https://www.adaptabletools.com/docs/ui-progress-indicator/index.md)       |

## ReactFrameworkComponent

All Custom Components which target AdapTable React leverage the [`ReactFrameworkComponent`](https://www.adaptabletools.com/docs/reference/reactframeworkcomponent.md) object.

This is a function that receives [Adaptable API](https://www.adaptabletools.com/docs/technical-reference-adaptable-api/index.md) and returns a React element.

```ts
export type ReactFrameworkComponent = ({
  adaptableApi,
}: {
  adaptableApi: AdaptableApi;
}) => ReactElement;
```

- The `ReactFrameworkComponent` is **automatically** mounted and rendered when its containing element becomes visible
- And it is automatically unmounted when the component is hidden

## Using State

A common requirement is for custom React components (e.g. Custom Toolbars) to listen to app state.

From AdapTable `18.1.10`, custom components render in the same React tree as your app when you wrap AdapTable with your context providers inside `<Adaptable.Provider />`.

In order to use context, render `<Adaptable.Provider />` inside your app context provider and then you can use the context values in your custom toolbar components.

- This section applies when you provide custom UI via `ReactFrameworkComponent` (toolbars, tool panels, and similar)
- If you need React state inside [Adaptable Options](https://www.adaptabletools.com/docs/technical-reference-adaptable-options/index.md) itself — for example dynamic context menu configuration defined in options rather than a separate component — see [Component State in Adaptable Options](https://www.adaptabletools.com/docs/react-dev-guide-tutorial-component-state-in-adaptable-options/index.md)

```tsx
const App = () => {
  const [appState, setAppState] = useState({
    //...
  });

  return <AppContext.Provider value={appState}>

    {/* Your custom toolbars used in Adaptable will have access to the app state */}
    <Adaptable.Provider
      gridOptions={agGridOptions}
      adaptableOptions={adaptOptions}
      modules={[...agGridModules]}
    >
      <Adaptable.UI style={{flex: 'none'}} />
      <Adaptable.AgGridReact  />
    </Adaptable.Provider>

  </AppContext.Provider>
```

**Example: Custom React Toolbars with state from context**

Using React state in Custom Components

- In this demo we provide a Custom Toolbar, which allows star-rating of the app
- The custom component is displayed both outside of AdaTtable as well as inside the AdapTable Dashboard, via a custom toolbar
- The two places where the component is displayed **share the same state**
- Note: the custom `<AppStars />` component uses [React context](https://react.dev/reference/react/useContext) to access the app state
  
### Expand to see how state was shared

```tsx
const AppContext = createContext({
  appStars: 3,
  setAppStars: (value: number) => {},
});
const adaptableOptions = {
  primaryKey: 'id',
  adaptableId: 'Custom Toolbar Demo',
  dashboardOptions: {
    customToolbars: [
      {
        name: 'AppStars',
        title: 'App Stars',
        frameworkComponent: () => {
          return <AppStars />;
        },
      },
    ],
  }
  initialState: {
    Dashboard: {
      Tabs: [
        {
          Name: 'Grid Configuration',
          Toolbars: ['LayoutToggle', 'Layout', 'AppStars'],
        },
      ],
    },
    // ...
  }
}
const App = () => {
  const [appStars, setAppStars] = useState(3);
  const appState = {
    appStars,
    setAppStars,
  };
  return <AppContext.Provider value={appState}>
    <AppStars />
    <Adaptable.Provider
      gridOptions={agGridOptions}
      adaptableOptions={adaptOptions}
      modules={[...agGridModules]}
      onAdaptableReady={(adaptableReadyInfo: AdaptableReadyInfo) => {
        // save a reference to adaptable api
        adaptableApiRef.current = adaptableReadyInfo.adaptableApi;
        onAdaptableReady(adaptableReadyInfo);
      }}>
      <Adaptable.UI style={{flex: 'none'}} />
      <Adaptable.AgGridReact  />
    </Adaptable.Provider>
  </AppContext.Provider>
}
```

Demo project (React); the files without a marker are shown below:

```text
src/
  agGridModules.ts     [template]
  App.tsx
  AppContext.ts
  AppStars.tsx
  columnDefs.ts        [template]
  gridOptions.ts       [template]
  index.tsx            [template]
  onAdaptableReady.ts  [template]
  rowData.ts           [template]
  SlideToggle.tsx
  styles.css           [template + shown below]
package.json           [template]
tsconfig.json          [template]
```

Files marked `[template]` are the same in every demo; see the [demo template](https://www.adaptabletools.com/llms/demo-template.md#react).

**`src/App.tsx`**

```ts
import * as React from 'react';
import {useState} from 'react';
import {
  Adaptable,
  AdaptableApi,
  AdaptableOptions,
  AdaptableReadyInfo,
} from '@adaptabletools/adaptable-react-aggrid';

import {GridOptions} from 'ag-grid-enterprise';

// import adaptable css
import '@adaptabletools/adaptable-react-aggrid/index.css';
// import aggrid themes (using Balham theme)

import './styles.css';

import {onAdaptableReady} from './onAdaptableReady';
import {gridOptions} from './gridOptions';
import {agGridModules} from './agGridModules';
import {SlideToggle} from './SlideToggle';
import {AppStars} from './AppStars';
import {AppContext} from './AppContext';
import {WebFramework} from './rowData';

const App: React.FunctionComponent = () => {
  const adaptableApiRef = React.useRef<AdaptableApi>(null);

  const [appStars, setAppStars] = useState(3);

  const appState = {
    appStars,
    setAppStars,
  };

  const appStateRef = React.useRef(appState);
  appStateRef.current = appState;

  const [adaptOptions, setAdaptOptions] = useState<
    AdaptableOptions<WebFramework>
  >({
    primaryKey: 'id',
    adaptableId: 'Using React State Demo',
    stateOptions: {loadState: () => Promise.resolve({})},
    dashboardOptions: {
      customToolbars: [
        {
          name: 'LayoutToggle',
          title: 'Current layout',
          frameworkComponent: ({adaptableApi}) => {
            return (
              <SlideToggle
                onText={'Pivot'}
                offText={'Standard'}
                onToggleChange={(toggleValue: boolean) => {
                  if (toggleValue) {
                    adaptableApi.layoutApi.setLayout('Pivot Cols Layout');
                  } else {
                    adaptableApi.layoutApi.setLayout('Standard Layout');
                  }
                }}
              />
            );
          },
        },
        {
          name: 'AppStars',
          title: 'App Stars',
          frameworkComponent: () => {
            return <AppStars />;
          },
        },
      ],
    },
    initialState: {
      Dashboard: {
        Tabs: [
          {
            Name: 'Grid Configuration',
            Toolbars: ['LayoutToggle', 'Layout', 'AppStars'],
          },
        ],
      },
      Theme: {CurrentTheme: 'dark'},
      Layout: {
        CurrentLayout: 'Standard Layout',
        Layouts: [
          {
            Name: 'Standard Layout',
            TableColumns: [
              'name',
              'language',
              'github_stars',
              'topics',
              'license',
              'created_at',
              'has_wiki',
              'updated_at',
              'pushed_at',
              'github_watchers',
              'description',
              'open_issues_count',
              'closed_issues_count',
              'open_pr_count',
              'closed_pr_count',
              'has_projects',
              'has_pages',
              'week_issue_change',
            ],
            AutoSizeColumns: true,
          },
          {
            Name: 'Pivot Cols Layout',
            PivotColumns: ['license'],
            PivotGroupedColumns: ['language'],
            PivotAggregationColumns: [
              {
                ColumnId: 'github_watchers',
                AggFunc: 'sum',
              },
              {
                ColumnId: 'github_stars',
                AggFunc: 'sum',
              },
            ],
          },
        ],
      },
      StyledColumn: {
        StyledColumns: [
          {
            ColumnId: 'github_stars',
            Name: 'github_stars Gradient',
            GradientStyle: {
              RangeValueType: 'Number',
              CellRanges: [{Min: 'Col-Min', Max: 'Col-Max', Color: '#a52a2a'}],
            },
          },
        ],
      },
      QuickSearch: {
        CellMatchStyle: {
          BackColor: '#a1c0d4',
        },
      },
    },
  });
  const [agGridOptions, setAgGridOptions] = useState<GridOptions>(gridOptions);

  return (
    <AppContext.Provider value={appState}>
      <div style={{display: 'flex', flexFlow: 'column', height: '100vh'}}>
        <div
          style={{
            background: 'var(--ab-color-primary)',
            color: 'var(--ab-color-primary-foreground)',
            padding: 'calc(var(--ab-base-space) * 2)',
          }}>
          <AppStars />
        </div>
        <Adaptable.Provider
          gridOptions={agGridOptions}
          adaptableOptions={adaptOptions}
          modules={[...agGridModules]}
          onAdaptableReady={(adaptableReadyInfo: AdaptableReadyInfo) => {
            // save a reference to adaptable api
            adaptableApiRef.current = adaptableReadyInfo.adaptableApi;
            onAdaptableReady(adaptableReadyInfo);
          }}>
          <Adaptable.UI style={{flex: 'none'}} />
          <Adaptable.AgGridReact />
        </Adaptable.Provider>

        <div id="demoOutputBelow"></div>
      </div>
    </AppContext.Provider>
  );
};
export default App;
```

**`src/SlideToggle.tsx`**

```ts
import * as React from 'react';
import {useState} from 'react';

export interface SlideToggleProps {
  defaultValue?: boolean;
  onText?: string;
  onColor?: string;
  offText?: string;
  offColor?: string;
  onToggleChange(toggleValue: boolean): void;
}
export const SlideToggle = (props: SlideToggleProps) => {
  const {
    defaultValue = false,
    onText = 'ON',
    onColor = 'var(--ab-color-success)',
    offText = 'OFF',
    offColor = 'var(--ab-color-info)',
    onToggleChange,
  } = props;

  const [toggleValue, setToggleValue] = useState(defaultValue);

  return (
    <>
      <label className="switch">
        <input
          className="switch-input"
          type="checkbox"
          checked={toggleValue}
          onChange={() => {
            const newValue = !toggleValue;
            setToggleValue(newValue);
            onToggleChange(newValue);
          }}
        />
        <span
          className="switch-label"
          data-on={onText}
          data-off={offText}
          style={{background: toggleValue ? onColor : offColor}}></span>
        <span className="switch-handle"></span>
      </label>
    </>
  );
};
```

**`src/AppStars.tsx`**

```ts
import * as React from 'react';
import {useContext, useState} from 'react';
import {AppContext} from './AppContext';

export const AppStars = () => {
  const {appStars: stars, setAppStars} = useContext(AppContext);

  const [hoveredStars, setHoveredStars] = useState<number | null>(null);

  return (
    <>
      <div>
        <div>Click to star the app</div>
        <div>
          {new Array(5).fill(0).map((_, i) => {
            const current = i + 1;
            const selected =
              hoveredStars != null ? current <= hoveredStars : current <= stars;

            return (
              <button
                className="app-star-button"
                key={current}
                style={{
                  display: 'inline-block',
                  color: 'var(--ab-color-primary-foreground)',
                }}
                onMouseEnter={() => setHoveredStars(current)}
                onMouseLeave={() => setHoveredStars(null)}
                onClick={() => setAppStars(current)}>
                {selected ? '⭐' : '☆'}
              </button>
            );
          })}
        </div>
      </div>
    </>
  );
};
```

**`src/AppContext.ts`**

```ts
import {createContext} from 'react';

export const AppContext = createContext({
  appStars: 3,
  setAppStars: (value: number) => {},
});
```

**`src/styles.css`**

```css
/*
  toggle-switch
*/
.switch {
  position: relative;
  display: block;
  width: 100px;
  height: 30px;
  padding: 3px;
  margin: 2px;
  background: -webkit-linear-gradient(top, #eeeeee, #ffffff 25px);
  border-radius: 18px;
  box-shadow: inset 0 -1px white, inset 0 1px 1px rgba(0, 0, 0, 0.05);
  cursor: pointer;
  box-sizing: content-box;
}
.switch-input {
  position: absolute;
  top: 0;
  left: 0;
  opacity: 0;
  box-sizing: content-box;
}
.switch-input:checked ~ .switch-label {
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15),
    inset 0 0 3px rgba(0, 0, 0, 0.2);
}
.switch-input:checked ~ .switch-label:before {
  opacity: 0;
}
.switch-input:checked ~ .switch-label:after {
  opacity: 1;
}
.switch-input:checked ~ .switch-handle {
  left: 74px;
  box-shadow: -1px 1px 5px rgba(0, 0, 0, 0.2);
}
.switch-label {
  position: relative;
  display: block;
  height: inherit;
  background: #eceeef;
  font-size: 10px;
  text-transform: uppercase;
  border-radius: inherit;
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.12),
    inset 0 0 2px rgba(0, 0, 0, 0.15);
  box-sizing: content-box;
  transition: All 0.3s ease;
  -webkit-transition: All 0.3s ease;
  -moz-transition: All 0.3s ease;
  -o-transition: All 0.3s ease;
}
.switch-label:before {
  position: absolute;
  top: 50%;
  margin-top: -0.5em;
  line-height: 1;
  -webkit-transition: inherit;
  -moz-transition: inherit;
  -o-transition: inherit;
  transition: inherit;
  box-sizing: content-box;
  content: attr(data-off);
  right: 11px;
  color: #ffffff;
  text-shadow: 0 1px rgba(0, 0, 0, 0.2);
}
.switch-label:after {
  position: absolute;
  top: 50%;
  margin-top: -0.5em;
  line-height: 1;
  -webkit-transition: inherit;
  -moz-transition: inherit;
  -o-transition: inherit;
  transition: inherit;
  box-sizing: content-box;
  content: attr(data-on);
  left: 11px;
  color: #ffffff;
  text-shadow: 0 1px rgba(0, 0, 0, 0.2);
  opacity: 0;
}
.switch-handle {
  position: absolute;
  top: 4px;
  left: 4px;
  width: 28px;
  height: 28px;
  background: -webkit-linear-gradient(top, #ffffff 40%, #f0f0f0);
  border-radius: 100%;
  box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.2);
  transition: All 0.3s ease;
  -webkit-transition: All 0.3s ease;
  -moz-transition: All 0.3s ease;
  -o-transition: All 0.3s ease;
}
.switch-handle:before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  margin: -6px 0 0 -6px;
  width: 12px;
  height: 12px;
  background: -webkit-linear-gradient(top, #eeeeee, #ffffff);
  border-radius: 6px;
  box-shadow: inset 0 1px rgba(0, 0, 0, 0.02);
}

/* App Stars */

.app-star-button {
  font-size: var(--ab-font-size-3);
  padding: calc(var(--ab-base-space) * 2);
  height: 30px;
  width: 30px;
  border-radius: 50%;
  border: none;
  line-height: 0px;
  background: var(--ab-color-primary);
  color: var(--ab-color-primary-foreground);
}
.app-star-button:hover {
  background: tomato;

  cursor: pointer;
}
```



<!-- /framework -->

<!-- framework: vue -->
### Vue

- AdapTable Vue lets you extend the UI with bespoke Vue components via `VueFrameworkComponent`
- Custom Toolbars, Tool Panels, Settings Panels, Menu Items, Windows, and Progress Indicators are supported

The AdapTable UI, and associated components, is designed to be fully extensible.

We expect developers to add additional, bespoke Vue components to meet their requirements

The following custom Vue components can be provided by developers:

| Custom Component    | UI guide                                                                             |
| ------------------- | ------------------------------------------------------------------------------------ |
| Dashboard Toolbars  | [Custom Toolbars](https://www.adaptabletools.com/docs/ui-dashboard-custom-toolbars/index.md)           |
| Tool Panels         | [Custom Tool Panels](https://www.adaptabletools.com/docs/ui-tool-panel-custom-tool-panel/index.md)     |
| Settings Panels     | [Custom Settings Panels](https://www.adaptabletools.com/docs/ui-settings-panel-custom/index.md)        |
| Column Menu Items   | [Custom Column Menu Items](https://www.adaptabletools.com/docs/ui-column-menu-custom-items/index.md)   |
| Context Menu Items  | [Custom Context Menu Items](https://www.adaptabletools.com/docs/ui-context-menu-custom-items/index.md) |
| Windows             | [Custom Popups](https://www.adaptabletools.com/docs/ui-popup-windows/index.md)                         |
| Progress Indicators | [Custom Progress Indicators](https://www.adaptabletools.com/docs/ui-progress-indicator/index.md)       |

## VueFrameworkComponent

All custom UI extensions in AdapTable Vue leverage the [`VueFrameworkComponent`](https://www.adaptabletools.com/docs/reference/vueframeworkcomponent.md) type.

It is a **factory function** that receives [Adaptable API](https://www.adaptabletools.com/docs/technical-reference-adaptable-api/index.md) and returns a Vue component (typically a VNode created with `h()`):

```ts
export type VueFrameworkComponent<Component extends unknown = unknown> = ({
  adaptableApi,
}: {
  adaptableApi: AdaptableApi;
}) => Component;
```

- The `VueFrameworkComponent` is **automatically** invoked when its containing element becomes visible
- And it is automatically unmounted when the component is hidden

### Wiring in Adaptable Options

For toolbars, tool panels, and similar extension points, pass a factory on `frameworkComponent` that uses `h()` to mount your component with props (see [Custom Toolbars](https://www.adaptabletools.com/docs/ui-dashboard-custom-toolbars/index.md)):

```ts
import {h} from 'vue';

frameworkComponent: ({adaptableApi}) =>
  h(SlideToggle, {
    onToggleChange: (value: boolean) => {
      /* use adaptableApi */
    },
  }),
```

- For [Column Menu](https://www.adaptabletools.com/docs/ui-column-menu-custom-items/index.md) and [Context Menu](https://www.adaptabletools.com/docs/ui-context-menu-custom-items/index.md) items, supply a factory that returns the component definition: `frameworkComponent: () => ThemeToggleMenuItem`
- AG Grid injects `adaptableApi` and menu context via `menuItemParams` on the component

## Using App State

To share app-level state with custom Vue components (for example a toolbar shown inside and outside the Dashboard), use Vue `provide` / `inject` on the host app that wraps AdapTable.

See [Custom Toolbars](https://www.adaptabletools.com/docs/ui-dashboard-custom-toolbars/index.md) for a full example.

<!-- /framework -->
