Blog
Post
Anatomy of Filter: How to Write Plugins like a Pro

Anatomy of Filter - How to Write Plugins like a Pro

Univer's Facade API allows developers to call APIs to implement simple custom functionalities. However, if you wish to develop a completely new feature or customize according to specific requirements, understanding Univer's architecture and learning how to write plugins that align with the architecture is essential.

We will use the official filtering plugin as an example to introduce the structure of a Univer plugin. Later, readers can delve into the source code and API Reference to learn how to write a more complex plugin based on this foundation.

You can find the source code for @univerjs/sheets-filter (opens in a new tab) and @univerjs/sheets-filter-ui (opens in a new tab) packages on GitHub.

💡

In the Univer official documentation, there is a tutorial to help readers get started with plugin development by writing a CSV import plugin. It is recommended to complete this tutorial before reading the blog.

💡

Currently, we do not strictly distinguish between npm packages and Univer plugins. When we refer to plugins, it may refer to an npm package or the Plugin class exported by the package.

Plugin Structure

How to Divide Plugins

A complex functionality is often split across multiple npm packages. The filtering feature consists of two npm packages:

@univerjs/sheets-filter

  • Defines the runtime data structure for filtering and the data structure in snapshots.
  • Defines mutations for changing filtering data.
  • Implements the coupling logic for filtering and basic sheet functionalities (such as the linkage between filtering and inserting rows/columns).
  • Provides the SheetsFilterModel for managing filtering models in multiple workbooks and worksheets.
  • Encapsulates the above modules in the UniverSheetsFilterPlugin.

@univerjs/sheets-filter-ui

  • Implements the rendering of the filtering feature, including the filter selection area and filter button.
  • Implements the UI for the filtering feature, including the filter panel and menu bar.
  • Implements other interactions, such as keyboard shortcuts.
  • Provides the SheetsFilterPanelService for managing the filter panel.
  • Encapsulates the above modules in the UniverSheetsFilterUIPlugin.

The reason for splitting into two packages is that the code in @univerjs/sheets-filter-ui relies on the DOM, while the code in @univerjs/sheets-filter is pure logical code that does not depend on the DOM and can run in Node.js and Web Worker environments, suitable for collaborative editing and server-side computation scenarios. By splitting them, we can easily reuse the code from @univerjs/sheets-filter and fully leverage the isomorphic advantages of the Univer architecture.

💡

For more information on how to divide plugins, you can refer to the relevant chapter in the architecture documentation here.

Plugin Directory Structure

All plugins have a similar directory structure.

The directory structure of sheets-filter-ui is as follows:

.
|-- README.md
|-- package.json
|-- src
|   |-- __testing__ // Testing resources
|   |   `-- data.ts
|   |-- commands
|   |   |-- commands // Commands of type CommandType.COMMAND
|   |   `-- operations // Commands of type CommandType.OPERATION
|   |-- controllers
|   |   |-- __tests__ // Test cases
|   |   |-- sheets-filter-permission.controller.ts // Logic related to permissions
|   |   |-- sheets-filter-ui-desktop.controller.ts // Desktop UI
|   |   |-- sheets-filter-ui-mobile.controller.ts // Mobile UI
|   |   |-- sheets-filter.menu.ts // Menu items
|   |   `-- sheets-filter.shortcut.ts // Shortcuts
|   |-- filter-ui-desktop.plugin.ts // Desktop plugin
|   |-- filter-ui-mobile.plugin.ts // Mobile plugin (experimental, not publicly released)
|   |-- index.ts // Package entry file, exports resources from here
|   |-- locale // Internationalization resources
|   |   |-- en-US.ts
|   |   |-- ru-RU.ts
|   |   |-- vi-VN.ts
|   |   |-- zh-CN.ts
|   |   `-- zh-TW.ts
|   |-- models // Data models for the filtering configuration panel
|   |   |-- __tests__
|   |   |-- conditions.ts
|   |   |-- extended-operators.ts
|   |   `-- utils.ts
|   |-- services
|   |   |-- __tests__
|   |   `-- sheets-filter-panel.service.ts // Service managing filtering configuration panel state and form data
|   |-- views
|   |   |-- components // React components
|   |   |-- render-modules // Render modules
|   |   `-- widgets // Components drawn on Canvas
|   |-- vite-env.d.ts
|   `-- worker // Code running in Web Worker
|       |-- generate-filter-values.service.ts // Calculate options in the filter panel
|       `-- plugin.ts
|-- tsconfig.json
|-- tsconfig.node.json
`-- vite.config.ts

The directory structure largely follows Univer's layered model, enabling developers familiar with Univer to quickly get started.

💡

Learn about Univer's layered model.

For comparison, the sheets-filter plugin has a similar directory structure:

