# カスタムコンポーネント

> Web SDK は、カスタムコンポーネントを統合するための複数の方法を提供しており、その機能を拡張およびカスタマイズできます。このガイドでは、いくつかの一般的な方法を紹介します。

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

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

- Requested language: `ja-JP`

- Content language: `ja-JP`

- Documentation version: `1.1.0`

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

---

Univer は、コンポーネントをレンダリング関数に直接渡すことはありません。さまざまなマウントポイントで使用する前に、Facade API を介してコンポーネントを Univer に登録する必要があります。

Facade API のインスタンスを取得する方法に慣れていない場合は、[Facade API](https://docs.univer.ai/ja-JP/guides/sheets/getting-started/facade.md) を参照してください。

## カスタムコンポーネントの登録

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

`univerAPI.registerComponent` メソッドを使用してカスタムコンポーネントを登録します。このメソッドは 3 つのパラメータを受け取ります：

* `componentKey`: コンポーネントの一意の識別子で、Univer 内で参照するために使用されます。
* `CustomComponent`: コンポーネントの実装で、React、Vue、または Web Component にすることができます。
* `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 は EOL（End of Life）でメンテナンスされなくなったため、Univer は現在 Vue 2.x 用の UI アダプタープラグインを提供していません。ただし、カスタムプラグインを作成することで Vue 2.x アダプターを実装できます。以下は 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'

/**
 * Univer が Vue 2 コンポーネントを UI コンポーネントとして使用できるようにするプラグイン。
 */
@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 ++]
```

Vue 2.x コンポーネントを登録する際は、`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 アダプターに登録します。これにより、Univer のサイドバーやダイアログで Angular コンポーネントを使用できます。

`@angular/core` と同じバージョンの `@angular/elements` と、他の Univer パッケージと同じバージョンの `@univerjs/ui-adapter-web-component` を追加してください。

Univer の初期化後に次の関数を呼び出します。ホストコンポーネントで `private readonly injector = inject(Injector)` により Angular インジェクターを取得し、`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 に柔軟に統合し、その機能を拡張・カスタマイズできます。

> [!WARNING: 注意事項]
> 1. これらのメソッドを使用する前に、Univer のレンダリングが完了していることを確認してください。 2.
>    登録が必要なコンポーネントについては、使用前に正しく登録されていることを確認してください。 3. `dispose()`
>    メソッドを使用して追加されたコンポーネントをクリーンアップし、メモリリークを避けてください。

### カスタムメニューアイテムの追加

トップメニューバー（Ribbon）と右クリックメニュー（Context Menu）の両方にカスタムコンポーネントを含めることができます。メニューアイテムにカスタムコンポーネントを追加するには、カスタムプラグインを作成する必要があります。[カスタムメニューアイテム追加のベストプラクティス](/blog/custom-plugin) を用意して、素早く開始できるようにしています。

### 組み込みコンポーネントの置き換え

> [!ERROR: 警告]
> 組み込みコンポーネントを置き換えると、一部の機能が正常に動作しなくなる可能性があります。コンポーネントの置き換えを試みる前に、ソースコードとドキュメントを十分に読み、慎重に進めてください。

`univerAPI.registerComponent` は既定でオーバーライドを登録します（`override: true`）。プラグインが既定コンポーネントを登録する順序に関係なく、オーバーライドが優先されます。各 `componentKey` で有効にできるオーバーライドは 1 つです。再登録する前に以前の登録を破棄してください。戻り値の `dispose()` を呼ぶと、登録済みの既定コンポーネントに戻ります。`{ override: false }` は既定コンポーネントを登録します。

対応するビューを初めて開く前に登録してください。登録の変更だけでは、マウント済みのビューは再描画されません。

Sheets は次のキーをパッケージの公開エントリーから提供し、対応する組み込みコンポーネントを置き換えられます。

| コンポーネント  | 公開キー                       | パッケージ                                        |
| -------- | -------------------------- | -------------------------------------------- |
| 定義された名前  | `DEFINED_NAME_CONTAINER`   | `@univerjs/sheets-ui`                        |
| その他の関数   | `MORE_FUNCTIONS_COMPONENT` | `@univerjs/sheets-formula-ui`                |
| データの入力規則 | `DATA_VALIDATION_PANEL`    | `@univerjs/sheets-data-validation-ui`        |
| 条件付き書式   | `CF_PANEL_KEY`             | `@univerjs/sheets-conditional-formatting-ui` |

例えば、組み込みの ColorPicker コンポーネントを簡単に置き換えるには：

```tsx
// Following code only replaces the UI of the built-in ColorPicker component, not its functionality
univerAPI.registerComponent('UI_COLOR_PICKER_COMPONENT', () => <input type="color" />)
```

### コンテンツコンポーネントとしての使用

#### サイドバーで

`univerAPI.openSidebar` メソッドを使用して、カスタムコンポーネントを含む Univer インターフェースのサイドバーを開くことができます。

```tsx
// [!code word:MyCustomSidebarComponent]
// 適切なタイミング（例：Univer が読み込まれた後）でコンポーネントを登録する必要があります
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/ja-JP/reference/facade/univer.md#opensidebar)

#### ダイアログで

`univerAPI.openDialog` メソッドを使用して、カスタムコンポーネントを含むダイアログを開くことができます。

```tsx
// [!code word:MyCustomDialogComponent]
// 適切なタイミング（例：Univer が読み込まれた後）でコンポーネントを登録する必要があります
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/ja-JP/reference/facade/univer.md#opendialog)

#### セルにポップアップを添付

`FRange.attachPopup` メソッドを使用して、特定のセル範囲にカスタムポップアップを添付できます。

* ポップアップは、セルに添付される一時的な DOM 要素で、通常は一時的なステータス情報を表示するために使用され、永続的な保存をサポートしません。
* [`FRange.attachPopup(popup: IFCanvasPopup)`](https://docs.univer.ai/ja-JP/reference/facade/range.md#attachpopup) メソッドを使用して、特定のセル範囲にカスタムポップアップを添付できます。
* ポップアップはセルの端に添付され、遮蔽されると自動的に位置と向きを調整します。

```tsx
// [!code word:MyCustomPopupComponent]
// 適切なタイミング（例：Univer が読み込まれた後）でコンポーネントを登録する必要があります
univerAPI.registerComponent(
  'MyCustomPopupComponent',
  () => <div>Hello Univer!</div>,
)

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

// 範囲の最初のセルにポップアップを添付
// `disposable` が null の場合、ポップアップが正常に追加されなかったことを示します
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:MyCustomFloatDOMComponent]
// 適切なタイミング（例：Univer が読み込まれた後）でコンポーネントを登録する必要があります
univerAPI.registerComponent(
  'MyCustomFloatDOMComponent',
  ({ data }) => (
    <div>
      Hello
      {data?.label}
      !
    </div>
  ),
)
const fWorkbook = univerAPI.getActiveWorkbook()
const fWorksheet = fWorkbook.getActiveSheet()

// ワークシートにフローティング DOM を追加
// `disposable` が null の場合、フローティング DOM が正常に追加されなかったことを示します
const disposable = fWorksheet.addFloatDomToPosition({
  componentKey: 'MyCustomFloatDOMComponent',
  initPosition: {
    startX: 100,
    endX: 200,
    startY: 100,
    endY: 200,
  },

  // コンポーネントのデータ
  data: {
    label: 'Univer',
  },
})

// フローティング DOM を削除
disposable.dispose()
```
