自定义组件
Web SDK 提供了多种方式来集成自定义组件,使你能够扩展和定制 Web SDK 的功能。本指南将介绍几种常用的方法。
Univer 并不会直接将组件作为参数直接传递给任何渲染函数,你需要通过 Facade API 将组件注册到 Univer 中之后,才能在各个挂载点使用它们。
如果你还不了解如何获取 Facade API 的实例,可以参考 Facade API。
注册自定义组件
univerAPI.registerComponent(componentKey, CustomComponent, options)使用 univerAPI.registerComponent 方法来注册自定义组件。这个方法接受三个参数:
componentKey: 组件的唯一标识符,用于在 Univer 中引用该组件。CustomComponent: 组件的实现,可以是 React、Vue 或 Web Components。options: 可选的配置选项,可用于指定组件所依赖的框架或其他相关设置。
React 组件
注册 React 组件无需额外的配置,只需确保组件是一个有效的 React 组件即可。以下是一个简单的示例:
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 插件:
pnpm add @univerjs/ui-adapter-vue3npm install @univerjs/ui-adapter-vue3yarn add @univerjs/ui-adapter-vue3bun add @univerjs/ui-adapter-vue3import { UniverVue3AdapterPlugin } from '@univerjs/ui-adapter-vue3'univer.registerPlugin(UniverVue3AdapterPlugin)注册 Vue 组件时,需要指定 framework 选项为 'vue3'
const Vue3Component = defineComponent({ setup(props) { return () => <div>Hello Univer!</div> },})univerAPI.registerComponent('MyVue3Component', Vue3Component, { framework: 'vue3',})Vue 2.x
由于 Vue 2.x 已经不再维护,Univer 暂时没有提供 Vue 2.x 的 UI 适配器插件的计划,你可以参考如下代码通过自定义插件来实现一个 Vue 2.x 的适配器:
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 })}然后将其注册:
import { UniverVue2AdapterPlugin } from './ui-adapter-vue2'univer.registerPlugin(UniverUIPlugin)univer.registerPlugin(UniverVue2AdapterPlugin)使用时需要指定 framework 选项为 'vue2'
const Vue2Component = Vue.component('MyVue2Component', { template: '<div>Hello, Univer!</div>',})univerAPI.registerComponent('MyVue2Component', Vue2Component, { framework: 'vue2',})Web Components
注册 Web Components 时,需要确保组件符合 Web Components 标准,并安装和注册 @univerjs/ui-adapter-web-component 的 UniverWebComponentAdapterPlugin 插件:
pnpm add @univerjs/ui-adapter-web-componentnpm install @univerjs/ui-adapter-web-componentyarn add @univerjs/ui-adapter-web-componentbun add @univerjs/ui-adapter-web-componentimport { UniverWebComponentAdapterPlugin } from '@univerjs/ui-adapter-web-component'univer.registerPlugin(UniverWebComponentAdapterPlugin)注册 Web Components 时,需要指定 framework 选项为 'web-component'
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, { 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 导入。
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', })}编辑器渲染完成后,使用注册的组件标识作为侧边栏内容:
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 的功能。
注意事项
- 在使用这些方法时,请确保Univer已经完成渲染。 2. 对于需要注册的组件,请确保在使用前已正确注册。 3. 使用
dispose()方法来清理和移除添加的组件,以避免内存泄漏。
添加自定义菜单项
顶部菜单栏(Ribbon)和右键菜单(Context Menu)都可以添加自定义组件。为菜单项添加自定义组件需要通过创建自定义插件来实现,我们准备了一份新增自定义菜单项的最佳实践来帮助你快速上手。
替换内置组件
警告
替换内置组件可能会导致一些功能无法正常工作,请在充分阅读源码和文档后自行寻找可替换的组件并谨慎操作。
通过 univerAPI.registerComponent 方法注册组件时,如果传入的 componentKey 已经存在,那么 Univer 会将其替换为新的组件。
例如简单地替换内置的 ColorPicker 组件:
// 此代码仅可在 UI 层面替换内置 ColorPicker 组件,无法替代其功能实现univerAPI.registerComponent('UI_COLOR_PICKER_COMPONENT', () => <input type="color" />)作为内容组件添加到……
在侧边栏中使用
使用 univerAPI.openSidebar 方法可以在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.openDialog 方法可以打开一个包含自定义组件的对话框。
// 你应该在合适的时机(比如渲染完成)注册组件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()将弹出框附加到单元格
使用 FRange.attachPopup 方法可以将自定义弹出框附加到特定的单元格范围。
- Popup 是一个单元格上面依附的临时 DOM,一般用于显示一些临时的状态信息,不支持持久化存储。
- 使用
FRange.attachPopup(popup: IFCanvasPopup)方法可以将自定义弹出框附加到特定的单元格范围。 - Popup 会吸附在单元的四边,如果被遮挡,会自动调整位置和方向。
// 你应该在合适的时机(比如渲染完成)注册组件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 方法可以在指定范围的起始单元格附加一个警告弹出框。
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 中引用该组件。- 完整的参数定义
// 你应该在合适的时机(比如渲染完成)注册组件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', },})// 移除浮动 DOMdisposable.dispose()你觉得这篇文档如何?