Rootstrap Blog

Link Tracking with StimulusReflex

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.

Initializing the project

To get started we are just going to follow the original docs and run the default rails new generator preconfigured to use StimulusJS as the Javascript backend and the name of our project (sho_lin is short for Shortened Link, get it? I promise it’s the first and only pun):

rails new sho_lin --webpack=stimulus -d postgresql

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

rails g controller pages index

And we’ll make this action our new root in our config/routes.rb

# change
get 'pages/index'

# to
root 'pages#index'

And we’ll dive into some Javascript as well by changing the HelloController in app/javascript/controllers/hello_controller.js for the following class

import { Controller } from "stimulus"

export default class extends Controller {
  static targets = ["output"]

  sayHello() {
    this.outputTarget.textContent = "Hello, Stimulus!"
  }
}

Now we just need to bind it to our markup in app/views/pages/index.html.erb by using some data attributes

<div data-controller="hello">
  <button data-action="click->hello#sayHello">Say hello</button>
  <h1 data-target="hello.output"></h1>
</div>

Now if you run the server with rails s and go to https://localhost:3000 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 Hello scoped to that HTML tree. That will start looking for specific data attributes which in this case are data-action and data-target. The first accepts a syntax to call specific methods on the instance of the controller on a given event, currently set to invoke the sayHello 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 textContent of our outputTarget the text will immediately appear on our h1 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

bundle add stimulus_reflex
bundle exec rails stimulus_reflex:install

Then let’s do some changes to the generated code by renaming app/reflexes/example_reflex.rb to app/reflexes/hello_reflex.rb and adjust the code inside as well

# change
class ExampleReflex < ApplicationReflex

# to
class HelloReflex < ApplicationReflex

And add a handler method called #greet to differentiate it from our frontend binding

  def greet
    @message = 'Hello, StimulusReflex!'
  end

And change the h1 tag in app/views/pages/index.html.erb to show our message

<h1><%= @message %></h1>

You DO NOT need to add the @message variable in the PagesController. If you do set it remember to use the ||= operator like @message ||= "Some default" since otherwise it would override the Reflex’s value.

Now we’ll adapt our frontend to call the HelloReflex by updating the HelloController in app/javascript/controllers/hello_controller.js to use StimulusReflex.

First import StimulusReflex

import StimulusReflex from 'stimulus_reflex'

Then register the controller in the connect function

  connect() {
    StimulusReflex.register(this)
  }

And finally, change the sayHello function to call the Reflex on the server

  sayHello() {
    this.stimulate('Hello#greet')
  }

Tho whole file should look like this:

import { Controller } from "stimulus"
import StimulusReflex from 'stimulus_reflex'

export default class extends Controller {
  connect() {
    StimulusReflex.register(this)
  }

  sayHello() {
    this.stimulate('Hello#greet')
  }
}

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:

class CreateShortenedLinks < ActiveRecord::Migration[6.0]
  def change
    create_table :shortened_links do |t|
      t.string :name, null: false
      t.string :shortened_path, null: false
      t.string :original_url, null: false
      t.integer :views_count, null: false, default: 0

      t.timestamps
    end
  end
end
# app/models/shortened_link

class ShortenedLink < ApplicationRecord
  validates :name, :shortened_path, :original_url, presence: true
  validates :shortened_path, uniqueness: true
end

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:

# app/controllers/shortened_links_controller.rb

class ShortenedLinksController < ApplicationController
  def index
    @shortened_links ||= ShortenedLink.all
  end
end
# app/views/shortened_links/index.html.erb

<h1>Shortened Links</h1>

<div class="row">
  <div class="col-8">
    <div class="row">
      <div class="col"><b>Name</b></div>
      <div class="col"><b>Shortened path</b></div>
      <div class="col"><b>Original url</b></div>
    </div>

    <% @shortened_links.each do |shortened_link| %>
      <div class="row">
        <div class="col"><%= shortened_link.name %></div>
        <div class="col"><%= shortened_link.shortened_path %></div>
        <div class="col"><%= shortened_link.original_url %></div>
      </div>
    <% end %>
  </div>
</div>

We are also going to add Bootstrap styling so it doesn’t look so dull. To do this, add the following tag to app/views/layouts/application.html.erb

<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css">

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:

# app/views/shortened_links/index.html.erb

<h1>Shortened Links</h1>

<div data-controller="shortened-links">
  <button data-action="click->shortened-links#newLink" class="btn btn-primary">New Shortened Link</button>

  <!-- links table ...-->
