docs/docs/en/runjs/context/import-async.md
Dynamically load ESM modules or CSS via URL, applicable to various RunJS scenarios. Use ctx.importAsync() when third-party ESM libraries are required, and ctx.requireAsync() for UMD/AMD libraries. Passing a .css address will load and inject the styles into the page.
| Scenario | Description |
|---|---|
| JSBlock | Dynamically load ESM libraries such as Vue, ECharts, or Tabulator to implement custom charts, tables, dashboards, etc. |
| JSField / JSItem / JSColumn | Load lightweight ESM utility libraries (e.g., dayjs plugins) to assist in rendering. |
| Workflow / Action Events | Load dependencies on demand before executing business logic. |
importAsync<T = any>(url: string): Promise<T>;
| Parameter | Type | Description |
|---|---|---|
url | string | The address of the ESM module or CSS. Supports shorthand <package>@<version> or subpaths <package>@<version>/<file-path> (e.g., [email protected], dayjs@1/plugin/relativeTime.js), which will be concatenated with the CDN prefix according to the configuration. Full URLs are also supported. When a .css file is passed, it will be loaded and injected as a style. For libraries depending on React, you can append [email protected],[email protected] to ensure they share the same React instance with the page. |
.css URL to load and inject it into the page).[email protected] actually requests https://esm.sh/[email protected].@dnd-kit/core, react-big-calendar) should append [email protected],[email protected] to avoid conflicts with the page's React instance, which could lead to "Invalid hook call" errors.https://esm.sh)./+esm for jsDelivr).ctx.importAsync() is preferred.Demonstrates the most basic usage of dynamically loading ESM modules and CSS by package name or full URL.
const Vue = await ctx.importAsync('[email protected]');
// Equivalent to loading from https://esm.sh/[email protected]
const relativeTime = await ctx.importAsync('dayjs@1/plugin/relativeTime.js');
// With subpath (e.g., dayjs plugin)
const pkg = await ctx.importAsync('https://cdn.example.com/my-module.js');
// Full URL
await ctx.importAsync('https://cdn.example.com/theme.css');
// Load CSS and inject into the page
Use ECharts to draw a sales overview chart with bar and line graphs.
// 1. Dynamically load ECharts module
const echarts = await ctx.importAsync('[email protected]');
// 2. Create chart container and render
const chartEl = document.createElement('div');
chartEl.style.width = '100%';
chartEl.style.height = '400px';
ctx.render(chartEl);
// 3. Initialize ECharts instance
const chart = echarts.init(chartEl);
// 4. Configure chart
const option = {
title: {
text: 'Sales Overview',
left: 'center',
},
tooltip: {
trigger: 'axis',
},
legend: {
data: ['Sales', 'Profit'],
top: '10%',
},
xAxis: {
type: 'category',
data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
},
yAxis: {
type: 'value',
},
series: [
{
name: 'Sales',
type: 'bar',
data: [120, 200, 150, 80, 70, 110],
},
{
name: 'Profit',
type: 'line',
data: [20, 40, 30, 15, 12, 25],
},
],
};
// 5. Set option and render chart
chart.setOption(option);
// 6. Optional: Responsive sizing
window.addEventListener('resize', () => {
chart.resize();
});
// 7. Optional: Event listener
chart.on('click', (params) => {
ctx.message.info(`Clicked ${params.seriesName} on ${params.name}, value: ${params.value}`);
});
Demonstrates rendering a data table with pagination and row click events in a block using Tabulator.
// 1. Load Tabulator styles
await ctx.importAsync('[email protected]/dist/css/tabulator.min.css');
// 2. Dynamically load Tabulator module
const { TabulatorFull } = await ctx.importAsync('[email protected]');
// 3. Create table container and render
const tableEl = document.createElement('div');
ctx.render(tableEl);
// 4. Initialize Tabulator table
const table = new TabulatorFull(tableEl, {
data: [
{ id: 1, name: 'Alice', age: 25, city: 'New York' },
{ id: 2, name: 'Bob', age: 30, city: 'London' },
{ id: 3, name: 'Charlie', age: 28, city: 'Paris' },
],
columns: [
{ title: 'ID', field: 'id', width: 80 },
{ title: 'Name', field: 'name', width: 150 },
{ title: 'Age', field: 'age', width: 100 },
{ title: 'City', field: 'city', width: 150 },
],
layout: 'fitColumns',
pagination: true,
paginationSize: 10,
});
// 5. Optional: Event listener
table.on('rowClick', (e, row) => {
const rowData = row.getData();
ctx.message.info(`Row clicked: ${rowData.name}`);
});
Shows how to load FullCalendar and its plugins via ESM and render a basic monthly view calendar.
// 1. Dynamically load FullCalendar core module
const { Calendar } = await ctx.importAsync('@fullcalendar/[email protected]');
// 2. Dynamically load dayGrid plugin
const dayGridPlugin = await ctx.importAsync('@fullcalendar/[email protected]');
// 3. Create calendar container and render
const calendarEl = document.createElement('div');
calendarEl.id = 'calendar';
ctx.render(calendarEl);
// 4. Initialize and render calendar
const calendar = new Calendar(calendarEl, {
plugins: [dayGridPlugin.default || dayGridPlugin],
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth',
},
});
calendar.render();
Uses @dnd-kit/core to implement a minimal example of dragging a Box to a target area within a block.
// 1. Load React, react-dom, @dnd-kit/core (?deps ensures same React instance to avoid Invalid hook call)
const React = await ctx.importAsync('[email protected]');
const { createRoot } = await ctx.importAsync('[email protected]/client');
const core = await ctx.importAsync('@dnd-kit/[email protected][email protected],[email protected]');
const { DndContext, closestCenter, PointerSensor, useSensor, useSensors, useDraggable, useDroppable } = core;
function DraggableBox() {
const { attributes, listeners, setNodeRef, transform } = useDraggable({ id: 'box' });
const style = {
padding: 12,
marginBottom: 8,
background: '#e6f7ff',
cursor: 'grab',
transform: transform ? 'translate3d(' + transform.x + 'px,' + transform.y + 'px,0)' : undefined,
};
return React.createElement('div', { ref: setNodeRef, style, ...attributes, ...listeners }, 'Drag me');
}
function DropZone() {
const { setNodeRef, isOver } = useDroppable({ id: 'zone' });
return React.createElement(
'div',
{
ref: setNodeRef,
style: { padding: 24, background: isOver ? '#b7eb8f' : '#f5f5f5', borderRadius: 8, minHeight: 80 },
},
'Drop here',
);
}
function App() {
const sensors = useSensors(useSensor(PointerSensor));
function onDragEnd(e) {
if (e.over && e.over.id === 'zone') ctx.message.success('Dropped in zone');
}
return React.createElement(
DndContext,
{ sensors, collisionDetection: closestCenter, onDragEnd },
React.createElement(
'div',
{ style: { maxWidth: 280 } },
React.createElement(DraggableBox),
React.createElement(DropZone),
),
);
}
// 2. Render
ctx.render(<App />);
This example relies only on @dnd-kit/core to trigger a notification when a Box is dropped into a specific area, demonstrating the simplest drag-and-drop interaction combining ctx.importAsync and React in RunJS.
Implements a vertical sortable list using dnd-kit's core, sortable, and utilities.
// 1. Load React and dnd-kit related packages (?deps ensures same React instance)
const React = await ctx.importAsync('[email protected]');
const { createRoot } = await ctx.importAsync('[email protected]/client');
const dndCore = await ctx.importAsync('@dnd-kit/[email protected][email protected],[email protected]');
const dndSortable = await ctx.importAsync('@dnd-kit/[email protected][email protected],[email protected]');
const dndUtils = await ctx.importAsync('@dnd-kit/[email protected]');
const { useState } = React;
const { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors } = dndCore;
const {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
useSortable,
} = dndSortable;
const { CSS } = dndUtils;
// 2. SortableItem component (must be inside SortableContext)
function SortableItem(props) {
const { id, label } = props;
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
padding: '12px 16px',
marginBottom: 8,
background: '#f5f5f5',
borderRadius: 6,
cursor: 'grab',
};
return React.createElement('div', { ref: setNodeRef, style, ...attributes, ...listeners }, label);
}
// 3. App: DndContext + SortableContext + Drag end handler
const labels = { 1: 'First', 2: 'Second', 3: 'Third', 4: 'Fourth' };
function App() {
const [items, setItems] = useState([1, 2, 3, 4]);
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
);
function handleDragEnd(event) {
const { active, over } = event;
if (over && active.id !== over.id) {
setItems((prev) => {
const oldIndex = prev.indexOf(active.id);
const newIndex = prev.indexOf(over.id);
return arrayMove(prev, oldIndex, newIndex);
});
ctx.message.success('List reordered');
}
}
return React.createElement(
DndContext,
{
sensors,
collisionDetection: closestCenter,
onDragEnd: handleDragEnd,
},
React.createElement(
SortableContext,
{ items, strategy: verticalListSortingStrategy },
React.createElement(
'div',
{ style: { maxWidth: 320 } },
items.map((id) => React.createElement(SortableItem, { key: id, id, label: labels[id] })),
),
),
);
}
// 4. Create container and mount React
const rootEl = document.createElement('div');
ctx.render(rootEl);
createRoot(rootEl).render(React.createElement(App));
This example uses @dnd-kit/core, @dnd-kit/sortable, and @dnd-kit/utilities to implement a sortable list that updates its order and displays a "List reordered" message after dragging.
Renders a calendar component supporting event display in the current block using react-big-calendar and date-fns for localization.
// 1. Load styles (ctx.importAsync uses ctx.loadCSS for .css files)
await ctx.importAsync('[email protected]/lib/css/react-big-calendar.css');
// 2. Load React, react-dom, react-big-calendar, date-fns, and locale (ensuring same React instance)
const React = await ctx.importAsync('[email protected]');
const { Calendar, dateFnsLocalizer } = await ctx.importAsync('[email protected][email protected],[email protected]');
const { format, parse, startOfWeek, getDay } = await ctx.importAsync('[email protected]');
const enUS = await ctx.importAsync('[email protected]/locale/en-US.js');
const localizer = dateFnsLocalizer({
format,
parse,
startOfWeek,
getDay,
locales: { 'en-US': enUS },
});
const events = [
{ title: 'All Day Event', start: new Date(2026, 0, 28), end: new Date(2026, 0, 28), allDay: true },
{ title: 'Meeting', start: new Date(2026, 0, 29, 10, 0), end: new Date(2026, 0, 29, 11, 0) },
];
// 3. Render React Calendar
ctx.render(
<Calendar
localizer={localizer}
events={events}
startAccessor="start"
endAccessor="end"
style={{ height: '80vh' }}
/>
);
Uses frappe-gantt to render a Gantt chart view showing task start/end times and progress.
// 1. Dynamically load Gantt styles and constructor
// Depends on ESM_CDN_BASE_URL (default https://esm.sh), shorthand paths can be used
await ctx.importAsync('[email protected]/dist/frappe-gantt.css');
const Gantt = await ctx.importAsync('[email protected]');
// 2. Prepare task data
let tasks = [
{
id: '1',
name: 'Redesign website',
start: '2016-12-28',
end: '2016-12-31',
progress: 20,
},
{
id: '2',
name: 'Develop new feature',
start: '2017-01-01',
end: '2017-01-05',
progress: 40,
dependencies: '1',
},
{
id: '3',
name: 'QA & testing',
start: '2017-01-06',
end: '2017-01-10',
progress: 10,
dependencies: '2',
},
];
// 3. Create container and render
const ganttEl = document.createElement('div');
ganttEl.id = 'gantt';
ganttEl.style.height = '400px';
ctx.render(ganttEl);
// 4. Initialize Gantt chart
let gantt = new Gantt('#gantt', tasks, {
view_mode: 'Day', // View granularity: 'Quarter Day' | 'Half Day' | 'Day' | 'Week' | 'Month'
language: 'en',
bar_height: 24,
padding: 18,
custom_popup_html(task) {
return `
<div class="details-container">
<h5>${task.name}</h5>
<p>Start: ${task._start.toISOString().slice(0, 10)}</p>
<p>End: ${task._end.toISOString().slice(0, 10)}</p>
<p>Progress: ${task.progress}%</p>
</div>
`;
},
});
Utilizes @asseinfo/react-kanban to render a basic Kanban board with columns like Backlog and Doing within a block.
// 1. Load styles (ctx.importAsync directly loads .css)
await ctx.importAsync('@asseinfo/[email protected]/dist/styles.css');
// 2. Load React, react-dom, @asseinfo/react-kanban (?deps ensures same React instance)
const React = await ctx.importAsync('[email protected]');
const { default: Board } = await ctx.importAsync('@asseinfo/[email protected][email protected],[email protected]');
const board = {
columns: [
{
id: 1,
title: 'Backlog',
cards: [
{ id: 1, title: 'Add card', description: 'Add capability to add a card in a column' },
],
},
{
id: 2,
title: 'Doing',
cards: [
{ id: 2, title: 'Drag-n-drop support', description: 'Move a card between the columns' },
],
},
],
};
// 4. Mount the board
ctx.render(<Board initialBoard={board} />);
ctx.importAsync() for better module semantics.[email protected],[email protected]. The version must match the React version used by the page, otherwise, an "Invalid hook call" error may occur.