Разработка кастомных виджетов Appsmith
Appsmith — open-source конкурент Retool с аналогичной моделью кастомизации. Custom Widget в Appsmith — это тоже iframe с postMessage, но API немного отличается. Если Retool использует npm-пакет с хуками, Appsmith предоставляет глобальный объект appsmith прямо в iframe-контексте.
Механизм Custom Widget
Кастомный виджет в Appsmith получает данные через appsmith.model, отправляет события через appsmith.triggerEvent и обновляет состояние через appsmith.updateModel. Всё это синхронизируется с основным приложением через postMessage без необходимости устанавливать SDK.
Виджет подключается как HTML-страница — можно писать vanilla JS прямо в редакторе Appsmith или задать внешний URL с собранным бандлом.
Встроенный редактор vs внешний бандл
Для простых компонентов — код прямо в Appsmith:
<!-- Inline HTML в редакторе Custom Widget -->
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
</head>
<body>
<canvas id="chart"></canvas>
<script>
let chart = null;
function initChart(data) {
const ctx = document.getElementById('chart').getContext('2d');
chart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: data.map(d => d.label),
datasets: [{
data: data.map(d => d.value),
backgroundColor: data.map(d => d.color),
}],
},
options: {
responsive: true,
onClick: (event, elements) => {
if (elements.length > 0) {
const idx = elements[0].index;
appsmith.triggerEvent('onSegmentClick', { item: data[idx] });
}
},
},
});
}
// Инициализация при загрузке
appsmith.onReady(() => {
initChart(appsmith.model.items || []);
});
// Реакция на изменение данных
appsmith.onModelChange((model) => {
if (chart) {
chart.data.labels = model.items.map(d => d.label);
chart.data.datasets[0].data = model.items.map(d => d.value);
chart.update();
}
});
</script>
</body>
</html>
Для сложных компонентов с React и TypeScript — внешний бандл:
// src/Widget.tsx
import { useEffect, useState } from 'react';
declare global {
interface Window {
appsmith: {
model: Record<string, unknown>;
onReady: (cb: () => void) => void;
onModelChange: (cb: (model: Record<string, unknown>) => void) => void;
triggerEvent: (name: string, payload?: unknown) => void;
updateModel: (updates: Record<string, unknown>) => void;
};
}
}
interface ScheduleItem {
id: string;
title: string;
start: string;
end: string;
resourceId: string;
}
export function SchedulerWidget() {
const [items, setItems] = useState<ScheduleItem[]>([]);
const [resources, setResources] = useState([]);
useEffect(() => {
window.appsmith.onReady(() => {
const model = window.appsmith.model;
setItems((model.items as ScheduleItem[]) || []);
setResources((model.resources as []) || []);
});
window.appsmith.onModelChange((model) => {
setItems((model.items as ScheduleItem[]) || []);
setResources((model.resources as []) || []);
});
}, []);
const handleEventDrop = (item: ScheduleItem, newStart: string, newEnd: string) => {
const updated = { ...item, start: newStart, end: newEnd };
window.appsmith.triggerEvent('onEventReschedule', { item: updated });
window.appsmith.updateModel({ lastAction: { type: 'reschedule', item: updated } });
};
return (
<FullCalendarWrapper
items={items}
resources={resources}
onEventDrop={handleEventDrop}
/>
);
}
Сборка для внешнего деплоя
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
rollupOptions: {
input: 'index.html',
},
},
base: './',
});
index.html в dist деплоится на Vercel/Netlify/S3 с публичным доступом. В Appsmith Custom Widget указывается URL index.html.
Передача событий и двусторонняя синхронизация
// Обновление состояния виджета из Appsmith-кода (через Model binding)
// В поле "Default Model" в настройках виджета:
{
"items": "{{fetchData.data}}",
"selectedId": "{{appState.selectedItem}}"
}
// В виджете — реакция на смену selectedId
appsmith.onModelChange((model) => {
highlightItem(model.selectedId);
});
// Из виджета — обновить глобальное состояние Appsmith
appsmith.updateModel({ selectedId: clickedItem.id });
// Это значение доступно как Widget.model.selectedId в других запросах и виджетах
Разница с Retool в разработке
В Retool можно использовать npm-пакеты через bundler seamlessly. В Appsmith inline-редактор не имеет npm — только CDN. Для серьёзных компонентов всегда нужен внешний бандл. Зато Appsmith полностью open-source — можно self-hosted без лицензионных ограничений, и кастомный виджет будет работать идентично на cloud и self-hosted.
Типичные задачи
Самые частые запросы: Gantt-планировщик ресурсов (FullCalendar + custom resource view), тепловые карты активности (D3 или ECharts heatmap), inline-редактор сложных JSON-конфигов (Monaco Editor), геоаналитика (Mapbox GL с фильтрами из Appsmith).
Сроки
Простой компонент на Chart.js или аналоге с передачей данных и одним событием — 1–2 дня. Компонент с двусторонней синхронизацией состояния, кастомной стилизацией и сложной интерактивностью — 3–5 дней. Тяжёлый планировщик или data grid с персистентностью — 1–2 недели.