.
|-- README.md
|-- docs
|   |-- README.md
|   `-- architecture.excalidraw
|-- package.json
|-- src
|   |-- commands
|   |   `-- mutations // Commands of type CommandType.MUTATION
|   |-- controllers
|   |   `-- sheets-filter.controller.ts // Part coupled with basic spreadsheet functionality, e.g., modifying filter range when inserting rows/columns
|   |-- index.ts
|   |-- models // Filtering data models
|   |   |-- __tests__
|   |   |-- custom-filters.ts // Conditional filters
|   |   |-- filter-model.ts // Runtime filtering data structure
|   |   `-- types.ts // Snapshot data structure
|   |-- plugin.ts
|   |-- services
|   |   `-- sheet-filter.service.ts // Service managing filtering models in multiple workbooks and worksheets
|   |-- utils.ts
|   `-- vite-env.d.ts
|-- tsconfig.json
|-- tsconfig.node.json
`-- vite.config.ts
💡

You can use the official CLI tool (opens in a new tab) to create plugins.

Custom Data

Plugins may require custom data structures, which need to consider these aspects: snapshot data structure, in-memory data structure, and mutations to modify these data structures.

Snapshot Data Structure

The snapshot data structure will be stored in the database, so it must be serializable. The format of the snapshot data for filtering is in types.ts (opens in a new tab). IAutoFilter only utilizes basic data structures, thus it can be easily serialized and deserialized using JSON.

💡

Learn about the concept of snapshots.

💡

First-party plugins of Univer refer to the OOXML specification (opens in a new tab) when defining snapshot data structures to facilitate import and export.

In-Memory Data Structure

The snapshot data structure might not be suitable for direct use in memory (e.g., high time complexity for queries and modifications), hence the need to define an in-memory data structure. The in-memory data structure defined by the filtering plugin is in filter-model.ts (opens in a new tab).

IResourcesManagerService

After defining the two data structures, we need to convert the snapshot data structure to the in-memory data structure, or vice versa, at the appropriate times. This process is achieved through IResourceManagerService. The filtering functionality in sheets-filter.service.ts (opens in a new tab) defines the relevant operations.

💡

Refer to Custom Model for more details.

Mutations

In Univer, modifications to data structures need to be completed using commands of the MUTATION type; otherwise, features like undo redo, collaborative editing, and history tracking cannot be implemented. The filtering mutations are defined in sheets-filter.mutation.ts (opens in a new tab).

💡

Read about the Command System to understand what commands are and the three types of commands.

💡

Read about Extending Commands to learn how to customize commands.

Coupling with the basic functionality of sheets

Due to the modular design of Univer, the coupling logic between functionalities needs to be implemented through various callback mechanisms. The relevant coupling logic for the filtering plugin is located in sheets-filter.controller.ts (opens in a new tab).

These coupling logics include:

  1. Intercepting the execution process of commands through SheetInterceptorService to supplement operations; for example, if the position of inserting rows and columns intersects with the filtering range, the filtering range needs to be updated.
  2. Intercepting row filtering logic through SheetInterceptorService to filter rows based on the filtering results.
  3. Listening to the beforeCommandExecute event of ICommandService to check the moving range when moving cells, and if the range includes the filtering header, then movement is prohibited.

And so on.

🤔

When dealing with the logic of coupling with the basic sheet functionality, it often requires calling relevant methods of SheetInterceptorService, which can be referred to the API documentation and usage of existing plugins.

💡

Some plugins need to extend copy-paste or drag-fill functionalities, and the Extending Commands section explains how to extend these two operations.

Rendering

Univer allows functional plugins to customize rendering, and the rendering module of the filtering plugin is located in sheets-filter.render-controller.ts (opens in a new tab). SheetsFilterRenderController is an IRenderModule that defines the logic for rendering the filter selection and buttons.

💡

Please read Architecture of Rendering Engine to understand how to write rendering layer code.

Developing UI

Menu Items

The menu items of Univer are described by an IMenuItem, and the menu item definition for the filter plugin is located in sheets-filter.menu.ts (opens in a new tab). These menu items are registered on the IMenuService during the initialization of SheetsFilterUIDesktopController.

💡

Please read Extending UI to learn how to customize commands, including mutations.

🎉

Univer's menu items are not bound to any specific UI framework or style implementation, allowing them to be used on different platforms with the same menu item definition. They can appear in various components such as context menus, toolbars, menu bars, and more.

Shortcuts

The shortcut definitions for the filter plugin are located in sheets-filter.shortcut.ts (opens in a new tab). These shortcuts are registered on the IShortcutService during the initialization of SheetsFilterUIDesktopController.

Custom Components

The filter panel is defined in the file SheetsFilterPanel.tsx, which is a React component. SheetsFilterUIDesktopController will register this component in the ComponentManager during initialization. When the user opens the filter panel by clicking the filter button, SheetsFilterUIDesktopController will call the method provided by SheetCanvasPopManagerService to render the panel at the specified position.

💡

React components in Univer can use the useDependency hook to access dependencies.

Plugin Encapsulation

Finally, the Plugin will serve as the encapsulation of the above modules, allowing users to simply register the Plugin to introduce the filtering functionality in the project without worrying about the complexity of the internal plugins.

The plugin will go through four lifecycles: STARTING, READY, RENDERED, and STEADY.

During the STARTING lifecycle, the plugin needs to call the add method of Injector to register the modules it provides on the injection container. The plugin can initialize the modules as needed in any lifecycle. Initializing a module is simple and only requires getting the module from the injection container once:

this._injector.get(SheetsFilterUIDesktopController);
💡

For more information on Univer's lifecycle mechanism, please refer to the documentation on Plugin Lifecycle.

💡

Univer uses the dependency injection pattern to manage dependencies between modules and instantiate modules. For more information on dependency injection, please refer to our documentation.

Internationalization

Plugins need to provide internationalization resources, and the internationalization resources for filtering are located in the locale (opens in a new tab) directory.

Facade API

To further abstract concepts such as services and commands for easier use, a Facade API needs to be provided.

The Facade API for filtering is primarily contained in the file f-filter.ts (opens in a new tab).

Author: Wenzhao Hu (opens in a new tab), Head of Engineering


Copyright © 2021-2024 DreamNum Co,Ltd. All Rights Reserved.