lib/volt/reactive/eventable.rb

Summary

Maintainability
A
25 mins
Test Coverage
module Volt
  # Listeners are returned from #on on a class with Eventable included.
  # Listeners can be stopped by calling #remove
  class Listener
    attr_reader :events
    def initialize(klass, events, callback)
      @klass    = klass
      @events    = events
      @callback = callback
    end

    def call(*args)
      @callback.call(*args) unless @removed
    end

    # Call the callback with self set to instance
    def instance_call(instance, *args)
      instance.instance_exec(*args, &@callback)
    end

    def remove
      fail 'Listener has already been removed' if @removed
      @removed = true

      @events.each do |event|
        @klass.remove_listener(event, self)
      end

      # Make things easier on the GC
      @klass    = nil
      @callback = nil
    end

    def inspect
      "<Listener:#{object_id} events=#{@events}>"
    end
  end

  # Include Eventable to add a basic event/trigger system to a class.  Listeners can be
  # added with #on(event_name) { ... }  Events can be triggered with #trigger!
  module Eventable
    # Sets up a listener on the class the Eventable module was included in.
    # event should be a string or symbol.  When something calls #trigger!(event_name) on
    # the class, it will trigger any listener with the same event name.
    #
    # returns: a listener that has a #remove method to stop the listener.
    def on(*events, &callback)
      fail '.on requires an event' if events.size == 0

      listener = Listener.new(self, events, callback)

      @listeners ||= {}

      events.each do |event|
        event             = event.to_sym
        @listeners[event] ||= []
        @listeners[event] << listener

        first_for_event = @listeners[event].size == 1
        first           = first_for_event && @listeners.size == 1

        # Let the included class know that an event was registered. (if it cares)
        if self.respond_to?(:event_added)
          # call event added passing the event, the scope, and a boolean if it
          # is the first time this event has been added.
          event_added(event, first, first_for_event)
        end
      end

      listener
    end

    # Triggers event on the class the module was includeded.  Any .on listeners
    # will have their block called passing in *args.
    def trigger!(event, *args)
      event = event.to_sym

      if @listeners && @listeners[event]
        # TODO: We have to dup here because one trigger might remove another
        @listeners[event].dup.each do |listener|
          # Call the event on each listener
          listener.call(*args)
        end
      end
    end

    # Stops the listener returned by calling .on(:event_name)  Triggers #event_removed
    # if there are no more listeners for that event.
    def remove_listener(event, listener)
      event = event.to_sym

      fail "Unable to delete #{event} from #{inspect}" unless @listeners && @listeners[event]

      @listeners[event].delete(listener)

      last_for_event = @listeners[event].size == 0

      if last_for_event
        # No registered listeners now on this event
        @listeners.delete(event)
      end

      last = last_for_event && @listeners.size == 0

      # Let the class we're included on know that we removed a listener (if it cares)
      if self.respond_to?(:event_removed)
        # Pass in the event and a boolean indicating if it is the last event
        event_removed(event, last, last_for_event)
      end
    end
  end
end