clearwater-rb / grand_central

State-management and action-dispatching for Ruby apps
23 stars 3 forks source link

Add callable Dispatcher #2

Closed jgaskins closed 7 years ago

jgaskins commented 8 years ago

I noticed in Clearwater apps I do this a lot:

div([
  button({ onclick: proc { store.dispatch A.new } }, 'A'),
  button({ onclick: proc { store.dispatch B.new } }, 'B'),
  button({ onclick: proc { store.dispatch C.new } }, 'C'),
])

This is a lot of repetition of proc { |e| store.dispatch my_action }. The only thing that differs between them is the action that gets dispatched. Since Clearwater allows any call-able object to handle UI events, I wanted to be able to simplify this to something like this:

div([
  button({ onclick: Dispatch.new(A.new) }, 'A'),
  button({ onclick: Dispatch.new(B.new) }, 'B'),
  button({ onclick: Dispatch.new(C.new) }, 'C'),
])

That way, we don't have to wrap the dispatch in a proc but we still delay it until the event occurs. For events that pass parameters, like input events, we could pass a block instead of an argument to the Dispatch constructor:

input(oninput: Dispatch.new { |e| A.new(e.target.value) }),

Now, as for how Dispatch knows how to direct it at the store we created (since stores are not singletons and we don't track store creation), we would set that up after our store is declared:

store = GrandCentral::Store.new(initial_state) do |state, action|
  # ...
end

Dispatch = GrandCentral::Dispatcher.new(store)

Then all dispatches triggered by Dispatch go to store.

jgaskins commented 8 years ago

Unfortunately, this doesn't help async dispatching. If I want to fetch remote data, I don't know if there's really a way to make this more concise without making it ugly:

button({ onclick: method(:get_data) }, 'Get Data')

def get_data
  store.dispatch(FetchData.new).then do |response|
    store.dispatch LoadData.new(response)
  end
end

I'm very interested in suggestions for this particular use case. If I'm going to dispatch to the store, I don't want to do it two different ways inside the same component. :-\

jgaskins commented 8 years ago

I wonder if something like this might be doable:

Dispatch.async(FetchData.new).then do |response|
  Dispatch.new LoadData.new(response)
end

The async indicates that this is an async action that has a promise. The return value of that call would itself be a Promise that gets resolved/rejected when the given action's promise is resolved/rejected. The thing is, we may not be able to return an actual Promise, but instead a Promise-like object because the return value of the block would need to be called.

jgaskins commented 8 years ago

After playing with this over the weekend, I've determined that not only is the above suggestion really difficult to implement (I still haven't done it successfully), but it would also be weird to put as an inline event handler on an element in a Clearwater app:

button({ onclick: Dispatch.async(FetchData.new).then { |resp| Dispatch.new(LoadData.new(resp) } }, 'Get Data')

You could extract it to a method, but then that's not really different from the onclick: method(:get_data) approach, except you wouldn't pass the method itself, but rather the return value of the method. I'd still like to be able to have first-class support for async dispatching, but it should be simpler and easier.