One typical example is a component that collects (and displays) education history provided by the user. I'll list out just a few key UI behavior:
* A "read" view which represents a list of containers with titles that show each instance of a users answer (for example, if you provided 5 sets of answers, such as 5 universities you attended, we need to show those 5 in some sort of preview form on the page)
* A button that when clicked opens up a modal which contains multiple "steps" - each step is a small set of form inputs that range from simple ones like date to more complex ones involving search / autocomplete or file uploading.
* A button inside the modal that lets you navigate through the various steps / groups of form inputs.
* A "save" button inside the modal that persists your answers. This closes the modal and shows your set of answers as an instance in the list view (see the first bullet).
* In-line validation errors as the user is filling out each step of the modal
* Messages to notify the user that a modal needs to be opened and completed (Like if the user closed the modal before filling everything out, we want to let them know it's not fully completed).
Where some of the biggest complexity crop up:
* Form input data handling and general state management. As I mentioned, we can have upwards of maybe 20 controls in this component. At minimum, we need to write code to listen to input changes and make sure this new information is kept in sync with both internal component state and other parts of the UI. If you gave us a valid answer, we need to make sure to update the UI to show a checkmark (in the simple case). We also have controls that show or hide based on your answers to other form controls, so we'll need to handle inputs by potentially hiding or showing other inputs.
* Ok, so what if we need to write some UI updating code? Well, updating any part of the UI requires manual DOM manipulation. At minimum, this can be something simple like toggling visibility of an element by adding or removing a class. At worst, this involves appending html template strings (like if we need to show a list of errors on top of the modal) when the user attempts to save bad answers.
* Cross-component communication is difficult. Want to add a subcomponent? And it needs to communicate with the parent? For example, each step in our view is sort of a "sub form" and it needs to talk to the parent "modal" to keep global state - you'll need to roll some basic event handling code between the two.
* Testability. All these DOM changes - how do we test that our component is behaving? At the time we adopted stimulus, there wasn't a standard way of testing our stimulus controller so we've mostly leaned on (expensive) UI integration tests.
Some of this complexity is sort of inherent to forms - forms are complex UI components. There are hosts of libraries in other frameworks for dealing with form inputs alone (in order to cut down on the boilerplate you have to write). For example, formik in react helps cut down on a ton of boilerplate you have to write in order to wire the form control DOM state to react state.
Some of this complexity is also our own doing - we keep A LOT of state in the DOM. If a user blanks out an answer, we need to update the DOM with an additional form control whose value will get passed back to the backend to persist the change in the DB. Moving over to ReactJS won't help us here - this will require changes to our backend vs frontend API's.
However, having to handle the bulk of those DOM changes yourself requires a lot of code, is pretty error prone, and quite hard overall to maintain (as anyone who has rolled apps using vanilla javascript can attest to). StimulusJS doesn't offer many facilities for changing your UI in a declarative way.
Finally, cross-component communication is very common and while there is a number of ways to pass data back and form between stimulus controllers, there isn't really a nice way to do it outside of passing events. This is fine for simple cases, but error prone for our use cases. In reactjs, invoking callbacks that change parent state feels much more straightforward and has been easier to test.
I don't know enough about which parts of the site you're referring to to say for sure, but stimulus / stimulus-like approaches are perfectly fine for most display-only views even if they're fairly complex.
There are pages in our app where stimulus works great and the UI is fairly complex from a standpoint of the number of strictly user facing behavior (click this, show X, Y, and Z).
The complexity involved in handling form UI is just not very suitable for stimulus from my experience. That said, you can still do a lot in terms of following good programming practices to create something that looks and works great and is reasonably maintainable.
It ultimately comes down to your (and your teams) tolerance and business needs - there's no hard reason we need to stop using stimulus, we've just decided that there are parts of our app where we could make leaps in developer productivity by using something else.
I built complex form before on financial platform where it contains complex investment details, different entities for different kind of offering .. with vanilla javascript. But that may sound grumpy old man in argument. Stimulus' weak point may be where it requires many DOM mutation per action, that's probably better use declarative way to re-render the whole thing in different way based on data.
Not at all - I still favor vanillajs if only to avoid the bloat of the whole modern js toolchain (webpack...babel...extra compile times)
There's also a big difference for us within the form world between forms with ephemeral state and forms that need to persist state through many interactions and sessions. The former is perfectly fine for us to handle with vanilla js, the latter just becomes easier for developers with a declarative model.
Thank you for the detailed answer. I can definitely see you’ve crossed the line of adding subtle interactivity to the dom :)
The behaviors you describe are intricate and contain interconnected components which are probably managed by something like vuex (or the react equivalent)
Yeah, I also want to emphasize (again) that none of this is a criticism of stimulus - the key words / phrases they used two years ago and continue to use are things like "modest" and "augmenting html". Our needs were very different then. I'm not complaining that my hand saw isn't doing the same amount of work as a chain saw.
Awesome answer. I work a lot on business systems with complex forms and agree with everything you say.
We are still feeling our way for a solution as we don't want to move to a SPA (swapping one set of costs for another) but have outgrown our current SSR + hand-rolled JS + jQuery + Bootstrap JS mess.
I still have reservations about moving too heavily towards an SPA - we still mostly have server-side rendered pages that render with react components so we don't have to deal with things like client side routing. It's also still important for us we render most of the page server-side for performance reasons, but I'm not too informed about the specific trade-offs there in load times if we did push more rendering to the client.
I feel like there's some opportunity in the space to create a truly re-usable form library that can hook into your backend and be themed / customized to suite the look and feel of your app. There's some companies already in that space though, so I might take some time to check out some existing solutions and see if there's some kind of needs gap. What's challenging about this problem though (as I'm sure you're also aware) is that there are a ton of variability in requirements when it comes to forms - it's something I wish were more standardized but at times it feels like one of those bikeshedding topics where everyone has an opinion on how inputs show be displayed, how errors are shown, etc.
> There's some companies already in that space though, so I might take some time to check out some existing solutions and see if there's some kind of needs gap.
Which companies are you thinking of? It's been too long since I looked beyond $dayjob and investigated what could be possible.
One typical example is a component that collects (and displays) education history provided by the user. I'll list out just a few key UI behavior:
* A "read" view which represents a list of containers with titles that show each instance of a users answer (for example, if you provided 5 sets of answers, such as 5 universities you attended, we need to show those 5 in some sort of preview form on the page)
* A button that when clicked opens up a modal which contains multiple "steps" - each step is a small set of form inputs that range from simple ones like date to more complex ones involving search / autocomplete or file uploading.
* A button inside the modal that lets you navigate through the various steps / groups of form inputs.
* A "save" button inside the modal that persists your answers. This closes the modal and shows your set of answers as an instance in the list view (see the first bullet).
* In-line validation errors as the user is filling out each step of the modal
* Messages to notify the user that a modal needs to be opened and completed (Like if the user closed the modal before filling everything out, we want to let them know it's not fully completed).
Where some of the biggest complexity crop up:
* Form input data handling and general state management. As I mentioned, we can have upwards of maybe 20 controls in this component. At minimum, we need to write code to listen to input changes and make sure this new information is kept in sync with both internal component state and other parts of the UI. If you gave us a valid answer, we need to make sure to update the UI to show a checkmark (in the simple case). We also have controls that show or hide based on your answers to other form controls, so we'll need to handle inputs by potentially hiding or showing other inputs.
* Ok, so what if we need to write some UI updating code? Well, updating any part of the UI requires manual DOM manipulation. At minimum, this can be something simple like toggling visibility of an element by adding or removing a class. At worst, this involves appending html template strings (like if we need to show a list of errors on top of the modal) when the user attempts to save bad answers.
* Cross-component communication is difficult. Want to add a subcomponent? And it needs to communicate with the parent? For example, each step in our view is sort of a "sub form" and it needs to talk to the parent "modal" to keep global state - you'll need to roll some basic event handling code between the two.
* Testability. All these DOM changes - how do we test that our component is behaving? At the time we adopted stimulus, there wasn't a standard way of testing our stimulus controller so we've mostly leaned on (expensive) UI integration tests.
Some of this complexity is sort of inherent to forms - forms are complex UI components. There are hosts of libraries in other frameworks for dealing with form inputs alone (in order to cut down on the boilerplate you have to write). For example, formik in react helps cut down on a ton of boilerplate you have to write in order to wire the form control DOM state to react state.
Some of this complexity is also our own doing - we keep A LOT of state in the DOM. If a user blanks out an answer, we need to update the DOM with an additional form control whose value will get passed back to the backend to persist the change in the DB. Moving over to ReactJS won't help us here - this will require changes to our backend vs frontend API's.
However, having to handle the bulk of those DOM changes yourself requires a lot of code, is pretty error prone, and quite hard overall to maintain (as anyone who has rolled apps using vanilla javascript can attest to). StimulusJS doesn't offer many facilities for changing your UI in a declarative way.
Finally, cross-component communication is very common and while there is a number of ways to pass data back and form between stimulus controllers, there isn't really a nice way to do it outside of passing events. This is fine for simple cases, but error prone for our use cases. In reactjs, invoking callbacks that change parent state feels much more straightforward and has been easier to test.
Hope that helps