0

I want to be able to dispatch custom events (with properties) from my own javascript objects. I will have many objects so I want them to inherit this events dispatching interface so I dont repeat EventDispatcher code I have assign it as object prototype. Do you think is good approach?

//event dispatcher

(function() {
  var WR_EventDispatcher = function() {

    var self = this;

    self.events = {};

    self.addEventListener = function(name, handler) {
      if (self.events.hasOwnProperty(name))
        self.events[name].push(handler);
      else
        self.events[name] = [handler];
    };

    self.removeEventListener = function(name, handler) {
      if (!self.events.hasOwnProperty(name))
        return;

      var index = self.events[name].indexOf(handler);
      if (index != -1)
        self.events[name].splice(index, 1);
    };

    self.fireEvent = function(name, args) {
      if (!self.events.hasOwnProperty(name))
        return;

      if (!args || !args.length)
        args = [];

      var evs = self.events[name],
        l = evs.length;
      for (var i = 0; i < l; i++) {
        evs[i].apply(null, args);
      }
    };

  };

  window.WR_EventDispatcher = WR_EventDispatcher;
}(window));

//my custom object

(function(window) {
  "use strict"
  var WR_PlaylistManager = function(data) {

    var self = this;

    function test() {
      var k = 6;
      self.fireEvent('NEXT_READY', [{
        'a': 2,
        'b': 11
      }]);
    }
  }

  WR_PlaylistManager.prototype = new WR_EventDispatcher();

  window.WR_PlaylistManager = WR_PlaylistManager;

}(window));


var _WR_PlaylistManager = new WR_PlaylistManager();

_WR_PlaylistManager.addEventListener('NEXT_READY', function(data) {

})
Peter Seliger
  • 11,747
  • 3
  • 28
  • 37
Toniq
  • 4,492
  • 12
  • 50
  • 109

1 Answers1

1

The next provided implementation of an EventTargetMixin (or Observable mixin) is due to partially answer the OP's question of this current thread but also in order to reply to the OP's comment of another, almost equal question of the same OP two-and-a-half weeks later.

  1. Answering the current thread's question

    "I will have many objects so I want them to inherit this events dispatching interface so I dont repeat EventDispatcher code I have assign it as object prototype. Do you think is good approach?"

    The inheritance based approach is going to fail.

    What happens when the OP does WR_PlaylistManager.prototype = new WR_EventDispatcher() is, that from this very point every WR_PlaylistManager instance refers to (shares) the same sole WR_EventDispatcher instance.

    But the OP wants every WR_PlaylistManager instance to be capable of featuring and managing its very own set of event listeners. Thus the OP has to ensure the creation of both an own context and an own scope of what the OP refers to as WR_EventDispatcher.

    Nevertheless exactly this can be accomplished with what the OP already came up with. The WR_EventDispatcher function only has to be applied to every WR_PlaylistManager instance (or any other object type) via e.g. ...

    const wrPlaylistManager = new WR_PlaylistManager();
    WR_EventDispatcher.call(wrPlaylistManager);
    

    Also, what the OP refers to as ... "events dispatching interface" ... is not an interface but is already an implementation which satisfies an interface. Such an approach (its implementation and application / usage) in my opinion should be referred to as function based mixin.

    Thus the OP's WR_EventDispatcher implementation should be renamed to either Observable or EventTarget or withEventTarget or EventTargetMixin.

  2. Answering how to implement a mixin for observable types / targets

    In addition, even though such an implementation looks like a constructor function, it is not and also is discouraged to be used as such. Instead, it always has to be applied to any object like this ... EventTargetMixin.call(anyObject) ... where anyObject afterwards features the methods of an event target like addEventListener, removeEventListener and dispatchEvent, where the OP should do both renaming fireEvent in favor of the latter method's name and also make dispatchEvent capable of dispatching real event types in a fail-/spoof- safe way.

    In order to achieve the just mentioned requirements more easily / readable / maintainable the next provided EventTarget mixin implementation internally uses two other abstractions Event and EventListener where the latter ensures the handleEvent method.

// `EventTargetMixin`-tests with observable-types.


