Initializing the project
To get started we are just going to follow the original docs and run the default [.c-inline-code]rails new[.c-inline-code] generator preconfigured to use StimulusJS as the Javascript backend and the name of our project ([.c-inline-code]sho_lin[.c-inline-code] is short for Shortened Link, get it? I promise it's the first and only pun):
By the way, StimuluJS is a front-end framework concerned with adding functionality to the HTML rather than controlling the whole DOM. If you wish to learn more about it, here's a recent post that explains how it works.
Testing that StimulusJS works
Even though this framework deserves a more in-depth explanation, let's work on a quick example to make sure everything works as expected and to provide you with insights on how it works.
First, we'll create a basic controller with an index action to just render some HTML
And we'll make this action our new root in our [.c-inline-code]config/routes.rb[.c-inline-code]
And we'll dive into some Javascript as well by changing the [.c-inline-code]HelloController[.c-inline-code] in [.c-inline-code]app/javascript/controllers/hello_controller.js[.c-inline-code] for the following class
Now we just need to bind it to our markup in [.c-inline-code]app/views/pages/index.html.erb[.c-inline-code] by using some data attributes
Now if you run the server with [.c-inline-code]rails s[.c-inline-code] and go to [.c-inline-code]https://localhost:3000[.c-inline-code] you'll see that when you click on the "Say hello" button you'll get back "Hello, Stimulus!". I think the code it's pretty self-explanatory but I'll give a quick walkthrough anyway.
Here we are just telling StimulusJS to hook the controller prefixed with [.c-inline-code]Hello[.c-inline-code] scoped to that HTML tree. That will start looking for specific data attributes which in this case are [.c-inline-code]data-action[.c-inline-code] and [.c-inline-code]data-target[.c-inline-code]. The first accepts a syntax to call specific methods on the instance of the controller on a given event, currently set to invoke the [.c-inline-code]sayHello[.c-inline-code] method when the click event occurs. The latter binds the text to a target, which is just like a reference to the element but more at the same time, keeping them synchronized.
So in this case when we set the [.c-inline-code]textContent[.c-inline-code] of our [.c-inline-code]outputTarget[.c-inline-code] the text will immediately appear on our [.c-inline-code]h1[.c-inline-code] tag. Keep this concept in the back of your mind since we'll keep using it throughout the series.
Adding Reflex to our previous example
Now that we've seen how StimulusJS works let's get down to business. The first step is to install it, which is extremely simple
Then let's do some changes to the generated code by renaming [.c-inline-code]app/reflexes/example_reflex.rb[.c-inline-code] to [.c-inline-code]app/reflexes/hello_reflex.rb[.c-inline-code] and adjust the code inside as well
And add a handler method called [.c-inline-code]#greet[.c-inline-code] to differentiate it from our frontend binding
And change the [.c-inline-code]h1[.c-inline-code] tag in [.c-inline-code]app/views/pages/index.html.erb[.c-inline-code] to show our message
You DO NOT need to add the [.c-inline-code]@message[.c-inline-code] variable in the [.c-inline-code]PagesController[.c-inline-code]. If you do set it remember to use the [.c-inline-code]||=[.c-inline-code] operator like [.c-inline-code]@message ||= "Some default"[.c-inline-code] since otherwise it would override the Reflex's value.
Now we'll adapt our frontend to call the HelloReflex by updating the HelloController in [.c-inline-code]app/javascript/controllers/hello_controller.js[.c-inline-code] to use StimulusReflex.
First import [.c-inline-code]StimulusReflex[.c-inline-code]
Then register the controller in the connect function
And finally, change the [.c-inline-code]sayHello[.c-inline-code] function to call the Reflex on the server
Tho whole file should look like this:
You can test it again and you should get back "Hello, StimulusReflex!". How did it happen? Behind the scenes, StimulusReflex opens up a WebSocket connection and runs the Reflex's action and then the Controller's action sending the resulting HTML over the wire. On the client's side, it does a diff between the result and the current DOM performing the minimum updates to update the latter.
Up next
We now have a basic Hello World example up where you can see the whole code on these two commits. Now it's time to get our hands dirty and write some code. The first thing we are going to do is take care of managing our shortened links.
Creating the model
Let's start with some good old fashioned Rails code, we'll create the migration for our links and the associated model:
Notice how we added some simple validations to make it more fun.
Listing links
Let's start with the simplest of actions, listing the links. To do so, we'll create the following files:
We are also going to add Bootstrap styling so it doesn't look so dull. To do this, add the following tag to [.c-inline-code]app/views/layouts/application.html.erb[.c-inline-code]
The link form
What we did in the last section was pretty normal, huh? So, let's keep going. Now we'll add a button to create new links by stimulating a Reflex. To do so, we add this markup and the corresponding StimulusReflex code:
Now we should create that Reflex in [.c-inline-code]app/reflexes/shortened_links_reflex.rb[.c-inline-code]
And change the controller's index action to take default values.
The only thing left to do is to actually render the form when we need to by adding the following in our [.c-inline-code]app/views/shortened_links/index.html.erb[.c-inline-code]
For the actual form, let's do something special. We don't want to redirect to another page but rather allow creating them on the spot. It seems like a modal it's what we need, but in the interest of using less Javascript and leveraging StimulusReflex we'll take a different approach. You'll get it in a minute.
This idea of using backend-managed modals it's taken from LiveView's modal and I'll admit that the code here is basically a copy paste of that 😅. So let's just add the basic modal markup in [.c-inline-code]app/views/shortened_links/_form.html.erb[.c-inline-code]
With the needed CSS in [.c-inline-code]app/assets/stylesheets/modal.css[.c-inline-code]
There's not much to be said here, the only interesting part is [.c-inline-code]<div class="live-modal" data-controller="shortened-link-form">[.c-inline-code] where we are actually using a new controller. This is just to decompose our code into more reusable units.
Let's go ahead and add that Stimulus controller in [.c-inline-code]app/javascript/controllers/shortened_link_form_controller.js[.c-inline-code] with a method to close the modal
For which we'll clearly need a new reflex in [.c-inline-code]app/reflexes/shortened_link_form_reflex.rb[.c-inline-code] with the following code:
Pretty reactive, we just need to change a variable, and the [.c-inline-code]if[.c-inline-code] we added above will automatically stop rendering the modal by not sending the HTML to the frontend. This is very neat in my opinion.
Next up we just bind the closing action on the [.c-inline-code]X[.c-inline-code].
And on the backdrop:
But we also need to trap the click so that it doesn't close when clicking inside. So we just add this method to trap that - [.c-inline-code]app/javascript/controllers/shortened_link_form_controller.js[.c-inline-code]
And bind it in the view:
Also, just for fun, let's also close the modal when the exit key is pressed:
Cool! Now we have a form inside a working modal and all with just a few lines of Javascript.
Adding validations
ActiveRecord validations rule! That's why we want to use them as much as possible. To do this, we first need to send the values to the Reflex by binding them in the JS controller and adding a validation action.
Again, this means we need to change our [.c-inline-code]app/reflexes/shortened_link_form_reflex.rb[.c-inline-code] to handle this.
We need to add the action again or it'll get overridden later in the controller. For now, we can just hard-code it. Then we just need to bind for each attribute.
Saving the links
As a last step, we just need to add code to actually save the link. First in the [.c-inline-code]FormController[.c-inline-code] and then in the [.c-inline-code]ShortenedLinkFormReflex[.c-inline-code]:
And then we just need to bind again:
As a last touch, we can also add a flash notice, the markdown here is straightforward:
Then set the following message after creating the link:
And that's it, we can now create shortened links!
Joined sessions
If you open a new incognito session, you'll see there's some funny business going on. When working on one session it will also trigger actions on the other side, so opening a modal will magically make it appear on other users' screens. Like so:
That's alright, the fix is quite simple, we just need to set a [.c-inline-code]CableReady[.c-inline-code] identifier in our [.c-inline-code]ApplicationController[.c-inline-code]:
Editing and destroying
The first thing we need to do to allow these actions is to actually add the buttons. We can do so at [.c-inline-code]app/views/shortened_links/index.html.erb[.c-inline-code]:
Don't forget to add the following empty call after [.c-inline-code]Original url[.c-inline-code]:
And now we can implement the edit action. For that, we need the id on the HTML [.c-inline-code]app/views/shortened_links/_form.html.erb[.c-inline-code]:
And to use it on the StimulusJS controller [.c-inline-code]app/javascript/controllers/shortened_link_form_controller.js[.c-inline-code]:
We also need to add a method to load the link correctly in [.c-inline-code]app/reflexes/shortened_link_form_reflex.rb[.c-inline-code]:
There's also the issue of how do we determine the current form of action, to do so we need a simple method:
And refactor the methods we have to use it:
The only thing left to do is change the modal's title according to the action [.c-inline-code]app/views/shortened_links/_form.html.erb[.c-inline-code]:
Destroying a record should be pretty simple, so I'll just show you the code. Change [.c-inline-code]app/views/shortened_links/index.html.erb[.c-inline-code]:
Bind it in [.c-inline-code]app/javascript/controllers/shortened_links_controller.js[.c-inline-code]:
And add the method in the Reflex [.c-inline-code]app/reflexes/shortened_links_reflex.rb[.c-inline-code]:
Up next
We've learned how to do the basic CRUD operations that can be applied to any domain. All that's left is actually tracking the views which we'll cover in the next and last part of this series. And as always, here's a link to the complete code on this post:
Now we are going to use what we previously built to start tracking views. To do so, we will use ActionCable and CableReady to broadcast and make changes in our frontend.
Redirecting Users
Let's start with the simple stuff, when a user follows one of our shortened links, they expect to be redirected to the original URL. To do this, we need to create a new controller and add the corresponding route:
Make sure to add this at the bottom of the block so it does not override any other routes.
By using the following, the implementation should be pretty straightforward:
We want to use [.c-inline-code]#increment![.c-inline-code] as it's atomic, and we also don't want to run any validations as it would just slow us down. However, what you really want to do is have a background job to increment the attribute. That way you are absolutely certain that the redirect will happen in the least possible time. However, that is outside the scope of this article.
Once this in place, when people click on a shortened link, for example, [.c-inline-code]www.sholi.com/58dad962[.c-inline-code], they will get redirected to the original URL, and we will then track an additional view for that link.
Displaying Link Views
At this point, we could just make a static page and spit out our data, but where's the fun in that? To provide you with an alternative method for passing data from the server to the view, we will make page views update in real-time, by using CableReady.
To do this, let's first generate a new channel by running the following:
Then we'll change [.c-inline-code]app/channels/link_views_channel.rb[.c-inline-code] to just stream from [.c-inline-code]"link_views"[.c-inline-code]:
We must now tell our client channel (the Javascript one) to perform CableReady operations, if necessary. You can do this by editing the code inside the following: [.c-inline-code]app/javascript/channels/link_views_channel.js[.c-inline-code] to match the following snippet:
FYI, don't forget to actually import CableReady at the top of the file.
Let's now change our Rails views to actually show the link's views count. Go to [.c-inline-code]app/views/shortened_links/index.html.erb[.c-inline-code] and add a new column on the table.
Then, populate it with the right data:
Notice how we wrapped the views count inside a span tag with an ID. We are doing this because we need to tell CableReady to perform operations later on. To do this, we must require a selector.
So, just copy the selector and change [.c-inline-code]app/controllers/redirections_controller.rb[.c-inline-code] to look like this:
What's going on here? Well it's pretty simple, we are including [.c-inline-code]CableReady::Broadcaster[.c-inline-code] to so we can broadcast operations. Then we add an operation on the [.c-inline-code]"link_views"[.c-inline-code] channel (the one we set to stream from already). This operation is a text replacement in the node with the id [.c-inline-code]link-#{link.id}-views-count[.c-inline-code], that's what the [.c-inline-code]#[.c-inline-code] symbolizes, and the text is the new view count.
Conclusion
Throughout this post, we learned how to setup StimulusReflex, build CRUD apps on a breeze, and track data in real-time. All of this with minimal JS and maximum productivity.
What's Next?
This concludes on how to use StimulusReflex, but you can still play around with this project and add more functionality with the following:
- Track the browser from the links users clicked
- Track the user's country
- Add charts in real-time
- And much more...
Or, just start a project of your own and give StimulusReflex a try!This article is about StimulusReflex, a new tool to help you bring Rails to the era of the backend-side-managed frontends. I was surprised to see that Phoenix LiveView and following with things like Motion and Sockpuppet use WebSockets to push updates from the server to the client and update the DOM accordingly.Luckily the team at StimulusReflex's folks created a gem that does just that. I'll show you how to use it by building a link shortener with this new and exciting technology. In this part of the series, we'll focus on setting up and running StimulusReflex so we can build awesome features for our next project.