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