Source: observable/observable.js

/**
 * Observable Pattern Implementations
 *
 * @module observable
 */

export { Observable, ObservableObject, ObservableList };

/**
 * Creates an Observable
 * @param {any} item
 */
const Observable = item => {
  const listeners = [];
  return {
    get: () => item,
    set: newItem => {
      if (item === newItem) return;
      const oldItem = item;
      item = newItem;
      listeners.forEach(notify => notify(newItem, oldItem));
    },
    onChange: callback => {
      listeners.push(callback);
      callback(item, item);
    },
  };
};

/**
 * Creates an object on which each property is observable
 * @param {any} object
 */
const ObservableObject = object => {
  const listeners   = [];
  const subscribers = {};

  const notify = newObject => {
    if (object == newObject) return;
    const oldObject = object;
    object = newObject;

    Object.keys(newObject).forEach(key => {
      const newValue = newObject[key];
      const oldValue = oldObject[key];
      if (oldValue === newValue) return;
      (subscribers[key] || []).forEach(subscriber => subscriber(newValue, oldValue));
    });
    listeners.forEach(listener => listener(newObject, oldObject));
  };

  return {
    get:       ()              => object,
    set:       newObject       => notify({ ...object, ...newObject }),
    push:      (key, value)    => notify({ ...object, ...{ [key]: value } }),
    remove:    key             => notify({ ...object, ...{ [key]: undefined } }),
    replace:   newObject       => {
      const emptyObject = Object.assign({}, object);
      Object.keys(emptyObject).forEach(key => emptyObject[key] = undefined);
      notify({ ...emptyObject, ...newObject});
    },
    onChange:  callback        => { listeners.push(callback); callback(object, object); },
    subscribe: (key, callback) => {
      subscribers[key] = subscribers[key] || [];
      subscribers[key].push(callback);
      callback(object[key], object[key]);
    },
    // unsubscribe, removeOnChange
  };
};

/**
 * Creates an Observable list
 * @param {any[]} list
 */
const ObservableList = list => {
  const addListeners     = [];
  const removeListeners  = [];
  const replaceListeners = [];
  return {
    onAdd:     listener => addListeners    .push(listener),
    onRemove:  listener => removeListeners .push(listener),
    onReplace: listener => replaceListeners.push(listener),
    add: item => {
      list.push(item);
      addListeners.forEach(listener => listener(item));
    },
    remove: item => {
      const i = list.indexOf(item);
      if (i >= 0) {
        list.splice(i, 1);
      } // essentially "remove(item)"
      removeListeners.forEach(listener => listener(item));
    },
    replace: (item, newItem) => {
      const i = list.indexOf(item);
      if (i >= 0) {
        list[i] = newItem;
      }
      replaceListeners.forEach(listener => listener(newItem, item));
    },
    count:   ()    => list.length,
    countIf: pred  => list.reduce((sum, item) => (pred(item) ? sum + 1 : sum), 0),
    indexOf: item  => list.indexOf(item),
    get:     index => list[index],
    getAll:  ()    => list,
  };
};

/* EXPERIMENTS */
const EventManager = () => {
  const subscribers = {};
  return {
    publish:   (name, data)       => (subscribers[name] || []).forEach(subscriber => subscriber(data)),
    subscribe: (name, subscriber) => {
      subscribers[name] = subscribers[name] || [];
      subscribers[name].push(subscriber);
    },
    unsubscribe: (name, subscriber) => {
      subscribers[name] = (subscribers[name] || []).filter(s => s !== subscriber);
    },
  };
};

const Observer = callback => {
  return {
    observe: observable => observable.onChange(callback),
  };
};

const EventObservable = obj => {
  const events = { CHANGED: 0, ADDED: 1, REMOVED: 2, MADE_INVALID: 3 };
  const observers = [];
  return {
    events,
    get: () => obj,
    onChange: observer => observers.push(observer),
    changeTo: (newObj, event = events.CHANGED) => {
      if (obj === newObj) return;
      observers.forEach(notify => notify(newObj, event, obj));
      obj = newObj;
    },
  };
};

const ObservableForm = form => {
  return Object.getOwnPropertyNames(HTMLElement.prototype)
    .filter(p => p.startsWith('on'))
    .reduce((events, event) => {
      events[event] = callback => selector => {
        const elements = selector ? form.querySelectorAll(selector) : form.querySelectorAll('*');
        elements.forEach(element => element.addEventListener(event.substring(2), e => callback(e)));
      };
      return events;
    }, {});
};