Live previews with Rails and Stimulus 2
nts.strzibny.name
nts.strzibny.name
Of course there are even more ways how to do this kind of thing. Keep sharing!
I had to throw in some debouncing and special handling for hidden inputs, as changes to hidden inputs do not trigger a change event on the form.
In the end the markup was very simple and is very reusable (StimulusJS v1):
<div data-controller="iframe-preview" data-iframe-preview-url="<%= preview_path(@letter) %>">
<form data-target="iframe-preview.form">
<textarea name="body">My letter</textarea>
</form>
<iframe data-target="iframe-preview.iframe">
</div>
This is when StimulusJS becomes really nice, when you can compose behavior in your markup with some simple data attributes. I did not think at first that I would need this controller in other places, but a couple of weeks later, I actually needed it, and was able to reuse the controller without modification for another use case.The benefit of being able to reuse the server template logic isn't being demonstrate because of the simplicity of the example.
The title is pure clickbait, it‘s nothing more than a simple ajax call. The type of logic you‘ve done 10 years ago with a little bit of jquery.
Turbo seemed more connected with existing models, and so I chose a Stimulus controller instead. But maybe I just don't know Turbo as much yet.
1. Replace the `output` target with a Turbo frame. 2. Add a value to the `data-controller` div with a `preview-url`: https://stimulus.hotwire.dev/reference/values 3. Change the `Rails.ajax` call with `let url = URL.new(this.previewUrlValue); url.searchParams.append('body', this.tweetTarget.value; this.outputTarget.src = url.toString();` 4. Change the `preview` action to render HTML with the same frame.
Now you have an automatic refreshing frame with less code.
You can also have a hidden form submission that does this from the existing controller so you don't need to specify the URL.
For example, you can add a hidden submit button like
form.submit 'preview', data: { composer_target: 'submit' }, hidden: true
Instead of a stimulus target, you wrap your preview area in a turbo frame <turbo-frame id="output">
...
</turbo-frame>
In your (ruby) controller you can re-use the existing controller action: def create
@post = Post.new(post_attributes)
preview && return if params[:commit] == 'preview'
...
end
Do the turbo junk in a private method for the preview: private
def preview
render turbo_stream: turbo_stream.replace(
'output', partial: "posts/preview", locals: { post: @post }
end
end
Your stimulus controller now just does this: preview() {
this.submitTarget.click();
}
I'm not sure which I prefer!I just refactored some old jquery stuff to use turbo and stimulus on a really complex form and it turned out well, I think. But below a certain level of complexity, it would have made sense to just do everything directly in stimulus with no AJAX at all.
I know your preview example is contrived (it's tough getting a real-world representative demo into a blog post, so please don't read this as criticism, I don't mean to say it's a bad example and I'm not trying to pick on it!) but really simple use cases are exactly the "sweet spot" for doing everything directly in the browser using Stimulus without having to call back to the server. Only once you start piling in business logic or reshuffling major parts of the display around does it fully pay off, and that's also about when you'll start appreciating having the whole model and the ability to re-use partials (rather than having to pick off a few pieces manually).
EDIT:
And just to be clear, in the sample code I put in above, you don't actually have to use the model. You can always just shove a single parameter into a special partial and go.
But as for the AJAX call, I still think it's quite simple solution that most will instantly understand (and that's a good thing).
this would be very useful, as there are not many comparative articles that really help you choose one approach over another. a number of years ago, i went through a couple rounds of turbo vs. ujs vs. websockets vs. custom js vs. something other library i don't remember atm, to try to figure out the best option for the app i was working on. lots of articles talk about the strengths of a library compared to others, but almost none walk through an example (or two) with enough depth to show those differences explicitly.
- Making everything go through a "controller" instead of well designed clients and APIs. Mixing rendering, redirection, ajax handling as well as page serving as well as business logic all into a "controller" is painful. ("Models" and the spaghetti design patterns they come with are even worse)
- PHP style templates that trigger database queries inline, making them difficult and eventually impossible to optimize
- Scattering view code across random places instead of writing view code with its corresponding view logic in the same file/component
- Wiring up functionality to templates by targeting selectors
- Manually setting innerHTML for dynamic functionality
- Dealing with rails "turbo" anything, which is not useful in a real world application, and dangerous (we've had production outages because of trying to use turbolinks)
- Not being able to customize behavior because you're locked into Rails design limitations
I'm a bit skeptical of Rails performance with the method the author details here, given that Ruby is much, much slower than Elixir.
I'm curious if even Elixir folks deploy Live Views in consumer-facing apps. But I can totally see it being used for admin interfaces or internal applications, for sure.
The method used in the blog post is making an ajax request to the server and then displaying the results in a div. For comparison, GitHub is running Rails and when you type into its search box it's making an ajax request to the server and displaying the results in a div.
Just wanted to bring that up here because this is different than what Live View does or what using Turbo Streams would do to broadcast changes over a websocket connection if you were using Rails.
I don't know of anyone running a large scale app using Live View as a primary focus but I did chat with someone on my podcast about how they used Phoenix and Live View to build https://textdb.dev. That episode is at https://runninginproduction.com/podcast/68-textdb-is-a-simpl.... TextDB trended here on HN a few months ago at https://news.ycombinator.com/item?id=23948234. It handled the HN front page load without issues, but it's also not doing a ton.
https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
In this case, the main advantage is that scaling is stateless, so your application servers just have to be able to deal with more traffic. There is nothing more to it.
As for Phoenix LiveView, it has both advantage and disadvantage of being stateful (something as using Action Cable). You now have to be scaling web sockets, but you save on authenticating the request (the request is lighter).
Even if LiveView is stateful, it's done on the platform which is optimized exactly for this purpose.
In both approaches, you also have to take into account that server latency might be a deal breaker for you.
Overall I'm a fan of what they're trying to do with Hey.com, but this particular use case feels like a big UX miss to me.
For any seasoned rails developer this is not difficult. Nothing about hotwire changes the testing approach in any meaningful way versus the old style ujs/jquery techniques, so the older system test tutorials and SO questions are just as valid as they were before.
Are there any technologies where UI testing isn't a massive pain?
document.querySelector("input").addEventListener("input", e => { document.querySelector("p").innerHTML = `<strong>${e.target.value}</strong>` });
Reusing the logic could be done if the server was node or through compiling the formatter to wasm. I struggle to see how Hotwire makes things simpler and has comparable speed to a SPA?As for the Stimulus part, the idea is to use Stimulus for everything interactive, so those extra 80kb is for the whole application.
Btw Turbo Stream is kind of cool, but in this particular case, do you think its' really better?