</div>
// app/javascript/controllers/shortened_links_controller.js

import { Controller } from 'stimulus'
import StimulusReflex from 'stimulus_reflex'

export default class extends Controller {
  connect () {
    StimulusReflex.register(this)
  }

  newLink() {
    this.stimulate('ShortenedLinksReflex#new')
  }
}

Now we should create that Reflex in app/reflexes/shortened_links_reflex.rb

class ShortenedLinksReflex < ApplicationReflex
  def new
    @form_action = :new
    @shortened_link = ShortenedLink.new
  end
end

And change the controller’s index action to take default values.

  # app/controllers/shortened_links_controller.rb

  def index
    @shortened_links ||= ShortenedLink.all
    @shortened_link ||= ShortenedLink.new
    @form_action ||= :none
  end

The only thing left to do is to actually render the form when we need to by adding the following in our app/views/shortened_links/index.html.erb

<h1>Shortened Links</h1>

<% if @form_action == :new %>
  <%= render partial: "form", locals: { shortened_link: @shortened_link, form_action: @form_action } %>
<% end %>

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 app/views/shortened_links/_form.html.erb

<div class="live-modal" data-controller="shortened-link-form">
  <div class="live-modal-backdrop">
    <div class="live-modal-content">
      <div class="live-modal-close">&times;</div>
      <h2>New Shortened link</h2>
        
      <%= fields_for(:shortened_link, shortened_link) do |form| %>
          
        <% if shortened_link.errors.any? %>
          <div id="error_explanation">
            <h5><%= pluralize(shortened_link.errors.count, "error") %> prohibited this shortened_link from being saved:</h5>

            <ul>
              <% shortened_link.errors.full_messages.each do |message| %>
                <li><%= message %></li>
              <% end %>
            </ul>
          </div>
        <% end %>

        <div class="form-group">
          <%= form.label :name %>
          <%= form.text_field :name, class: "form-control" %>
        </div>

        <div class="form-group">
          <%= form.label :shortened_path %>
          <%= form.text_field :shortened_path, class: "form-control" %>
        </div>

        <div class="form-group">
          <%= form.label :original_url %>
          <%= form.text_field :original_url, class: "form-control" %>
        </div>

        <button class="btn btn-primary">Save</button>
      <% end %>
    </div>
  </div>
</div>

With the needed CSS in app/assets/stylesheets/modal.css

.live-modal-backdrop {
    opacity: 1 !important;
    position: fixed;
    z-index: 1;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    overflow: auto;
    background-color: #FFF;
    background-color: rgba(0,0,0,0.4);
}

.live-modal-content {
    background-color: #fefefe;
    margin: 15% auto;
    padding: 0 20px 20px 20px;
    border: 1px solid #888;
    width: 80%;
}

.live-modal-close {
    color: #aaa;
    text-align: end;
    font-size: 28px;
    font-weight: bold;
}

.live-modal-close:hover,
.live-modal-close:focus {
    color: black;
    text-decoration: none;
    cursor: pointer;
}

There’s not much to be said here, the only interesting part is <div class="live-modal" data-controller="shortened-link-form"> 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 app/javascript/controllers/shortened_link_form_controller.js with a method to close the modal

import { Controller } from "stimulus"
import StimulusReflex from 'stimulus_reflex'

export default class extends Controller {
  connect () {
    StimulusReflex.register(this)
  }

  closeModal() {
    this.stimulate('ShortenedLinkFormReflex#close_modal')
  }
}

For which we’ll clearly need a new reflex in app/reflexes/shortened_link_form_reflex.rb with the following code:

class ShortenedLinkFormReflex < ApplicationReflex
  def close_modal
    @form_action = :none
  end
end

Pretty reactive, we just need to change a variable, and the if 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 X.

<div class="live-modal-close" data-action="click->shortened-link-form#closeModal">&times;</div>

And on the backdrop:

<div class="live-modal-backdrop" data-action="click->shortened-link-form#closeModal">

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 – app/javascript/controllers/shortened_link_form_controller.js

  trapClick(e) {
    e.stopPropagation()
  }

And bind it in the view:

<div class="live-modal-content" data-action="click->shortened-link-form#trapClick">

Also, just for fun, let’s also close the modal when the exit key is pressed:

<div class="live-modal"
     data-controller="shortened-link-form"
     data-action="keydown->shortened-link-form#handleKeyDown"
>
  handleKeyDown(e) {
    if (e.key === 'Escape') {
      this.closeModal()
    }
  }

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.

// app/javascript/controllers/shortened_link_form_controller.js

