Plugins Guide
When to use
Use plugins when you need reusable side effects around color changes or commits (analytics, telemetry, policy checks, integrations).
Quick example
ts
import { createBerryPickrController, type BerryPickrPlugin } from '@appberry/berrypickr';
const auditPlugin: BerryPickrPlugin = {
name: 'audit-plugin',
onCommit(event) {
console.log('committed', event.transactionId, event.value?.to('hexa'));
}
};
const controller = createBerryPickrController({
plugins: [auditPlugin],
pluginErrorPolicy: 'emit'
});Live Plugin Demos
Lifecycle
`setup` + teardown behavior
Initialize and destroy the controller to verify plugin setup and cleanup flow.
setup count 0
teardown count 0
setup subscription updates 0
Lifecycle log
Telemetry
`onChange` vs `onCommit`
Apply live edits and explicit commits to compare telemetry payloads.
change events 0
commit events 0
Last change none
Last commit none
Telemetry log
Error Policy
`pluginErrorPolicy` behavior
The demo plugin throws on commit so you can compare `emit` and `throw` outcomes.
pluginError events 0
Error policy log
Each demo validates one plugin responsibility:
- Lifecycle demo: confirms
setupand teardown execution and cleanup timing. - Telemetry demo: shows
onChangeandonCommitpayload differences. - Error policy demo: compares
pluginErrorevent emission (emit) against thrown exceptions (throw).
Options/Methods
Plugin contract
ts
interface BerryPickrPlugin {
name: string;
setup?: (context: BerryPickrPluginContext) => void | (() => void);
onChange?: (event: BerryPickrChangeEvent, state: BerryPickrStateSnapshot) => void;
onCommit?: (event: BerryPickrCommitEvent, state: BerryPickrStateSnapshot) => void;
}Lifecycle phases
setup: called once when controller is createdchange: called for everychangeeventcommit: called for everycommiteventteardown: called oncontroller.destroy()for setup cleanup
Error handling (pluginErrorPolicy)
| Policy | Behavior |
|---|---|
'emit' | Catches plugin callback failures and emits pluginError |
'throw' | Rethrows plugin callback failures to caller |
ts
controller.on('pluginError', ({ plugin, phase, error }) => {
console.error(`[${plugin}] failed during ${phase}`, error);
});Robust plugin sample
ts
import type {
BerryPickrPlugin,
BerryPickrChangeEvent,
BerryPickrCommitEvent,
BerryPickrStateSnapshot
} from '@appberry/berrypickr';
interface TelemetrySink {
track(name: string, payload: Record<string, unknown>): void;
}
export const createTelemetryPlugin = (sink: TelemetrySink): BerryPickrPlugin => {
const trackChange = (event: BerryPickrChangeEvent, state: BerryPickrStateSnapshot): void => {
sink.track('berrypickr.change', {
instanceId: event.instanceId,
transactionId: event.transactionId,
source: event.source,
context: event.context,
format: state.format,
value: event.value?.to('hexa') ?? null
});
};
const trackCommit = (event: BerryPickrCommitEvent): void => {
sink.track('berrypickr.commit', {
instanceId: event.instanceId,
transactionId: event.transactionId,
context: event.context,
value: event.value?.to('hexa') ?? null
});
};
return {
name: 'telemetry',
setup() {
sink.track('berrypickr.setup', { at: Date.now() });
return () => sink.track('berrypickr.teardown', { at: Date.now() });
},
onChange: trackChange,
onCommit: trackCommit
};
};Gotchas
- Keep
onChangefast; it is high frequency. - With
'throw', plugin exceptions can break user interaction flows. - Treat teardown as required cleanup for listeners/timers opened in
setup.