Closed jgaskins closed 7 years ago
@ajjahn You've weighed in on a couple of the other dispatching-from-component PRs, was wondering if you might have some feedback here, as well.
Also, that MyActionClass.store = my_store
propagates to all subclasses, as well, so you can define a subclass of GrandCentral::Model
from which all your other actions will derive and they will all automatically use the store set by that class.
This is interesting. I believe out of #2, #6, and #7, I like this the best. I do, however, like how #7 allows you to make objects able to pretty much stand in place of a store object.
While toying with #7, I have been including Store.dispatcher
into a few of my actions and adding a call method to dispatch them. They look something like this:
Clicked = GrandCentral::Action.create do
include MyStore.dispatcher
def call
dispatch(self)
end
end
class MyComponent
include Clearwater::Component
def render
div({ onclick: Clicked.new }, "Click Me")
end
end
It tidies things up, but doesn't help when you need to operate on the event. This solves that problem; I definitely like the form the component takes in your example.
I wonder if there is a reasonable way to provide the event processing in the Dispatcher
's call
method. There will be unique scenarios requiring more flexibility there.
One option is to provide a custom Dispatcher
class:
class DropDispatcher < Dispatcher
def call(event)
event.prevent.stop_propagation
super(event.files)
end
end
Dropped = GrandCentral::Action.create do
store MyStore
dispatcher DropDispatcher
end
I don't love that. Another option would be to give the behavior in a block:
class Dispatcher
def call *args
args = @action_class.dispatch.call(*args) if @action_class.dispatch
@store.dispatch @action_class.new(*@args, *args)
end
end
Dropped = GrandCentral::Action.create do
store MyStore
dispatch do |event|
event.prevent.stop_propagation
event.files
end
end
Actually, strike that last part. After a closer look, I suppose just allowing most raw events as an argument to the action wouldn't be much different than providing it in a custom class or block. Anything beyond the event types (input, submit) you've got in the call method can just be handled in a method in the action.
Yeah, I really like a lot of the stuff in #7 and I've been using it a lot in my components. I like how you used it on the actions themselves to come up with something not too far from this PR. We get around the problem of "how do we let components know about dispatching to the store without giving them access to the state?" by not letting them know the store exists in the first place.
This PR is mostly a proof of concept after playing around with Elm (view functions in Elm don't know about update
, just the messages themselves), but I really like it so far. Another thing I really like about it is that we can use a dispatch callback like the following:
# Side effects
store.on_dispatch do |old, new, action|
case action
when FetchProducts
action.promise
.then(&:json) # Get JSONified response body
.then(&LoadProducts)
.catch(&FetchProductsError)
end
end
I've been experimenting with this pattern to consolidate side effects into a single place for actions that have them and it simplifies things a bit. This doesn't work in MRI unless the action class responds to to_proc
, but it does work in Opal. Even if the Opal team fixes this hack, we can still implement to_proc
with a single line.
I've had enough time to try this for real and I think it works really well, but there are still some areas where this is lacking. For example, it only supports submit
and input
events. This only works well for form submissions and text
/ range
(and similar) inputs. Radio buttons, checkboxes, select boxes, and other similar things (which trigger change
events and have different properties to use) must still be used the old-fashioned way.
Granted, none of this is technically GrandCentral's responsibility to know about since it's UI-related and GrandCentral is about state (and none of it matters when it runs server-side anyway), but I think this has the potential to save a lot of complexity in front-end apps and I want to provide enough support so that people aren't falling back to that complexity in so many cases.
Merged and version 0.4.0 released
Excellent.
Because three PRs open with different ideas to remove knowledge of a store from other things like Clearwater components (#2, #6, and #7) isn't enough, here's a fourth! This PR makes actions dispatchable by sending
call
to the class itself. Consider the following action:Then these two lines become equivalent:
In a Clearwater app, this gets really powerful:
In this component, the action classes themselves handle their own dispatching. Notice we don't need to explicitly prevent the form submission or get the text input value from the
input
event. Both of these are handled for us. That's a little magic, but it does it in a way that keepsgrand_central
from depending on anything outside of itself. It's merely an optimization when it is used in this context.The idea is that you're probably using a single store for all actions you define for your app anyway, so this optimizes that particular setup. If you aren't using that setup, you'll just need to keep doing what you've been doing all along.
"But what about that
AddTodo[new_todo_text]
?", you may be asking. That, my friend, is a partially applied invocation (but in a more OO way — don't judge me). It means that if your action takes some arguments to its initializer that are known ahead of time and others that aren't known until you're ready to dispatch the action, you can specify the ones you do know up front. Since we already know the text of the new todo, we can go ahead and apply that. This is great for the Clearwater use case because the action may be based on something passed into the component, but may also need information from a UI event.