export default class extends Controller {
  static targets = ['name', 'shortenedPath', 'originalUrl']
 
  // ...

  validate(e) {
    this.stimulate('ShortenedLinkFormReflex#validate', [e.target], this.shortenedLinkParams)
  }

  get shortenedLinkParams () {
    return {
      shortened_link: {
        name: this.nameTarget.value,
        shortened_path: this.shortenedPathTarget.value,
        original_url: this.originalUrlTarget.value
      }
    }
  }
}

Again, this means we need to change our app/reflexes/shortened_link_form_reflex.rb to handle this.

  def validate(params)
    @form_action = :new
    @shortened_link = ShortenedLink.new(shortened_link_params(params))
    @shortened_link.validate
  end

  private

  def shortened_link_params(params)
    ActionController::Parameters.new(params).require(:shortened_link).permit(:name, :shortened_path, :original_url)
  end

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.

<div class="form-group">
  <%= form.label :name %>
  <%= form.text_field :name,
                      class: "form-control",
                      data: {
                          target: "shortened-link-form.name",
                          action: "input->shortened-link-form#validate",
                          reflex_permanent: true
                      }
  %>
</div>

<div class="form-group">
  <%= form.label :shortened_path %>
  <%= form.text_field :shortened_path,
                      class: "form-control",
                      data: {
                          target: "shortened-link-form.shortenedPath",
                          action: "input->shortened-link-form#validate",
                          reflex_permanent: true
                      }
  %>
</div>

<div class="form-group">
  <%= form.label :original_url %>
  <%= form.text_field :original_url,
                      class: "form-control",
                      data: {
                          target: "shortened-link-form.originalUrl",
                          action: "input->shortened-link-form#validate",
                          reflex_permanent: true
                      }
  %>
</div>

Saving the links

As a last step, we just need to add code to actually save the link. First in the FormController and then in the ShortenedLinkFormReflex:

// app/javascript/controllers/shortened_link_form_controller.js

export default class extends Controller { 
  // ...

  save(e) {
    this.stimulate('ShortenedLinkFormReflex#save', [e.target], this.shortenedLinkParams)
  }
}
class ShortenedLinkFormReflex < ApplicationReflex
  # ...

  def save(params)
    @form_action = :new
    @shortened_link = ShortenedLink.new(shortened_link_params(params))
    if @shortened_link.save
      @form_action = :none
    end
  end
end

And then we just need to bind again:

<button class="btn btn-primary" data-action="click->shortened-link-form#save">Create Shortened Link</button>

As a last touch, we can also add a flash notice, the markdown here is straightforward:

<% if flash.key?(:notice) %>
  <p id="notice" class="alert alert-primary"><%= flash[:notice] %></p>
<% end %>

<h1>Shortened Links</h1>

Then set the following message after creating the link:

  def save(params)
    # ...
    if @shortened_link.save
      @flash_notice = "Shortened Link saved successfully"
      @form_action = :none
    end
  end

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:

Joined Sessions

That’s alright, the fix is quite simple, we just need to set a CableReady identifier in our ApplicationController:

class ApplicationController < ActionController::Base
  before_action :set_action_cable_identifier

  private

  def set_action_cable_identifier
    cookies.encrypted[:session_id] = session.id.to_s
  end
end

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 app/views/shortened_links/index.html.erb:

  <div class="row">
    <div class="col"><%= shortened_link.name %></div>
    <div class="col"><%= shortened_link.shortened_path %></div>
    <div class="col"><%= shortened_link.original_url %></div>
    <div class="col">
      <div class="btn btn-secondary">Edit</div>
      <div class="btn btn-danger">Delete</div>
    </div>
  </div>

Don’t forget to add the following empty call after Original url:

  <div class="col"><b>Original url</b></div>
  <div class="col"></div>

And now we can implement the edit action. For that, we need the id on the HTML app/views/shortened_links/_form.html.erb:

<div class="live-modal"
     data-controller="shortened-link-form"
     data-action="keydown->shortened-link-form#handleKeyDown"
     data-shortened-link-form-link-id="<%= shortened_link.id %>"
>

And to use it on the StimulusJS controller app/javascript/controllers/shortened_link_form_controller.js:

  get shortenedLinkId() {
    const id = parseInt(this.data.get('linkId'))
    return !!id ? id : null
  }

  get shortenedLinkParams () {
    return {
      id: this.shortenedLinkId,
      shortened_link: {
        name: this.nameTarget.value,
        shortened_path: this.shortenedPathTarget.value,
        original_url: this.originalUrlTarget.value
      }
    }
  }

