Build a real-time newsletter signup form with Rails and Turbo
Tags: rails, ruby
Published: 2023-11-10 15:10:39

---
title: Build a real-time newsletter signup form with Rails and Turbo
published: true
date: 2023-10-01 17:46:55 UTC
tags: rails, ruby
canonical_url: https://web-crunch.com/posts/build-a-realtime-newsletter-rails-turbo
---

![Build a real-time newsletter signup form with Rails and Turbo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qzl5olytpjp5clwoqyge.jpg)

For nearly every meaningful project I've taken on, there's always been a landing page first to help me market and semi-validate an idea.

Tons of tools exist to capture email and market to that audience, but I've always been bothered by the lack of control of that data and how we blindly hand it over to services like Mailchimp, Mailerlite, ConvertKit, etc...

Those tools save you time and enable you to focus on your project, but I commonly like to keep that stuff in-house. That way, as my project scales, so can my marketing tactics.

Collecting a simple email address for the purpose of a newsletter seems super downright and straightforward overkill for Rails. Still, in this guide, I’ll show you some techniques to make the process snappy and more delightful than the legacy ways involving page refreshes and redirects.

## Create a vanilla rails app

```
rails new hotwire_newsletters
```

#### Install Rails UI - Optional

I made Rails UI to help save myself and your time. You are more than welcome to skip this step.

```
bundle add railsui --github getrailsui/railsui --branch main
```

With Rails UI installed, you’ll need to boot your server `bin/dev` and visit `localhost:3000`. From there, you may configure your Rails UI installation in a couple of clicks. Don't worry about installing any pages for now.

The next simplified steps are

1. Choose a CSS framework
2. Choose a theme
3. Click “Save changes”

### Generate a Subscriber resource

It's worth noting that in many apps I’ve built, the model `Subscriber` is used for other things like billing, so to keep that from being an issue, you might name your resource `NewsletterSubscriber` or something similar.

A scaffold is overkill for our need, but I’m more focused on speed. We’ll clean up the cruft this generates in a later step.

```
rails g scaffold Subscriber email
```

Migrate the database and create the new `Newsletter` table.

```
rails db:migrate
```

### Update routing

The default routing at this stage points to the Rails UI start page. Let’s change that to be a new page on a new `pages_controller.rb`. If you don't have a `pages_controller.rb`, you can make one manually or run `rails g controller pages home`.

```ruby
Rails.application.routes.draw do
  resources :newsletters

  if Rails.env.development? || Rails.env.test?
    mount Railsui::Engine, at: "/railsui"
  end

  # Inherits from Railsui::PageController#index
  # To override, add your own page#index view or change to a new root
  # Visit the start page for Rails UI any time at /railsui/start
  # root action: :index, controller: "railsui/page"

  devise_for :users

  # Defines the root path route ("/")
  root "pages#home"
end
```

_If using Rails UI, comment out the existing root path and add the new one at the bottom._

### Update pages\_controller file

Create a `page_controller.rb` file in `app/controllers` and a `home` action within that controller. Add the corresponding `app/views/pages` directory with a `home.html.erb` template as well.

```ruby
class PagesController < ApplicationController
  def home
    @hide_nav = true
  end
end
```

For this guide, I’ve added an instance variable called `@hide_nav`. The nav feels like a distraction since we want to focus more on the subscriber form. We'll use this in the application layout file to not render the nav if it's set to `true`

```erb
<!-- app/views/layouts/application.html.erb -->
<!DOCTYPE html>
<html>
  <head>
    <title>HotwireNewsletterSubscription</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>
    <%= stylesheet_link_tag "application", "https://unpkg.com/trix@2.0.0/dist/trix.css", "data-turbo-track": "reload" %>

    <%= render "shared/fonts" %>
    <%= render "shared/meta" %>
    <%= yield :head %>
  </head>

  <body class="rui">
    <%= render "shared/flash" %>
    <% if content_for(:header).present? %>
      <%= yield :header %>
    <% else %>
      <%= render "shared/nav" unless @hide_nav %>
    <% end %>
    <%= yield %>
    <%= railsui_launcher if Rails.env.development? %>
  </body>
</html>
```

You should now see the home page as the root page in your app without a navigation in sight.

## Dialing in the subscribe page

I’m going to treat the “home” page as a simple subscribe page for this guide.

In the view, we’ll render a primary container with a form. Notice the `turbo_frame_tag` with the ID of `newsletter.`

Also, notice the `src` attribute, which dynamically renders the view on the other end of the path you include.

```erb
<!-- app/views/page/home.html.erb -->
<div class="flex flex-col justify-center items-center h-screen bg-slate-50 border-t">
  <div class="w-[460px] mx-auto rounded-xl bg-white p-8 shadow border border-slate-300">
    <div class="mb-6">
      <h4 class="tracking-tight">Subscribe to our newsletter</h4>
      <p class="my-6 text-slate-700">Tips based on proven scientific research.</p>
    </div>

    <%= turbo_frame_tag "newsletter", src: new_subscriber_path %>
  </div>
</div>

```

