Observer Pattern API
API reference for model change observers in the Notations library
The Notations library uses the Observer pattern to notify listeners about model changes. This enables incremental updates to views and efficient change tracking.
Overview
Each observable entity (Group, Role, Line, Block) manages its own list of typed observers. When the model changes, registered observers are notified with strongly-typed callbacks.
Benefits
- Type Safety - Observers have strongly-typed method signatures
- Traceability - Easy to see who is observing what entity
- Explicit Contracts - Clear interface between observables and observers
- Partial Implementation - Observers can implement only the methods they need
Enabling Events
Before observers receive notifications, events must be enabled on the entity:
const group = new Group();
group.enableEvents(); // Enable observer notifications
// Later, you can disable events if needed
group.disableEvents();
// Check if events are enabled
if (group.eventsEnabled) {
console.log("Events are enabled");
}
GroupObserver
Interface for observing atom changes in a Group.
Interface Definition
interface GroupObserver<TAtom = any, TGroup = any> {
/** Called when atoms are added to the end of the group */
onAtomsAdded?(group: TGroup, atoms: TAtom[], index: number): void;
/** Called when atoms are inserted at a specific position */
onAtomsInserted?(group: TGroup, atoms: TAtom[], index: number): void;
/** Called when atoms are removed from the group */
onAtomsRemoved?(group: TGroup, atoms: TAtom[]): void;
}
Usage Example
import { Group, Note, Atom, GroupObserver } from 'notations';
const group = new Group();
group.enableEvents();
// Create an observer
const observer: GroupObserver<Atom, Group> = {
onAtomsAdded: (group, atoms, index) => {
console.log(`${atoms.length} atoms added at index ${index}`);
atoms.forEach(atom => {
if (atom.TYPE === 'Note') {
console.log(` Note: ${(atom as Note).value}`);
}
});
},
onAtomsRemoved: (group, atoms) => {
console.log(`${atoms.length} atoms removed`);
}
};
// Register the observer
const unsubscribe = group.addObserver(observer);
// Make changes - observer will be notified
group.addAtoms(false, new Note("S"), new Note("R"));
// Output: 2 atoms added at index 0
// Note: S
// Note: R
// Unsubscribe when done
unsubscribe();
RoleObserver
Interface for observing atom changes in a Role.
Interface Definition
interface RoleObserver<TAtom = any, TRole = any> {
/** Called when atoms are added to the end of the role */
onAtomsAdded?(role: TRole, atoms: TAtom[], index: number): void;
/** Called when atoms are inserted at a specific position */
onAtomsInserted?(role: TRole, atoms: TAtom[], index: number): void;
/** Called when atoms are removed from the role */
onAtomsRemoved?(role: TRole, atoms: TAtom[]): void;
}
Usage Example
import { Line, Role, Note, Atom, RoleObserver } from 'notations';
const line = new Line();
const role = new Role(line, "swaras");
role.enableEvents();
const observer: RoleObserver<Atom, Role> = {
onAtomsAdded: (role, atoms, index) => {
console.log(`Added to role "${role.name}": ${atoms.length} atoms`);
}
};
role.addObserver(observer);
role.addAtoms(new Note("G"), new Note("M"));
// Output: Added to role "swaras": 2 atoms
LineObserver
Interface for observing role changes on a Line.
Interface Definition
interface LineObserver<TRole = any, TLine = any> {
/** Called when a role is added to the line */
onRoleAdded?(line: TLine, roleName: string, role: TRole): void;
/** Called when a role is removed from the line */
onRoleRemoved?(line: TLine, roleName: string): void;
}
Usage Example
import { Line, Role, LineObserver } from 'notations';
const line = new Line();
line.enableEvents();
const observer: LineObserver<Role, Line> = {
onRoleAdded: (line, roleName, role) => {
console.log(`New role created: ${roleName}`);
// You can now add a RoleObserver to the new role
role.enableEvents();
role.addObserver({
onAtomsAdded: (r, atoms) => {
console.log(` Role ${r.name} got ${atoms.length} new atoms`);
}
});
},
onRoleRemoved: (line, roleName) => {
console.log(`Role removed: ${roleName}`);
}
};
line.addObserver(observer);
// Create a role - observer notified
const role = line.ensureRole("swaras", true);
// Output: New role created: swaras
// Add atoms to the role - nested observer notified
role.addAtoms(new Note("P"), new Note("D"));
// Output: Role swaras got 2 new atoms
BlockObserver
Interface for observing item changes on a Block.
Interface Definition
interface BlockObserver<TItem = any, TBlock = any> {
/** Called when an item is added to the block */
onItemAdded?(block: TBlock, item: TItem, index: number): void;
/** Called when an item is removed from the block */
onItemRemoved?(block: TBlock, item: TItem, index: number): void;
}
Usage Example
import { Block, Line, BlockObserver } from 'notations';
const block = new Block("section", null, "Pallavi");
block.enableEvents();
const observer: BlockObserver = {
onItemAdded: (block, item, index) => {
console.log(`Item added at index ${index}`);
if (item.TYPE === "Line") {
console.log(" A new line was added");
}
}
};
block.addObserver(observer);
// Add a line
block.addBlockItem(new Line());
// Output: Item added at index 0
// A new line was added
Change Types
The library defines enums for categorizing changes:
AtomChangeType
enum AtomChangeType {
ADD = "add", // Atoms added to the end
INSERT = "insert", // Atoms inserted at a position
REMOVE = "remove", // Atoms removed
UPDATE = "update" // Atom properties changed
}
RoleChangeType
enum RoleChangeType {
ADD = "add", // Role added to line
REMOVE = "remove" // Role removed from line
}
BlockItemChangeType
enum BlockItemChangeType {
ADD = "add", // Item added to block
REMOVE = "remove" // Item removed from block
}
Common Patterns
Hierarchical Observation
Watch an entire notation structure by adding observers at each level:
function observeNotation(block: Block) {
block.enableEvents();
// Watch for new lines in the block
block.addObserver({
onItemAdded: (b, item, index) => {
if (item.TYPE === "Line") {
observeLine(item as Line);
}
}
});
// Also observe existing lines
for (const item of block.blockItems) {
if (item.TYPE === "Line") {
observeLine(item as Line);
}
}
}
function observeLine(line: Line) {
line.enableEvents();
// Watch for new roles
line.addObserver({
onRoleAdded: (l, name, role) => {
observeRole(role);
}
});
// Also observe existing roles
for (const role of line.roles) {
observeRole(role);
}
}
function observeRole(role: Role) {
role.enableEvents();
role.addObserver({
onAtomsAdded: (r, atoms, index) => {
console.log(`Role ${r.name}: ${atoms.length} atoms added`);
},
onAtomsRemoved: (r, atoms) => {
console.log(`Role ${r.name}: ${atoms.length} atoms removed`);
}
});
}
DOM Synchronization
Keep a DOM view in sync with model changes:
class RoleView {
private container: HTMLElement;
private role: Role;
constructor(role: Role, container: HTMLElement) {
this.role = role;
this.container = container;
// Enable events and observe
role.enableEvents();
role.addObserver({
onAtomsAdded: (r, atoms, index) => this.renderAtoms(atoms, index),
onAtomsRemoved: (r, atoms) => this.removeAtoms(atoms)
});
// Initial render
this.renderAll();
}
private renderAll() {
this.container.innerHTML = '';
this.role.atoms.forEach((atom, i) => this.renderAtom(atom, i));
}
private renderAtoms(atoms: Atom[], startIndex: number) {
const before = this.container.children[startIndex];
atoms.forEach((atom, i) => {
const el = this.createAtomElement(atom);
if (before) {
this.container.insertBefore(el, before);
} else {
this.container.appendChild(el);
}
});
}
private removeAtoms(atoms: Atom[]) {
// Find and remove DOM elements for these atoms
atoms.forEach(atom => {
const el = this.container.querySelector(`[data-uuid="${atom.uuid}"]`);
if (el) el.remove();
});
}
private createAtomElement(atom: Atom): HTMLElement {
const el = document.createElement('span');
el.className = 'atom';
el.dataset.uuid = String(atom.uuid);
el.textContent = atom.TYPE === 'Note' ? (atom as Note).value : '_';
return el;
}
}
See Also
- API Reference - Complete API documentation
- Model Events Demo - Interactive demo
- Advanced Tutorial - Advanced techniques