We also need to add a method to load the link correctly in app/reflexes/shortened_link_form_reflex.rb:

  def load_shortened_link(params)
    shortened_link = if params[:id].present?
                        ShortenedLink.find(params[:id])
                      else
                        ShortenedLink.new
                      end
    shortened_link.assign_attributes(shortened_link_params(params))
    
    shortened_link
  end

There’s also the issue of how do we determine the current form of action, to do so we need a simple method:

  def determine_form_action
    @shortened_link.persisted? ? :edit : :new
  end

And refactor the methods we have to use it:

  def validate(params)
    @shortened_link = load_shortened_link(params)
    @shortened_link.validate
    @form_action = determine_form_action
  end

  def save(params)
    @shortened_link = load_shortened_link(params)
    @form_action = determine_form_action
    if @shortened_link.save
      @flash_notice = "Shortened Link saved successfully"
      @form_action = :none
    end
  end

The only thing left to do is change the modal’s title according to the action app/views/shortened_links/_form.html.erb:

<% if form_action == :edit %>
  <h2>Edit Shortened link</h2>
<% else %>
  <h2>New Shortened link</h2>
<% end %>

Destroying a record should be pretty simple, so I’ll just show you the code. Change app/views/shortened_links/index.html.erb:

<div class="col">
  <div class="btn btn-secondary" data-action="click->shortened-links#editLink" data-link-id="<%= shortened_link.id %>">Edit</div>
  <div class="btn btn-danger" data-action="click->shortened-links#destroyLink" data-link-id="<%= shortened_link.id %>">Delete</div>
</div>

Bind it in app/javascript/controllers/shortened_links_controller.js:

  destroyLink(e) {
    if (confirm('Are you sure you want to remove this link?')) {
      const linkId = parseInt(e.target.dataset.linkId, 10)
      this.stimulate('ShortenedLinksReflex#destroy', linkId)
    }
  }

And add the method in the Reflex app/reflexes/shortened_links_reflex.rb:

  def destroy(id)
    ShortenedLink.find(id).destroy!
    @flash_notice = "Shortened Link destroyed successfully"
  end

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.

Header image

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:

# app/controllers/redirections_controller.rb

class RedirectionsController < ApplicationController
  def index
  end
end
# config/routes.rb

Rails.application.routes.draw do
  # ...
  get '/:path', to: 'redirections#index'
end

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:

# app/controllers/redirections_controller.rb

class RedirectionsController < ApplicationController
  def index
    link = ShortenedLink.find_by!(shortened_path: params[:path])
    link.increment!(:views_count)

    redirect_to link.original_url
  end
end

We want to use #increment! 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, www.sholi.com/58dad962, 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:

bundle exec rails generate channel LinkViews

Then we’ll change app/channels/link_views_channel.rb to just stream from "link_views":

class LinkViewsChannel < ApplicationCable::Channel
  def subscribed
    stream_from "link_views"
  end
end

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: app/javascript/channels/link_views_channel.js to match the following snippet:

consumer.subscriptions.create('LinkViewsChannel', {
  received(data) {
    if (data.cableReady) {
      CableReady.perform(data.operations)
    }
  }
});

FYI, don’t forget to actually import CableReady at the top of the file.

import CableReady from 'cable_ready'

Let’s now change our Rails views to actually show the link’s views count. Go to app/views/shortened_links/index.html.erb and add a new column on the table.

  <div class="col"><b>Original url</b></div>
  <div class="col"><b>Views</b></div>
  <div class="col"></div>

Then, populate it with the right data:

  <div class="col"><%= shortened_link.original_url %></div>
  <div class="col"><%= tag.span shortened_link.views_count, id: "link-#{shortened_link.id}-views-count" %></div>
  <div class="col">

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 app/controllers/redirections_controller.rb to look like this:

class RedirectionsController < ApplicationController
  include CableReady::Broadcaster

  def index
    link = ShortenedLink.find_by!(shortened_path: params[:path])
    link.increment!(:views_count)

    cable_ready["link_views"].text_content(
      selector: "#link-#{link.id}-views-count",
      text: link.views_count
    )
    cable_ready.broadcast

    redirect_to link.original_url
  end
end

What’s going on here? Well it’s pretty simple, we are including CableReady::Broadcaster to so we can broadcast operations. Then we add an operation on the "link_views" channel (the one we set to stream from already). This operation is a text replacement in the node with the id link-#{link.id}-views-count, that’s what the # 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!

About 

0 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.