Skip to content

Custom trackers

The easiest way to add a tracker is directly from the plugin settings — no code required.

  1. Open Settings > Postpartum Tracker > Trackers tab.

  2. Scroll to the bottom of the tracker library and click Create custom tracker.

  3. Fill in the form:

    • Name — the display name (e.g., “Vitamin D drops”)
    • Icon — pick from curated emojis or search ~200 emojis by keyword
    • Description — one-line explanation
    • Category — where it appears in the library (baby care, baby development, mother’s recovery, general)
    • Duration — enable if it should have a start/stop timer
  4. Add fields — define custom data fields for each entry:

    • Text — free-form input (e.g., notes, descriptions)
    • Number — numeric value with optional unit (e.g., temperature in F)
    • Select — dropdown with predefined options (e.g., mild/moderate/severe)
    • Boolean — yes/no toggle (e.g., “With food?”)
    • Rating — numeric scale (e.g., 1-5 or 1-10)

    Each field also has these options:

    • Optional — when enabled, the field is skipped on quick-tap and only shown on long-press (hold-for-details). Useful for fields you don’t need every time.
    • Collect on — controls when the field is prompted:
      • log — collected on a single log action (default for non-duration trackers)
      • start — collected when starting a timer
      • stop — collected when stopping a timer
      • always — collected on every action (start, stop, log)
  5. Click Save.

Your custom tracker immediately appears in the library with a [custom] badge. Enable it and it works just like any built-in library tracker — with quick-action buttons, entry list, daily summary, and Todoist integration.

Custom trackers are stored in your plugin settings (data.json) and persist across sessions.

  • Click the pencil icon to edit a custom tracker’s settings.
  • Click the trash icon to delete it (and remove it from the registry).

Building a custom TrackerModule class (advanced)

Section titled “Building a custom TrackerModule class (advanced)”

For trackers that need specialized UI or complex logic beyond what the settings builder can do, you can create a custom TrackerModule class in code.

Only create a custom TrackerModule when you need:

  • Custom UI beyond dynamic form fields
  • Complex state management (e.g., the feeding tracker’s active timer with side tracking)
  • Custom stats computation or visualization
  • Special alert logic that doesn’t fit the interval-based pattern
  • Interaction with other modules’ data

Create a new directory under src/trackers/:

src/trackers/my-module/
MyModule.ts
myModuleStats.ts (optional)
import type { TrackerModule, QuickAction, HealthAlert } from '../BaseTracker';
import type { PostpartumTrackerSettings, TrackerEvent } from '../../types';
interface MyEntry {
id: string;
timestamp: string;
// your fields here
}
interface MyStats {
todayCount: number;
// your stats here
}
export class MyModule implements TrackerModule<MyEntry, MyStats> {
readonly id = 'my-module';
readonly displayName = 'My module';
readonly defaultExpanded = false;
readonly defaultOrder = 50;
private entries: MyEntry[] = [];
private save: (() => Promise<void>) | null = null;
parseEntries(raw: unknown): MyEntry[] {
if (!Array.isArray(raw)) return [];
return raw as MyEntry[];
}
serializeEntries(): MyEntry[] {
return this.entries;
}
emptyEntries(): MyEntry[] {
return [];
}
update(entries: MyEntry[]): void {
this.entries = entries;
}
buildUI(
bodyEl: HTMLElement,
save: () => Promise<void>,
settings: PostpartumTrackerSettings,
emitEvent: (event: TrackerEvent) => void,
): void {
this.save = save;
// Build your UI here using bodyEl
}
getQuickActions(): QuickAction[] {
return [{
id: 'my-module-log',
label: 'Log',
icon: '📋',
cls: 'pt-quick-action',
onClick: (timestamp?: string) => {
// create entry, push to this.entries, call this.save()
},
}];
}
computeStats(entries: MyEntry[], dayStart: Date, dayEnd: Date): MyStats {
const today = entries.filter(e => {
const t = new Date(e.timestamp);
return t >= dayStart && t < dayEnd;
});
return { todayCount: today.length };
}
renderSummary(el: HTMLElement, stats: MyStats): void {
el.createSpan({ text: `${stats.todayCount} today` });
}
// Optional: live timer updates (called every 200ms)
tick(): void {}
// Optional: health alerts
getAlerts(): HealthAlert[] {
return [];
}
}
import { MyModule } from './trackers/my-module/MyModule';
// In onload(), after the built-in module registrations:
this.registry.register(new MyModule());

In src/types.ts, add your module ID to DEFAULT_SETTINGS.enabledModules:

enabledModules: ['feeding', 'diaper', 'medication', 'my-module'],

Or let users enable it via the tracker library in settings.

Use pointerdown/pointerup with preventDefault() + stopImmediatePropagation() for all interactive elements. Regular click events are eaten by CodeMirror 6 in Live Preview mode.

button.addEventListener('pointerdown', (e) => {
e.preventDefault();
e.stopImmediatePropagation();
});
button.addEventListener('pointerup', (e) => {
e.preventDefault();
e.stopImmediatePropagation();
this.handleClick();
});
// Fallback for reading mode:
button.addEventListener('click', () => this.handleClick());

Calling save() rewrites the code block JSON, which causes Obsidian to destroy the current widget and create a new one. Do not hold references to DOM elements across saves.

Always sort entries by timestamp after adding or editing:

this.entries.sort((a, b) =>
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
);

Use the shared InlineEditPanel component for editing entries. It handles datetime-local pickers, CodeMirror event prevention, and mobile-friendly layout.

If your module should trigger Todoist tasks or other integrations, emit events:

emitEvent({
type: 'simple-logged', // or define a new event type
entry: newEntry,
module: this.id,
});

Add your event type to the TrackerEvent.type union in src/types.ts if it doesn’t fit the existing types.