How to write a CSV import plugin
We will learn how to write a Univer plugin by writing a real case.
By learning this case, you can learn the following:
- How to create a Univer plugin
- How to mount the plugin to the Univer instance
- How to use the plugin’s lifecycle
- How to use the Univer dependency injection system
- How to customize the UI of Univer
- How to access and use the underlying API of Univer
Assuming you already have the following knowledge reserves:
- Basic JavaScript
- Basic TypeScript
Case Introduction
This plugin allows users to import CSV files into Univer tables.
Let’s experience the effect of this plugin first.
Online experience: CSV Import Plugin
Requirement decomposition
The plugin needs to complete the following functions:
- Append a menu button to the toolbar through the Univer API, and define the icon, text, and other properties of the menu button.
- Respond to the click event of the menu button. After clicking the menu button, a file selection box will pop up in the browser, and the CSV file will be selected.
- Convert the CSV content into the data structure of Univer.
- Set the data to the current table cell through the Univer API.
Preparation
1. Create a plugin
You can follow this article to practice together. We will develop based on the Vite initial Demo source code and enter the Playground to start together.
We create the ImportCSVButton.ts
file in the src/plugins
directory, the code is as follows:
import { Plugin, Inject, Injector, Univer } from '@univerjs/core'
/**
* Import CSV Button Plugin
* A simple Plugin example, show how to write a plugin.
*/
class ImportCSVButtonPlugin extends Plugin {
static override pluginName = 'import-csv-plugin';
constructor(
// inject injector, required
@Inject(Injector) override readonly _injector: Injector
) {
super()
}
/** Plugin onStarting lifecycle */
onStarting() {
console.log('onStarting') // todo something
}
}
export default ImportCSVButtonPlugin
The plugin needs to inherit the Plugin
class, which provides the basic functions of the plugin, such as the lifecycle of the plugin, the dependency injection of the plugin, etc.
To define a plugin name, you use the override
keyword. This name serves as an identifier for the plugin and should be unique within an instance.
The plugin’s constructor function injects the Injector
object through the @Inject
decorator, which can be used to obtain other objects of Univer.
If we need to use other objects of Univer, we can use the @Inject
decorator to inject them, which will be explained later.
We output a log in the onStarting
lifecycle of the plugin, which will be executed when the plugin is mounted to the Univer instance. We initialize the internal module of the plugin in this lifecycle.
For more information about the lifecycle of the plugin, you can check Plugin Lifecycle for more information.
2. Mount the plugin to the Univer instance
By querying the API documentation, we can find the Univer.registerPlugin
method, which can mount the plugin to the Univer instance.
We mount the plugin in src/index.ts
, the code is as follows:
import { Univer } from '@univerjs/core'
import ImportCSVButtonPlugin from '../plugins/ImportCSVButton'
// ...omit other code
const univer = new Univer()
// ...omit other code
univer.registerPlugin(csvImportPlugin)
Refresh the page, you can see that the onStarting
log is output, indicating that the plugin has been mounted to the Univer instance and the onStarting
lifecycle has been executed.
The mounting order of plugins depends on the internal dependency relationship of the plugin. If plugin A depends on plugin B, then plugin B must be mounted to the Univer instance before plugin A.
Document:Univer.registerPlugin
Develop the plugin
1. Register the menu button UI
We append the toolbar button in the onStarting
lifecycle of the plugin.
We append the action bar menu button using the IMenuService.addMenuItem
method, which takes an IMenuItem
object as a parameter, which defines the properties of the menu button, such as the icon, text, display position, etc.
We need to define an IMenuItem
object first, the code is as follows:
const menuItem: IMenuItem = {
id: 'import-csv-button', // button id, also used as the click event command id
title: 'Import CSV', // button text
tooltip: 'Import CSV', // tooltip text
icon: 'RenameSingle', // button icon
type: MenuItemType.BUTTON, // button type
positions: [MenuPosition.TOOLBAR_START], // add to toolbar
}
Then, we need to access the IMenuService
instance object, which can be obtained through the @Inject
decorator.
Through the injection of the ID, we can obtain the corresponding object instance from the DI container. The injection ID can be a string constant. For the sake of maintenance, a variable name is often defined to store the injection ID.
In Univer, the injection ID is usually the same as the interface name. For example, the variable name of the injection ID of the class instance object that implements the IMenuService
interface type is also IMenuService
.
We inject the class instance object that implements the IMenuService
interface type into the plugin constructor function, the code is as follows:
import { IMenuService } from '@univerjs/core'
// ...omit other code
class ImportCSVButtonPlugin extends Plugin {
constructor(
// inject injector, required
@Inject(Injector) override readonly _injector: Injector,
// inject menu service, to add toolbar button
@Inject(IMenuService) private menuService: IMenuService,
) {
// ...omit other code
}
// ...omit other code
}
// ...omit other code
Then,we can append the menu button through the IMenuService
instance object in the onStarting
lifecycle of the plugin, the code is as follows:
// ...omit other code
onStarting () {
// ...omit other code
this.menuService.addMenuItem(menuItem);
}
// ...omit other code
Refresh the page, you can see that the toolbar has a menu button, but you can’t click it yet, because we haven’t defined the click event of the menu button.
2. Register a command to respond to the click event of the menu button
In Univer, the click of the menu button in the Univer menu toolbar will trigger a command with the same id
as the menu button. Therefore, we only need to register the same command to respond to the click event of the menu button.
We can register a new command through the ICommandService.registerCommandHandler
method. Similarly, we can obtain the corresponding object instance by injecting the ID of ICommandService
. We add the following code to the plugin constructor function:
import { ICommandService } from "@univerjs/core";
// ...omit other code
constructor (
// ...omit other code
// inject command service, to register command handler
@Inject(ICommandService) readonly commandService: ICommandService
) {
// ...omit other code
}
// ...omit other code
Then, we can register the command handler in the onStarting
lifecycle of the plugin, the code is as follows:
// ...omit other code
onStarting () {
// ...omit other code
const command: ICommand = {
id: "import-csv-button", // command id, same as menu button id
type: CommandType.OPERATION,
handler: (accessor: IAccessor) => {
console.log('click button'); // todo something
return true;
}
}
// register command handler
this.commandService.registerCommand(command);
}
// ...omit other code
ICommand.handler is the event handler function. When the command is triggered, the function will be called.
The parameter accessor
of the event handler function is an IAccessor
object, which can access other objects in the DI container. IAccessor.get
is similar to the Inject
decorator, both are part of the dependency injection system of Univer.
IAccessor
decouples the Command
from other objects in Univer, making the organization of code more flexible and maintainable.
Fresh the page, you can see that after clicking the button, the console outputs the click button
log, indicating that the button click event has been successfully registered.
Reference document:
3. Convert CSV to ICellData
Next, we need to pop up the file selection box in the click event, read the CSV file selected by the user.Because this code does not involve Univer, so it will not be elaborated in this article. You can check the method waitUserSelectCSVFile
source code.
Let’s talk about how to convert the CSV two-dimensional array into the data structure ICellData
of Univer.
ICellData
is the cell data structure in Univer, which contains the value and style of the cell, where the value is stored in the v
attribute, and the style is stored in the s
attribute, the simplified code is as follows:
import type { ICellData } from '@univerjs/core'
// ...omit other code
function parseCSVToUniverData(csv: string[][]): ICellData[][] {
return csv.map((row) => {
return row.map((cell) => {
return {
v: cell || '',
}
})
})
}
// ...omit other code
Reference document:ICellData
4. Set the data to the table
Finally, we need to set the CSV data to the current table, which can be achieved by calling the SetRangeValuesCommand
command through the ICommandService.executeCommand
method.
The vast majority of operations in Univer are registered with commands, providing a unified user experience for developers, and facilitating expansion and maintenance.
In addition, the menu button click event we just defined can also be triggered by other plugins or users through commands.
If you want to learn more about commands, you can check Command System for more information.
We can use this.commandService.executeCommand
to access the instance object of ICommandService
, but for the decoupling of the code and the independence of the Command, we can also use IAccessor.get
to obtain the instance object of ICommandService
.
import { SetRangeValuesCommand } from "@univerjs/sheets";
// ...omit other code
handler: (accessor: IAccessor) => {
// ...omit other code
// get command service
const commandService = accessor.get(ICommandService);
// wait user select csv file
waitUserSelectCSVFile({ csv, rowsCount, colsCount }) => {
// set sheet data
commandService.executeCommand(SetRangeValuesCommand.id, {
range: {
startColumn: 0, // start column index
startRow: 0, // start row index
endColumn: colsCount - 1, // end column index
endRow: rowsCount - 1, // end row index
},
value: parseCSV2UniverData(csv),
});
})
// ...omit other code
return true;
}
// ...omit other code
At this point, we have completed the development of the plugin. Refresh the page, you can see that after clicking the menu button, the file selection box will pop up, and after selecting the CSV file, the content of the CSV file will be displayed in the table.
Summary
The complete code of the plugin can be found inImportCSVButton.ts.
This plugin demonstrates how to extend the UI and functionality of Univer through the Univer plugin system. I hope this article can help you quickly get started with Univer plugin development.
As the scale of plugins increases, it is recommended to further understand the Hierarchical Structure to better understand the plugin system of Univer.
Univer is still in its infancy, if you have any questions or suggestions, please feel free to submit PR or Issue.