onfire icon indicating copy to clipboard operation
onfire copied to clipboard

Event Return values

Open hazah opened this issue 12 years ago • 3 comments

From my current reading, the framework doesn't handle any return value from an event. Often, the signal-slot pattern provides for a method for dealing with possible return values. Either through collecting them, or aggregating them somehow. I believe it would make an invaluable addition to the gem.

hazah avatar May 08 '13 20:05 hazah

Please make an example of how and especially where you would like to use the event return value.

apotonick avatar May 09 '13 01:05 apotonick

This is a snippet from a personal application I'm working on. The following component is responsible for loading the model (and only loading the model).

class UserLoadWidget < Apotomo::Widget
  responds_to_event :load
  responds_to_event :load_multiple

  expose(:users, params: :options)
  expose(:user, params: :options)

  def load evt
    trigger :load_multiple, user: { id: options[:id] }

    evt[:context][user_context] = user
  end

  def load_multiple evt
    options[:user] = evt[:user] unless evt[:user].empty?
  end

private

  def user_context
    options[:user_context] || :user
  end

end

Here I use decent_exposure to convert the options hash into the object (it has a well defined interface, so there isn't a lot of magic there). This works as expected, but notice the second line of the load method? -- this is the return value.

Here's another component that uses the one above:

class UserPageWidget < Apotomo::Widget
  expose(:user) { widget(:user, user: options[:context][:user]) }

  has_widgets do |root|
    options[:context] ||= {}

    root << widget(:user_load, id: options[:id])
    root.respond_to_event :load, on: :user_load

    trigger :load, context: options[:context]

    root << user
  end

  def display
    render
  end

end

I am aware that this is not optimally organized at the moment, but the main idea is to trigger the load event here (and possibly in other, specialized components), and funnel them through a single point, the loader. Naturally, the loader has to return something. As you can see, in this model the page passes in a "context" to the loader to populate with data, which is then being passed along to yet another component specializing in user data display.

Here's that component to complete the picture:

class UserWidget < Apotomo::Widget

  expose(:user) { options[:user] }

  def display mode = :full, language = nil
    render locals: { mode: mode }
  end

end

Bear in mind that I lifted this model from Drupal (my day job). So this whole chain of user_page -> user && user_(load|load_multiple) of components mimicks the flow found there, so that should answer why I've laid things out in this particular way. Basically I plan to make this very dynamic in nature, so isolation/encapsulation is priority.

hazah avatar May 09 '13 15:05 hazah

The basic idea should be fairly simple to implement:

  module ProcessingMethods
      def bubble!
        node = source
        results = []
        # in a real visitor pattern, the visited would call #process_node.
        begin process_node(node, results) end while node = node.parent
        result_processor.call(results)
      end

      def process_node(node, results)
        node.handlers_for_event(self).each do |proc|
          return if stopped?
          results << call_handler(proc, node)
        end
      end

      def call_handler(proc, node)
        proc.call(self)
      end

      def result_processor
         # Logic here to allow for a custom processor or return a default
         return custom_processor if custom_processor
         proc do |results| # The default simply returns the accumulated results.
           results
         end
      end
    end

I grabbed this concept from the signals library of boost. What do you think?

Edit: I would even go as far as abstracting the collection of the results so that you can use different strategies, like allowing to collect information into any arbitrary structure. So instead of:

results << call_handler(proc, node)

I would do:

collector.add(call_handler(proc, node))

With a pluggable object to process the collection:

def collector
  @collector ||= custom_collector || default_collector
end

And in bubble!:

result_processor.call(collector.results)

This is all very rough. But I hope it's clear what I'm getting at.

hazah avatar May 15 '13 14:05 hazah