const firstObservableType = {
  foo: 'foo',
  bar: 'bar',
};
EventTargetMixin.call(firstObservableType);

const secondObservableType = EventTargetMixin.call({
  baz: 'baz',
  biz: 'biz',
});

function logEvent(evt) {
  console.log({ evt });
}
const firstListener = firstObservableType
  .addEventListener('woosh', logEvent);

const secondListener = secondObservableType
  .addEventListener('booom', logEvent);


// dispatching tests.


// - dispatching via just a valid string value.

firstObservableType.dispatchEvent('woosh');   // yep.

secondObservableType.dispatchEvent('woosh');  // nope.
secondObservableType.dispatchEvent('booom');  // yep.


// - dispatching via an object featuring a valid `type`.

firstObservableType.dispatchEvent({           // yep.
  type: 'woosh',
});
firstObservableType.dispatchEvent({           // nope.
  type: 'booom',
});

secondObservableType.dispatchEvent({          // yep.
  type: 'booom',
});


// - dispatching via an object and trying
//   to spoof essential event data.

firstObservableType.dispatchEvent({
  id: '________',                   // - spoofing fails.
  type: 'woosh',                    //
  target: null,                     //
  payload: {},                      // - payload data passes.
});
secondObservableType.dispatchEvent({
  id: '########',                   // - spoofing fails.
  type: 'booom',                    //
  target: void 0,                   //
  data: ['foo', 'bar', 'baz'],      // - payload data passes.
  moreData: 'bizz boooz buzz',      //
});


// the above dispatching happes non blocking ...
// ... watch the precedence of all the beneath test's loggings.


console.log({
  firstObservableType,
  secondObservableType,
  firstListener,
  secondListener,
});


// listener tests.

console.log(
  '\nfirstListener.getType() ...',
  firstListener.getType()
);
console.log(
  'firstListener.getHandler() ...',
  firstListener.getHandler()
);

console.log(
  '\nsecondListener.getType() ...',
  secondListener.getType()
);
console.log(
  'secondListener.getHandler() ...',
  secondListener.getHandler()
);


// observable and listener tests.

// - `hasEventListener` tests.

console.log( // yep.
  "\nfirstObservableType.hasEventListener('woosh', logEvent) ?..",
  firstObservableType.hasEventListener('woosh', logEvent)
);
console.log( // yep.
  'firstObservableType.hasEventListener(firstListener) ?..',
  firstObservableType.hasEventListener(firstListener)
);

console.log( // nope.
  '\nsecondObservableType.hasEventListener(firstListener) ?..',
  secondObservableType.hasEventListener(firstListener)
);
console.log( // nope.
  "secondObservableType.hasEventListener('booom', (x => x)) ?..",
  secondObservableType.hasEventListener('booom', (x => x))
);
console.log( // nope.
  "secondObservableType.hasEventListener('foo', logEvent) ?..",
  secondObservableType.hasEventListener('foo', logEvent)
);
console.log( // yep.
  "secondObservableType.hasEventListener('booom', logEvent) ?..",
  secondObservableType.hasEventListener('booom', logEvent)
);

// - `removeEventListener` tests.

console.log( // nope.
  "\nfirstObservableType.removeEventListener(secondListener) ?..",
  firstObservableType.removeEventListener(secondListener)
);
console.log( // yep.
  "firstObservableType.removeEventListener(firstListener) ?..",
  firstObservableType.removeEventListener(firstListener)
);
console.log( // nope.
  "firstObservableType.hasEventListener('woosh', logEvent) ?..",
  firstObservableType.hasEventListener('woosh', logEvent)
);

