Use Stimulus for Sender.net signup form in Rails app
Tags: rails, stimulus
Published: 2023-03-06 13:04:42

If you're using a form from [Sender.net](https://app.sender.net/) to collect e-mail subscribers, you may find it doesn't behave well in a Rails app with Turbo installed.

Sender.net provides a tool in its dashboard for building and formatting signup forms, but it doesn't create HTML for you to insert into your web site; it creates javascript, which pulls the data from their site and puts it into an iframe. Like some other tools that give you only a `script` tag, it doesn't play nicely with something like Turbo that only replaces parts of the HTML rather than executing a full page refresh from the server.

You may find that it works on first visit, but if you navigate to a different page on your site and back again, the form is just a blank box. The problem is that the code doesn't unload when Turbo executes a page change, so when you go back to the page with the form on it, the code is still in memory and so it doesn't run again. Like [I described with TinyMCE](https://dev.to/djchadderton/making-tinymce-work-with-rails-turbolinks-and-stimulus-2ooa), you can use Stimulus to remove the code when the page unloads and replace it when it loads again.

The following assumes you have a Rails app with Stimulus installed, and that you have an account with Sender.net. You will also need to create an embedded form formatted to your liking.

Go to the forms section on Sender.net and click on the Overview button next to the form you wish to use. Scroll down the page, and you will see the javascript you need to add to your head section, which will look something like this:

```html
<script>
  (function (s, e, n, d, er) {
    s['Sender'] = er;
    s[er] = s[er] || function () {
      (s[er].q = s[er].q || []).push(arguments)
    }, s[er].l = 1 * new Date();
    var a = e.createElement(n),
        m = e.getElementsByTagName(n)[0];
    a.async = 1;
    a.src = d;
    m.parentNode.insertBefore(a, m)
  })(window, document, 'script', 'https://cdn.sender.net/accounts_resources/universal.js', 'sender');
  sender('xxxxxxxxxxxxxx')
</script>
```

You also need to add a `div` to your page where you want the form to appear:

```html
<div style="text-align: left" class="sender-form-field" data-sender-form-id="xxxxxxxxxxxxxxxxxxx"></div>
```

If you add the script to the head section of your `application.html.erb` file, it will be loaded for every page on your site, whether the form is on it or not. This may be fine for you, but I only want it on my homepage, so I've placed it all in the same partial with the `script` section wrapped in a `content_for` block, so:

```html
<%= content_for :head do %>
<script>
    ...
</script>
<% end %>
```

Just before the closing `head` tag in application.html.erb, put:

```html
<%= yield :head %>
```

This will automatically place the form on the page, but this is not what we want as, to solve the problem, we need to do this manually. To tell the script not to render the form on load, add `?explicit=true` to the end of the URL within the script tags, i.e. make the penultimate line read:

```javascript
  })(window, document, 'script', 'https://cdn.sender.net/accounts_resources/universal.js?explicit=true', 'sender');
```

The `div` tag needs to be wrapped in another `div` in order to add a Stimulus controller, like so:

```html
<%= tag.div data: {controller: "sender-form", sender_form_signup_id_value: "xxxxxx"} do %>
  <div style="text-align: left" class="sender-form-field" data-sender-form-id="xxxxxxxxxxxxxxxxxxx"></div>
<% end %>
```

Replace the "xxxxxx" for `sender_form_signup_id_value` with the ID of the form at the top of the overview page for the form on Sender.net.

In `app/javascript/controllers/`, create the file `sender_form_controller.js`

```javascript
import { Controller } from '@hotwired/stimulus'

export default class extends Controller {
  static values = {
    signupId: String
  }

}
```

Sender.net provides some code to check whether their form code has yet loaded, which we can adapt for the `connect()` method, calling the `render` method to create the form:

```javascript
connect() {
  if (!window.senderFormsLoaded) {
    window.addEventListener("onSenderFormsLoaded", () => {
      this.render()
      })
  } else {
    this.render()
  }
}

render() {
  window.senderForms.render(this.signupIdValue)
}
```

The `disconnect()` method is quite straightforward, just calling the `destroy()` method and passing the form id:

```javascript
disconnect() {
  window.senderForms.destroy(this.signupIdValue)
}
```

A final touch is to prevent these methods from running if this is a Turbo preview:

```javascript
disconnect() {
  if (!this.preview) window.senderForms.destroy(this.signupIdValue)
}
  
render() {
  if (!this.preview) window.senderForms.render(this.signupIdValue)
}

get preview () {
  return document.documentElement.hasAttribute('data-turbo-preview')
}
```

Add the controller to the `app/javascript/controllers/index.js` file:

```javascript
import SenderFormController from "./sender_form_controller.js"
application.register("sender-form", SenderFormController)
```

And that should be it. It doesn't render as quickly as an HTML form as it has to grab code from another server and render it, but at least you won't be left with an empty white box where there should be a form when you navigate away and back again.

Here is the Stimulus controller in full:

```javascript
import { Controller } from '@hotwired/stimulus'

export default class extends Controller {
  static values = {
    signupId: String
  }

  connect() {
    if (!window.senderFormsLoaded) {
      window.addEventListener("onSenderFormsLoaded", () => {
        this.render()
        })
    } else {
      this.render()
    }
  }
  
  disconnect() {
    if (!this.preview) window.senderForms.destroy(this.signupIdValue)
  }
  
  render() {
    if (!this.preview) window.senderForms.render(this.signupIdValue)
  }

  get preview () {
    return document.documentElement.hasAttribute('data-turbo-preview')
  }
}
```