`new_subscriber_path` points to `app/views/subscribers/new.html.erb`. So, the contents of that page essentially render in place if there's another turbo\_frame\_tag in that file. Pretty slick!

An important thing to note is that the `new.html.ere` template only contains the form but is surrounded by a familiar `turbo_frame_tag` with the same ID as we used on the `home.html.erb` template. Without this, the view won't render correctly.

```erb
<!-- app/views/newsletter_subscribers/new.html.erb-->
<%= turbo_frame_tag "newsletter" do %>
  <%= render "form", subscriber: @subscriber %>
<% end %>
```

I updated the design of the `_form.html.erb` partial slightly from what gets generated with Rails UI. You can find this in `app/views/subscribers/_form.html.erb`.

```erb
<!-- app/views/subscribers/_form.html.erb-->
<%= form_with(model: subscriber) do |form| %>
  <%= render "shared/error_messages", resource: form.object %>

  <div class="form-group">
    <%= form.label :email, class: "form-label" %>
    <%= form.text_field :email, class: "form-input", placeholder: "My email address is" %>
  </div>

  <div class="flex items-center justify-between flex-wrap">
    <div class="sm:flex-1 sm:mb-0 mb-6">
      <%= form.submit class: "btn btn-primary w-full" %>
    </div>
  </div>
<% end %>
```

### Remove the cruft

The scaffold we ran generated a lot of fluff that we didn’t need. I removed every view file in `app/views/subscribers` except for the `_form.html.erb` partial and the `new.html.erb` template within `app/views/subscribers`

## Simplifying the Subscribers Controller

The scaffold generator gives all the CRUD actions you might use in a typical Rails controller, but we're not using most of them. Much like the view files, I simplified the controller for our purposes and reduced everything to `create` and `new` actions. That leaves us with only a few lines of ruby.

```ruby
class SubscribersController < ApplicationController
  def new
    @subscriber = Subscriber.new
  end

  def create
    @subscriber = Subscriber.new(subscriber_params)

    unless @subscriber.save
      render :new
    end
  end

  private
    def subscriber_params
      params.require(:subscriber).permit(:email)
    end
end
```

Pay attention to the `create` action, which is where the magic lies.

Unless there’s an issue saved successfully, we’ll render the `new.html.erb` template.

If the newsletter subscriber does save, Rails knows to check for a `create.html.erb` file in a last-ditched effort to render something as a response. We can use turbo frames to create a new view showing a proper success state that effectively re-renders the view with updated content in real time.

Create a new view called `create.html.erb` in `app/views/subscribers` . I modified an alert component inside this template that ships with Rails UI to display a success banner and message.

```erb
<!-- app/views/subscribers/create.html.erb-->
<%= turbo_frame_tag "newsletter" do %>
  <div class="bg-green-50/90 text-green-700 p-4 rounded text-sm sm:flex items-center justify-between">
    <div class="flex items-start justify-between space-x-3">
      <%= icon "check", classes: "text-green-600 w-5 h-5 flex-shrink-0" %>

      <div class="flex-1">
        <p class="text-green-800 font-semibold">Successfully subscribed</p>
        <p class="leading-snug my-1">Look for a confirmation email from us in your inbox soon.</p>
      </div>
    </div>
  </div>
<% end %>
```

Notice we’re leveraging the same `turbo_frame_tag` with the `newsletter` ID being passed. This is important.

When you add a valid email address and submit the form, you should see the success state instantly.

### What about error states and validations?

This is relatively easy, assuming you’re already rendering proper form errors in your form.

I added a basic validation to the `Subscriber` model to ensure a value for `email` when the form is submitted. If not, we'll display errors.

```ruby
class Subscriber < ApplicationRecord
  validates :email, presence: true
end
```

![error state for subscriber form validation](https://f001.backblazeb2.com/file/webcrunch/error-state.png)

Rails Ul comes pre-styled with error handling, but feel free to modify this in `app/views/shared/_error_messages.html.erb`. You can also customize [what error messages return on the validation itself](https://guides.rubyonrails.org/active_record_validations.html#message).

### What about spam?

Good question. There are a lot of bots that will game your forms. One quick win that has helped me is using a CAPTCHA for all publicly accessible forms. A great one that integrates with Rails is called [invisible\_captcha](https://github.com/markets/invisible_captcha). It's a simple gem and is simple to use. Perhaps I'll do a quick tutorial on it in the future.

#### Useful links

- [The web-crunch Hotwire and Rails collection](https://web-crunch.com/collections/hotwire-and-rails)
- [Hotwire home page](https://hotwired.dev)
- Github Repo for this Guide
- [Subscribe to my YouTube channel](https://www.youtube.com/webcrunch)