-
-
Notifications
You must be signed in to change notification settings - Fork 176
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Setup StimulusReflex controller callbacks #45
Conversation
All tests were done with current master stimulus_reflex_todomvc + cable_ready, dom-events branch of stimulus_reflex.
As currently implemented, I don't see a lot of value over just using the event capture and interrogating the details. They just make the API bigger. The reason I like the onEdit (and you've given me a great idea in the form of beforeEdit, too!) is that the purpose of the callback is explicit and you don't have an ugly control structure. I believe that if we have the information needed to create the reflexStart and reflexComplete callbacks, we also have the information needed to create onEdit and beforeEdit method pairs for all of the events. If you look at my PR, you see that I just assemble the potential function names as strings, test to see if they are defined as functions and if they are, I execute them via the object[] notation. For the price of a few lines of JS in the library, every dev gets to use really amazing syntax. ¯\_(ツ)_/¯ FWIW, I feel like I sound harsh and negative. I'm really excited for this! I just want to get this right, and I am thorough. |
@leastbad The last commit should somewhat address why things weren't working for you. It will at least provide enough context to help developers work around any problems similar to what you encountered. The issue was related to the DOM selector that gets stitched together when an id isn't present. Apparently |
To test the error condition, simply |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall I love this. I think it makes a lot of sense and is pushing this library forward in a great way. Most of my comments could be taken as questions or nitpicks but felt it was worth bringing them up. As far as naming, I think they all make sense. Is there documentation we could update? That stood out to me above everything else.
Co-Authored-By: Andrew Mason <andrewmcodes@protonmail.com>
Co-Authored-By: Andrew Mason <andrewmcodes@protonmail.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like dispatch
much better. Happy to set up a time to pair on tests. Just ping me on Slack.
I finally had a chance to check out the latest and greatest for both stimulus_reflex and stimulus_reflex_todomvc. I watched the video, too. That was some seriously hi-res video! 111MB/26s is wild... when I download a 720p show it's usually 350MB or so for ~23 minutes. I digress! Did you notice some strange artifacts when you clicked the toggle buttons? For some reason, I can't actually get animate.css to work so my build is broken. I'm going from memory and the video, which sucks a bit. Not sure what's up, but my todomvc currently has no styles and there's no ActionCable WS loading... and no errors on bin/webpack or on my Chrome console. Annoying! Looking at the todomvc example, it seems as though most of what you've done is represented in your custom default_controller.js, correct? Let me start by saying that I definitely appreciate seeing some solid examples of the kinds of thing that really does belong in the stimulus-reflex controller, and benefit from the callback system as it is currently proposed/implemented. The best example is almost certainly the way you toggle the wait class on the body element. While hopefully most people never see a spinner because it's just that fast, this is a really clever implementation of a feature that really should be there, especially if you're calling long-running tasks like API calls on the server. The only problem I see here is that, depending on how things are implemented, it's entirely possible that the wait class would be over-written as soon as the user types or clicks on something else while the long-running call is still waiting. I could make a pretty good argument that the spinner is actually something that should be implemented with the template and an instance variable, because otherwise what happens when they refresh or load a second duplicate tab? BTW: the trick you used to place the cursor at the end of the autofocus text element is both a ridiculous hack and by far the most elegant of all the many ridiculous hacks I've seen. Keeping that one around for later... This is where I switch into constructive feedback mode, though. I am sincerely hoping that we can get on the same page about most if not all of the following, especially now that you've put some energy into implementing some basic UI feedback with the current API. First, while I agree that there probably are uses for a global, default controller (and it would appear to impose a 0 LOC cost on the project) I continue to feel strongly that the only correct place for callbacks initiated by actions associated with the todos controller is in the todos_controller.js. If I need to handle autofocus after calling "click->TodosReflex#edit" then I'm going to go look at the todos controller, not some abstract generic class that only exists because we added support for declarative reflexes a few weeks ago. From a code modularity and developer happiness perspective, having all events in your entire application handled in a generic controller reminds me of the scene in Fantasia when a well-intentioned Mickey Mouse starts summoning more and more dancing buckets and everyone just goes to hell on a road paved with good intentions. I would like very much to be able to mix-and-match which controllers I use on different views in my application, without bringing in global event code that gets ever more complex to handle different weird edge cases for individual screens. And that's assuming I'm the only one working on it! Junior dev nightmare flashbacks. Second, if you look at your reflexSuccess() example, you've already got two distinct logic branches which have very little to do with each other; a test for autofocus and a test for checkboxes... and this is for a tiny demo. Not only should every controller be able to handle its own callbacks, but the most important distinction is being lost when we put the primary focus on the class callback Success event. The action that triggered the success is just as important and first-class as the success flag. This is cutting to the heart of what I tried to raise in what I labelled as issue 6 in my previous comment. At full risk of flogging a horse, in my preferred and proposed API, you would instead see: todos_controller.js
You also managed to accidentally get me excited about supporting beforeEdit() and beforeToggle() as well. There should probably be onEditError(), too. If you look at the three supporting functions in your controller (isCheck, listItemTarget, headerTarget) they are each dedicated to either finding the correct element to work on or validating that a path on your reflexSuccess logical tree should be followed. In other words, they are pure ceremony and don't add functionality, just boilerplate. Just to be clear: I'm glad that the 4 class-level events are there and think they are super handy to expose in addition to the action-based events I'm advocating strongly for. If todos initiates a reflex action, it completely makes sense for todos to receive reflexSuccess events pertaining to it that will not be captured by my users, clients or invoices controllers. And by extension, it makes sense that the custom stimulus-reflex override controller could define a reflexError() event handler that picks up any errors for all controllers. Heck, the custom override controller could even define an onEdit() callback that would get fired for all controllers, too. But the hierarchy of information flow is really important when considering building a real-world app with the Reflex/Live View pattern. I gave you a working implementation of the proposed API and if you strip out the redirects and ability to define which callback you want in the reflex action, you don't have to make any changes to the Ruby lib at all. We have the controller and action available in the event.details, we just have to do a bit of split+toLowerCase+append magic to get the exact API I proposed. You can test the controller to see if the callback is defined, and if it is, you run it. Without the added complexity of parsing out redirects and overridden callbacks from the server, the actual implementation would be 6-10 lines. I'd also still like to discuss items 4 and 5, although I recognize that perhaps 4 should be a separate PR that we sort out after callbacks. As for 5, given that my build just won't let me actually click on anything right now, can you address the question of whether these callbacks work for both declarative reflexes as well as explicit calls to stimulate()? |
Pretty sure we share similar opinions regarding how to best architect a StimulusReflex project. Having said that, I'd also stress that StimulusReflex is a lower level utility and I don't feel it should impose a particular style... even though there are "best practices" that we can and should document. This is similar to Rails itself. I don't agree that more specific events like
|
I think that there's been a [awesome] misunderstanding. My PR dynamically looks up and calls those events based entirely on the name of the action. The entire implementation is apparently 8 lines, depending on how you count it. Swap out line 40... we can just take event.details.target.split('#')[1] instead. Similarily, swap out line 32 so the controller reference comes from event.details.target.split('#')[0]. No changes to the Ruby lib required. [I think it's event.details.target, but I don't have a working build right now. I think you know what I mean, though... the string with the "TodosReflex#action" in it.] Anyhow, at no point have I ever wanted or attempted to imply that we should hard-code any action-based event handler into the library. I passionately hope that this triggers a moment of 💡 ⚡ |
Indeed! This is a pretty slick convention. 🤔 Will think on this some more. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks really great so far, Nate. Thrilled to see it moving in this direction.
I'm going to kick the tires in my master branch of todomvc.
javascript/stimulus_reflex.js
Outdated
@@ -95,6 +92,34 @@ const invokeCallback = (name, controller) => { | |||
if (controller && typeof controller[name] === 'function') controller[name](); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This invokeCallback probably should have a different name because it's useful for all sorts of things that aren't callbacks. Can we call it send
as a homage to Ruby?
Is send
a valid function name in JS?
Probably not super important, but technically we could change the name of the controller parameter to class (or klass) because really this is a cool pattern even if you're working in React. I always like the idea that people reading the code can learn new tricks, and this isn't specifically a Stimulus thing.
Of course, it might just be confusing, too. I'm not hung up either way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right that it's something of a misnomer to name it a callback. I'll take some time to consider whether or not there is a better naming convention we can use.
I tried to kick the tires and I'm sad to say that I can't get the callbacks working. I'm sure it's something silly. I'm using the master branch of stimulus_reflex_todomvc, the only changes being that the gem points to my local checkout of your dom-events stimulus_reflex branch. The npm module is also linked to your dom-events branch. I have verified that things are as expected by inserting a console log in the stimulus_reflex.js which is executing. If you take the stock todos_controller.js and add the following below the cancelEdit definition:
None of the callbacks are firing, including editError if I make the reflex class raise a bogus error on the server. I'm clicking on the first todo item in the list, which is definitely firing an edit action based on the server log tail. This should be super vanilla. Any ideas why it doesn't seem to work? |
@leastbad Make sure you do the following during development.
Remember that the only event the todos controller handles (on the master branch of the todos demo) is Note also that args are no longer passed to the callbacks. |
Just getting this on my phone, looking forward to trying later. As I said: I’ve verified that I have my reflex gems and packages properly redirected.... otherwise, i would have had serious issues doing anything previously as well. I honestly wasn’t sure what, if anything, would arrive as an argument; I see now that we’re just calling the functions straight. The only thing that seems off is your comment about how the only event todos handles is cancelEvent... I’m using dom-events branch and defining the callbacks I want for todos actions. What am I missing? In other words, where else would they be? Or put more pointedly: this is the only place that they should be... |
Bear in mind that this PR is still WIP. I have more changes coming tonight. |
Observations about the current builds of todomvc:lifecycle-demo + SR:dom-events. First thing: I realized last night that the reason there's no connect() + register() in todos_controller is that it inherits from ApplicationController. Duh! I put a console log in beforeReflex() in application_controller. I added a beforeReflex with a console log in the todos controller. When I initiate a reflex (edit, cancelEdit or Toggle) I see the log generated by the beforeReflex() in todos but not the one from application_controller. If I then remove the beforeReflex() from todos, the one in application_controller fires. If don't know if this is intended functionality. You were talking about cancelling propagation although I don't see any evidence that you've implemented this in the library or the demo. [I wrote this before I realized the part about extending ApplicationController, above. But this still leads to a solid point.] The problem I anticipate here is this is another example of possible "cross-talk"; someone joins my team and creates a chill controller that defines a beforeReflex() and suddenly the critical code we rely on that lives in application_controller is "broken" even though they didn't change any code. The new guy has no idea that he's created a side-effect. It seems like the beforeReflex and afterReflex callbacks should fire on all controls that define them, including application_controller. Thinking about it, I don't have a strong opinion of whether to run them on the application or individual controllers first. I'm sure that there's a right answer. (I include the reflexSuccess and reflexError callbacks in the above as well.) Addendum: I realize now that the reason beforeReflex() isn't calling twice is that Todos is redefining the definition of the beforeReflex function after inheriting from ApplicationController. However, the larger issue remains: what if some joker creates a controller with a beforeReflex function in it, cancelling the version that's in ApplicationController? I honestly don't know how to fix this beyond putting it in the documentation that if they define a beforeReflex in any one of their controllers, it will clobber the one in ApplicationController, likely pissing someone in the office right off. QQ: will reflexError get access to the error message from a failed reflex on the server? Not so QQ: should you be able to return false from a beforeReflex or beforePoop and have it cancel sending the reflex entirely? Is there a way to inject a time delay via the beforeReflex or beforePoop callback wraps up and the reflex proceeds in sending data to the server? In Ruby we have a blocking sleep call. JS has a non-blocking async setTimeout which won't prevent the callback from returning before the timeout returns. It's actually a really thorny issue but I don't want to overthink. I was/am a little worried that we might need/want a blacklist of methods we don't want to allow reflexes for eg. def send proc lambda method_missing although I'm currently testing an InvoiceReflex#send that seems to work just fine. I guess you can .send(:send) in this crazy world. Anyhow, great news... I have added this to my index.html.erb:
When I click it, only the invoice controller calls its' beforeSend() callback. And I don't see any callbacks for todos being called inside of invoice. I think that you've nailed the problem. Great work. |
OOP is only one pattern people might use. I think it's reasonable to expect that they understand the repercussions and constraints if they choose to use it. When using inheritance, the appropriate way to get the desired behavior would be to call the // app/javascript/controllers/application_controller.js
import { Controller } from 'stimulus';
import StimulusReflex from 'stimulus_reflex';
export default class extends Controller {
connect() {
StimulusReflex.register(this);
}
beforeReflex() {
document.body.classList.add('wait');
}
} // app/javascript/controllers/todos_controller.js
import ApplicationController from './application_controller';
export default class extends ApplicationController {
beforeReflex() {
super.beforeReflex();
// do other things
}
} I think this is something that should be documented rather than adding mechanics to protect the developer from themselves.
I supported this when this PR was dispatching events. I'll update to send the error message into error based lifecycle methods like reflexError(sourceElement, errorMessage) { ...
Possibly... I like this thinking; however, I also want consistency across all lifecycle methods. Not sure what we'd cancel if the others returned false.
I've though about this for handling things like UI animations (fade & hide on delete etc...) with a promise that invokes the reflex when the animation completes. Slowing things down to create a better perceived speed and/or improve the UX. I think we should support it (make it optional), but doubt I'll add it to this PR. |
I'm a little late to the party, but I thought you guys would get a kick out of this: https://elixirschool.com/blog/live-view-with-channels/ Can I just say again that I love the API we came up with? It kind of warms my heart that this is impossible to do in LiveView. |
Enhancement
Inspired by #39
Demo using this feature. hopsoft/stimulus_reflex_todomvc#27
Description
Adds custom controller callbacks. The hope is to support more sophisticated use cases for complex UIs.
beforeReflex
reflexSuccess
reflexError
afterReflex
Checklist