Custom trackers
Creating custom trackers from settings
Section titled “Creating custom trackers from settings”The easiest way to add a tracker is directly from the plugin settings — no code required.
-
Open Settings > Postpartum Tracker > Trackers tab.
-
Scroll to the bottom of the tracker library and click Create custom tracker.
-
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
-
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 timerstop— collected when stopping a timeralways— collected on every action (start, stop, log)
-
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.
Editing and deleting
Section titled “Editing and deleting”- 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 the module
Section titled “Create the module”Create a new directory under src/trackers/:
src/trackers/my-module/ MyModule.ts myModuleStats.ts (optional)Implement TrackerModule
Section titled “Implement TrackerModule”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 []; }}Register in main.ts
Section titled “Register in main.ts”import { MyModule } from './trackers/my-module/MyModule';
// In onload(), after the built-in module registrations:this.registry.register(new MyModule());Add to enabled modules
Section titled “Add to enabled modules”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.
Important patterns
Section titled “Important patterns”Event handling in code blocks
Section titled “Event handling in code blocks”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());Save triggers full re-render
Section titled “Save triggers full re-render”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.
Entry sorting
Section titled “Entry sorting”Always sort entries by timestamp after adding or editing:
this.entries.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());Inline editing
Section titled “Inline editing”Use the shared InlineEditPanel component for editing entries. It handles datetime-local pickers, CodeMirror event prevention, and mobile-friendly layout.
Emitting events
Section titled “Emitting events”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.