# Custom Widget Development Guide
This guide provides comprehensive instructions for creating custom widgets in Trilium Notes. Widgets are fundamental UI components that enable you to extend Trilium's functionality with custom interfaces and behaviors.
## Prerequisites
Before developing custom widgets, ensure you have:
- Basic knowledge of TypeScript/JavaScript
- Understanding of jQuery and DOM manipulation
- Familiarity with Trilium's note structure
- A development environment with Trilium running locally
## Understanding Widget Architecture
### Widget Hierarchy
Trilium's widget system follows a hierarchical structure:
```
Component (base class)
└── BasicWidget
├── NoteContextAwareWidget
│ ├── TypeWidget (for note type widgets)
│ └── RightPanelWidget
└── Custom widgets (buttons, containers, etc.)
```
### Core Widget Classes
#### BasicWidget
The foundation class for all widgets. Provides basic rendering, positioning, and visibility management.
```typescript
import BasicWidget from "../widgets/basic_widget.js";
class MyCustomWidget extends BasicWidget {
doRender() {
this.$widget = $('
Hello Widget
');
}
}
```
#### NoteContextAwareWidget
Extends BasicWidget to respond to note changes. Use this when your widget needs to update based on the active note.
```typescript
import NoteContextAwareWidget from "../widgets/note_context_aware_widget.js";
class NoteInfoWidget extends NoteContextAwareWidget {
async refreshWithNote(note) {
if (!note) return;
this.$widget.find('.note-title').text(note.title);
this.$widget.find('.note-type').text(note.type);
}
doRender() {
this.$widget = $(`
`);
}
}
```
#### RightPanelWidget
Specialized widget for rendering panels in the right sidebar with a consistent card layout.
```typescript
import RightPanelWidget from "../widgets/right_panel_widget.js";
class StatisticsWidget extends RightPanelWidget {
get widgetTitle() {
return "Note Statistics";
}
async doRenderBody() {
this.$body.html(`
Words: 0
Characters: 0
`);
}
async refreshWithNote(note) {
const content = await note.getContent();
const wordCount = content.split(/\s+/).length;
const charCount = content.length;
this.$body.find('.word-count span').text(wordCount);
this.$body.find('.char-count span').text(charCount);
}
}
```
## Widget Lifecycle
### Initialization Phase
1. **Constructor**: Set up initial state and child widgets
2. **render()**: Called to create the widget's DOM structure
3. **doRender()**: Override this to create your widget's HTML
### Update Phase
1. **refresh()**: Called when widget needs updating
2. **refreshWithNote()**: Called for NoteContextAwareWidget when note changes
3. **Event handlers**: Respond to various Trilium events
### Cleanup Phase
1. **cleanup()**: Override to clean up resources, event listeners, etc.
2. **remove()**: Removes widget from DOM
## Event Handling
### Subscribing to Events
Widgets can listen to Trilium's event system:
```typescript
class EventAwareWidget extends NoteContextAwareWidget {
constructor() {
super();
// Events are automatically subscribed based on method names
}
// Called when entities are reloaded
async entitiesReloadedEvent({ loadResults }) {
console.log('Entities reloaded');
await this.refresh();
}
// Called when note content changes
async noteContentChangedEvent({ noteId }) {
if (this.noteId === noteId) {
await this.refresh();
}
}
// Called when active context changes
async activeContextChangedEvent({ noteContext }) {
this.noteContext = noteContext;
await this.refresh();
}
}
```
### Common Events
- `noteSwitched`: Active note changed
- `activeContextChanged`: Active tab/context changed
- `entitiesReloaded`: Notes, branches, or attributes reloaded
- `noteContentChanged`: Note content modified
- `noteTypeMimeChanged`: Note type or MIME changed
- `frocaReloaded`: Frontend cache reloaded
## State Management
### Local State
Store widget-specific state in instance properties:
```typescript
class StatefulWidget extends BasicWidget {
constructor() {
super();
this.isExpanded = false;
this.cachedData = null;
}
toggleExpanded() {
this.isExpanded = !this.isExpanded;
this.$widget.toggleClass('expanded', this.isExpanded);
}
}
```
### Persistent State
Use options or attributes for persistent state:
```typescript
class PersistentWidget extends NoteContextAwareWidget {
async saveState(state) {
await server.put('options', {
name: 'widgetState',
value: JSON.stringify(state)
});
}
async loadState() {
const option = await server.get('options/widgetState');
return option ? JSON.parse(option.value) : {};
}
}
```
## Accessing Trilium APIs
### Frontend Services
```typescript
import froca from "../services/froca.js";
import server from "../services/server.js";
import linkService from "../services/link.js";
import toastService from "../services/toast.js";
import dialogService from "../services/dialog.js";
class ApiWidget extends NoteContextAwareWidget {
async doRenderBody() {
// Access notes
const note = await froca.getNote(this.noteId);
// Get attributes
const attributes = note.getAttributes();
// Create links
const $link = await linkService.createLink(note.noteId);
// Show notifications
toastService.showMessage("Widget loaded");
// Open dialogs
const result = await dialogService.confirm("Continue?");
}
}
```
### Server Communication
```typescript
class ServerWidget extends BasicWidget {
async loadData() {
// GET request
const data = await server.get('custom-api/data');
// POST request
const result = await server.post('custom-api/process', {
noteId: this.noteId,
action: 'analyze'
});
// PUT request
await server.put(`notes/${this.noteId}`, {
title: 'Updated Title'
});
}
}
```
## Styling Widgets
### Inline Styles
```typescript
class StyledWidget extends BasicWidget {
doRender() {
this.$widget = $('