# 自定义组件

> Web SDK 提供了多种方式来集成自定义组件，使你能够扩展和定制 Web SDK 的功能。本指南将介绍几种常用的方法。

- Human documentation: [https://docs.univer.ai/zh-CN/guides/sheets/ui/components](https://docs.univer.ai/zh-CN/guides/sheets/ui/components)

- Agent Markdown: [https://docs.univer.ai/zh-CN/guides/sheets/ui/components.md](https://docs.univer.ai/zh-CN/guides/sheets/ui/components.md)

- Requested language: `zh-CN`

- Content language: `zh-CN`

- Documentation version: `1.0.0-rc.0`

- Source: [sheets/ui/components.zh-CN.mdx](https://github.com/dream-num/documentation/blob/dev/content/guides/sheets/ui/components.zh-CN.mdx)

---

Univer 并不会直接将组件作为参数直接传递给任何渲染函数，你需要通过 Facade API 将组件注册到 Univer 中之后，才能在各个挂载点使用它们。

如果你还不了解如何获取 Facade API 的实例，可以参考 [Facade API](https://docs.univer.ai/zh-CN/guides/sheets/getting-started/facade.md)。

## 注册自定义组件

```typescript
// [!code word:componentKey]
// [!code word:CustomComponent]
// [!code word:options]
univerAPI.registerComponent(componentKey, CustomComponent, options)
```

使用 `univerAPI.registerComponent` 方法来注册自定义组件。这个方法接受三个参数：

* `componentKey`: 组件的唯一标识符，用于在 Univer 中引用该组件。
* `CustomComponent`: 组件的实现，可以是 React、Vue 或 Web Components。
* `options`: 可选的配置选项，可用于指定组件所依赖的框架或其他相关设置。

### React 组件

注册 React 组件无需额外的配置，只需确保组件是一个有效的 React 组件即可。以下是一个简单的示例：

```tsx
function ReactComponent(props: Record<string, any>) {
  return <div>Hello Univer!</div>
}

univerAPI.registerComponent('MyReactComponent', ReactComponent)
```

### Vue 组件

#### Vue 3.x

注册 Vue 组件时，需要确保安装并注册 `@univerjs/ui-adapter-vue3` 的 `UniverVue3AdapterPlugin` 插件：

#### npm

```bash
npm install @univerjs/ui-adapter-vue3
```

#### pnpm

```bash
pnpm add @univerjs/ui-adapter-vue3
```

#### yarn

```bash
yarn add @univerjs/ui-adapter-vue3
```

#### bun

```bash
bun add @univerjs/ui-adapter-vue3
```

```typescript
import { UniverVue3AdapterPlugin } from '@univerjs/ui-adapter-vue3'

univer.registerPlugin(UniverVue3AdapterPlugin)
```

注册 Vue 组件时，需要指定 `framework` 选项为 `'vue3'`

```tsx
const Vue3Component = defineComponent({
  setup(props) {
    return () => <div>Hello Univer!</div>
  },
})

univerAPI.registerComponent('MyVue3Component', Vue3Component, {
  // [!code word:vue3]
  framework: 'vue3',
})
```

#### Vue 2.x

由于 Vue 2.x 已经不再维护，Univer 暂时没有提供 Vue 2.x 的 UI 适配器插件的计划，你可以参考如下代码通过自定义插件来实现一个 Vue 2.x 的适配器：

```typescript title="ui-adapter-vue2.ts"
import { DependentOn, Inject, Injector, Plugin } from '@univerjs/core'
import { ComponentManager, UniverUIPlugin } from '@univerjs/ui'
import Vue from 'vue'

/**
 * The plugin that allows Univer to use Vue 2 components as UI components.
 */
@DependentOn(UniverUIPlugin)
export class UniverVue2AdapterPlugin extends Plugin {
  static override pluginName = 'UNIVER_UI_VUE2_ADAPTER_PLUGIN'

  constructor(
    private readonly _config = {},
    @Inject(Injector) protected readonly _injector: Injector,
    @Inject(ComponentManager) protected readonly _componentManager: ComponentManager,
  ) {
    super()
  }

  override onStarting(): void {
    const { createElement, useEffect, useRef } = this._componentManager.reactUtils

    this._componentManager.setHandler('vue2', (component: any) => {
      return (props: Record<string, any>) =>
        createElement(VueComponentWrapper, {
          component,
          props: Object.keys(props).reduce<Record<string, any>>((acc, key) => {
            if (key !== 'key') {
              acc[key] = props[key]
            }
            return acc
          }, {}),
          reactUtils: { createElement, useEffect, useRef },
        })
    })
  }
}

export function VueComponentWrapper(options: {
  component: any
  props: Record<string, any>
  reactUtils: typeof ComponentManager.prototype.reactUtils
}) {
  const { component, props, reactUtils } = options
  const { createElement, useEffect, useRef } = reactUtils

  const domRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    if (!domRef.current) return

    const Constructor = Vue.extend(component)

    const instance = new Constructor({
      data: props,
    })
    instance.$mount()

    domRef.current.appendChild(instance.$el)

    return () => {
      instance.$destroy()
    }
  }, [props])

  return createElement('div', { ref: domRef })
}
```

然后将其注册：

```typescript
import { UniverVue2AdapterPlugin } from './ui-adapter-vue2' // [!code ++]

univer.registerPlugin(UniverUIPlugin)
univer.registerPlugin(UniverVue2AdapterPlugin) // [!code ++]
```

使用时需要指定 `framework` 选项为 `'vue2'`

```tsx
const Vue2Component = Vue.component('MyVue2Component', {
  template: '<div>Hello, Univer!</div>',
})

univerAPI.registerComponent('MyVue2Component', Vue2Component, {
  // [!code word:vue2]
  framework: 'vue2',
})
```

### Web Components

注册 Web Components 时，需要确保组件符合 Web Components 标准，并安装和注册 `@univerjs/ui-adapter-web-component` 的 `UniverWebComponentAdapterPlugin` 插件：

#### npm

```bash
npm install @univerjs/ui-adapter-web-component
```

#### pnpm

```bash
pnpm add @univerjs/ui-adapter-web-component
```

#### yarn

```bash
yarn add @univerjs/ui-adapter-web-component
```

#### bun

```bash
bun add @univerjs/ui-adapter-web-component
```

```typescript
import { UniverWebComponentAdapterPlugin } from '@univerjs/ui-adapter-web-component'

univer.registerPlugin(UniverWebComponentAdapterPlugin)
```

注册 Web Components 时，需要指定 `framework` 选项为 `'web-component'`

```tsx
class WebComponent extends HTMLElement {
  constructor() {
    super()
    const shadow = this.attachShadow({ mode: 'open' })
    const div = document.createElement('div')
    div.textContent = 'Hello Univer!'
    shadow.appendChild(div)
  }
}

univerAPI.registerComponent('my-web-component', WebComponent, {
  // [!code word:web-component]
  framework: 'web-component',
})
```

### Angular

通过 `@angular/elements` 的 `createCustomElement` 将 Angular 组件转换为自定义元素，再使用 Univer 的 Web Component 适配器注册，即可将 Angular 组件用于 Univer 的侧边栏或对话框。

添加与 `@angular/core` 版本一致的 `@angular/elements`，以及与其他 Univer 包版本一致的 `@univerjs/ui-adapter-web-component`。

在初始化 Univer 后调用以下函数。传入宿主组件的 Angular 注入器：在宿主组件中使用 `private readonly injector = inject(Injector)` 获取注入器，再调用 `registerAngularPanel(univer, univerAPI, this.injector)`。宿主组件中的 `inject` 和 `Injector` 从 `@angular/core` 导入。

```typescript title="angular-panel.ts"
import type { Injector } from '@angular/core'
import type { FUniver, Univer } from '@univerjs/presets'
import { Component } from '@angular/core'
import { createCustomElement } from '@angular/elements'
import { UniverWebComponentAdapterPlugin } from '@univerjs/ui-adapter-web-component'

@Component({
  selector: 'app-angular-panel',
  standalone: true,
  template: '<p>Hello from Angular!</p>',
})
export class AngularPanelComponent {}

export function registerAngularPanel(univer: Univer, univerAPI: FUniver, injector: Injector) {
  const element = createCustomElement(AngularPanelComponent, { injector })

  univer.registerPlugin(UniverWebComponentAdapterPlugin)
  return univerAPI.registerComponent('univer-angular-panel', element, {
    framework: 'web-component',
  })
}
```

编辑器渲染完成后，使用注册的组件标识作为侧边栏内容：

```typescript
const sidebar = univerAPI.openSidebar({
  header: { title: 'Angular' },
  children: { label: 'univer-angular-panel' },
  width: 360,
})
```

保存返回的注册句柄和侧边栏句柄，不再使用时调用各自的 `dispose()`。自定义元素在应用中注册一次即可：名称必须包含连字符，并与 Angular 组件的 selector 不同。

Angular Elements 将输入映射为元素属性，将输出转换为 DOM `CustomEvent` 事件。Univer 适配器会传递属性，但不会自动订阅 Angular 输出，需要交互时应显式连接这些事件。

## 使用自定义组件

通过以下方法，你可以灵活地在 Univer 中集成各种自定义组件，从而增强和定制 Univer 的功能。

> [!WARNING: 注意事项]
> 1. 在使用这些方法时，请确保Univer已经完成渲染。 2. 对于需要注册的组件，请确保在使用前已正确注册。 3. 使用 `dispose()`
>    方法来清理和移除添加的组件，以避免内存泄漏。

### 添加自定义菜单项

顶部菜单栏（Ribbon）和右键菜单（Context Menu）都可以添加自定义组件。为菜单项添加自定义组件需要通过创建自定义插件来实现，我们准备了一份[新增自定义菜单项的最佳实践](/blog/custom-plugin)来帮助你快速上手。

### 替换内置组件

> [!ERROR: 警告]
> 替换内置组件可能会导致一些功能无法正常工作，请在充分阅读源码和文档后自行寻找可替换的组件并谨慎操作。

通过 `univerAPI.registerComponent` 方法注册组件时，如果传入的 `componentKey` 已经存在，那么 Univer 会将其替换为新的组件。

例如简单地替换内置的 ColorPicker 组件：

```tsx
// 此代码仅可在 UI 层面替换内置 ColorPicker 组件，无法替代其功能实现
univerAPI.registerComponent('UI_COLOR_PICKER_COMPONENT', () => <input type="color" />)
```

### 作为内容组件添加到……

#### 在侧边栏中使用

使用 `univerAPI.openSidebar` 方法可以在Univer界面中打开一个包含自定义组件的侧边栏。

```tsx
// [!code word:MyCustomSidebarComponent]
// 你应该在合适的时机（比如渲染完成）注册组件
univerAPI.registerComponent('MyCustomSidebarComponent', () => <div>Hello Univer!</div>)

const sidebar = univerAPI.openSidebar({
  header: { title: 'My Sidebar' },
  children: { label: 'MyCustomSidebarComponent' },
  onClose: () => {
    console.log('close')
  },
  width: 360,
})

// 稍后关闭侧边栏
sidebar.dispose()
```

参考： [`univerAPI.openSidebar`](https://docs.univer.ai/zh-CN/reference/facade/univer.md#opensidebar)

#### 在对话框中使用

使用 `univerAPI.openDialog` 方法可以打开一个包含自定义组件的对话框。

```tsx
// [!code word:MyCustomDialogComponent]
// 你应该在合适的时机（比如渲染完成）注册组件
univerAPI.registerComponent('MyCustomDialogComponent', () => <div>Hello Univer!</div>)

const dialog = univerAPI.openDialog({
  id: 'unique-dialog-id', // 对话框的唯一标识符
  draggable: true,
  width: 300,
  title: { title: 'My Dialog' },
  children: {
    label: 'MyCustomDialogComponent',
  },
  destroyOnClose: true,
  preservePositionOnDestroy: true,
  onClose: () => {},
})

// 稍后关闭对话框
dialog.dispose()
```

参考： [`univerAPI.openDialog`](https://docs.univer.ai/zh-CN/reference/facade/univer.md#opendialog)

#### 将弹出框附加到单元格

使用 `FRange.attachPopup` 方法可以将自定义弹出框附加到特定的单元格范围。

* Popup 是一个单元格上面依附的临时 DOM，一般用于显示一些临时的状态信息，不支持持久化存储。
* 使用 [`FRange.attachPopup(popup: IFCanvasPopup)`](https://docs.univer.ai/zh-CN/reference/facade/range.md#attachpopup) 方法可以将自定义弹出框附加到特定的单元格范围。
* Popup 会吸附在单元的四边，如果被遮挡，会自动调整位置和方向。

```tsx
// [!code word:MyCustomPopupComponent]
// 你应该在合适的时机（比如渲染完成）注册组件
univerAPI.registerComponent(
  'MyCustomPopupComponent',
  () => <div>Hello Univer!</div>,
)

const fWorkbook = univerAPI.getActiveWorkbook()
const fWorksheet = fWorkbook.getActiveSheet()
const fRange = fWorksheet.getRange('A1:J10')

// 将弹出窗口附加到范围的第一个单元格
// 如果 `disposable` 为 null，则表示 popup 添加失败
const disposable = fRange.attachPopup({
  componentKey: 'MyCustomPopupComponent',
})

// 移除弹出框
disposable.dispose()
```

#### 附加警告弹出框

使用 `FRange.attachAlertPopup` 方法可以在指定范围的起始单元格附加一个警告弹出框。

```typescript
const fWorkbook = univerAPI.getActiveWorkbook()
const fWorksheet = fWorkbook.getActiveSheet()
const fRange = fWorksheet.getRange('A1:B2')

const disposable = fRange.attachAlertPopup({
  key: 'unique-alert-key', // 唯一标识符
  title: '这是一个警告！',
  message: '这是一个警告！',
  width: 300,
  height: 200,
  // 0: 信息
  // 1: 警告
  // 2: 错误
  type: 0,
})

// 稍后移除警告
disposable.dispose()
```

#### 添加浮动 DOM 到工作表

通过 `FWorksheet.addFloatDomToPosition` 方法可以在工作表上添加一个浮动 DOM。

* 使用该方法前需要安装 `@univerjs/sheets-drawing-ui` 插件或 `@univerjs/preset-sheets-drawing` 预设。
* 浮动 DOM 是悬浮在工作表上的可拖动组件，同时支持持久化存储。
* 需要在 Univer 渲染完成之后调用。
* `componentKey`: 组件的唯一标识符，用于在 Univer 中引用该组件。
* [完整的参数定义](https://github.com/dream-num/univer/blob/dev/packages/sheets-drawing-ui/src/facade/f-worksheet.ts#L32)

```tsx
// [!code word:MyCustomFloatingDOMComponent]
// 你应该在合适的时机（比如渲染完成）注册组件
univerAPI.registerComponent(
  'MyCustomFloatingDOMComponent',
  ({ data }) => (
    <div>
      Hello
      {data?.label}
      !
    </div>
  ),
)
const fWorkbook = univerAPI.getActiveWorkbook()
const fWorksheet = fWorkbook.getActiveSheet()

// 添加一个浮动 DOM
// 如果 `disposable` 为 null，则表示浮动 DOM 添加失败
const disposable = fWorksheet.addFloatDomToPosition({
  componentKey: 'MyCustomFloatingDOMComponent',
  initPosition: {
    startX: 100,
    endX: 200,
    startY: 100,
    endY: 200,
  },

  // 这是组件的数据
  data: {
    label: 'Univer',
  },
})

// 移除浮动 DOM
disposable.dispose()
```