console.log( // nope.
  "\nsecondObservableType.removeEventListener('foo', logEvent) ?..",
  secondObservableType.removeEventListener('foo', logEvent)
);
console.log( // yep.
  "secondObservableType.removeEventListener('booom', logEvent) ?..",
  secondObservableType.removeEventListener('booom', logEvent)
);
console.log( // nope.
  "secondObservableType.hasEventListener(secondListener) ?..",
  secondObservableType.hasEventListener(secondListener)
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
<script>

// import `EventTargetMixin` from module.
const EventTargetMixin = (function () {

  // implementation / module scope.

  function isString(value) {
    return (/^\[object\s+String\]$/)
      .test(
        Object.prototype.toString.call(value)
      );
  }
  function isFunction(value) {
    return (
      ('function' === typeof value) &&
      ('function' === typeof value.call) &&
      ('function' === typeof value.apply)
    );
  }

  // either `uuid` as of e.g. Robert Kieffer's
  // ... [https://github.com/broofa/node-uuid]
  // or ... Jed Schmidt's [https://gist.github.com/jed/982883]
  function uuid(value){
    return value
      ? (value^Math.random() * 16 >> value / 4).toString(16)
      : ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, uuid);
  }

  function sanitizeObject(value) {
    return (
      ('object' === typeof value) && value
    ) || {};
  }

  class Event {
    constructor({ id = uuid(), type, target, ...data }) {
      Object.assign(this, {
        id,
        type,
        target,
        ...data,
      });
    }
  }

  class EventListener {
    constructor(target, type, handler) {

      const initialEvent = new Event({ target, type });

      function handleEvent(evt) {
        // trying to be non blocking.
        setTimeout(handler, 0, new Event(

          // prevent spoofing of `initialEvent` data.
          Object.assign({}, sanitizeObject(evt), initialEvent)
        ));
      };
      function getHandler() {
        return handler;
      };
      function getType() {
        return type;
      };

      Object.assign(this, {
        handleEvent,
        getHandler,
        getType,
      });
    }
  }

  function EventTargetMixin() {
    const observable/*Target*/ = this;
    const listenersRegistry = new Map;

    function addEventListener(type, handler) {
      let reference;

      if (isString(type) && isFunction(handler)) {
        const listeners = listenersRegistry.get(type) ?? [];

        reference = listeners
          .find(listener => listener.getHandler() === handler);

        if (!reference) {
          reference = new EventListener(observable, type, handler);

          if (listeners.push(reference) === 1) {

            listenersRegistry.set(type, listeners);
          }          
        }
      }
      return reference;
    }

    function removeEventListener(type, handler) {
      let successfully = false;

      const listeners = listenersRegistry.get(type) ?? [];
      const idx = listeners
        .findIndex(listener => listener.getHandler() === handler);

      if (idx >= 0) {
        listeners.splice(idx, 1);

        successfully = true;
      }
      return successfully;
    }

    function dispatchEvent(evt) {
      const type = (
        (evt && ('object' === typeof evt) && isString(evt.type) && evt.type) ||
        (isString(evt) ? evt : null)
      );
      const listeners = listenersRegistry.get(type) ?? [];

      listeners
        .forEach(({ handleEvent }) => handleEvent(evt));

      // success state      
      return (listeners.length >= 1);
    }

    function hasEventListener(type, handler) {
      return !!(
        listenersRegistry.get(type) ?? []
      )
      .find(listener => listener.getHandler() === handler);
    }

    Object.defineProperties(observable, {
      addEventListener: {
        value: addEventListener,
      },
      removeEventListener: {
        value: function (typeOrListener, handler) {
          return (

            isString(typeOrListener) &&
            isFunction(handler) &&
            removeEventListener(typeOrListener, handler)

          ) || (

            (typeOrListener instanceof EventListener) &&
            removeEventListener(typeOrListener.getType(), typeOrListener.getHandler())

          ) || false;
        },
      },
      hasEventListener: {
        value: function (typeOrListener, handler) {
          return (

            isString(typeOrListener) &&
            isFunction(handler) &&
            hasEventListener(typeOrListener, handler)

          ) || (

            (typeOrListener instanceof EventListener) &&
            hasEventListener(typeOrListener.getType(), typeOrListener.getHandler())

          ) || false;
        },
      },
      dispatchEvent: {
        value: dispatchEvent,
      },
    });

    // return observable target/type.
    return observable;
  }

  // module's default export.
  return EventTargetMixin;

}());

</script>
Peter Seliger
  • 11,747
  • 3
  • 28
  • 37