Rails

Single Page Application Autoresponder with Rails 4 and Angular.js (part 3)


Welcome back to the third part of “How to create your own Single Page Application Autoresponder with Rails 4 and Angular.js”.

In this tutorial, we are going to implement some features that satisfy the requirements bellow:
– As a logged user, you’re able to generate and embed a sign up form in your website in order to add subscribers to any email list.
– As a visitor user you’re able to fill in a signup form and join an email list.

Sign up forms

Sign up forms are an incredible way to build a list of subscribers for your website. You just need to include this signup form in your website and convert them into subscribers through your sign up process.

So, the next feature to implement are signup forms, for the time being, I’m just going to include two fields: email and name; if we have some extra time later, we can go back to this section and create a super cool dynamic builder form.

Let’s start defining the new route for Signup Forms:

// app/assets/javascripts/autoresponder/states.js

        .state("email_list.details.signup_forms", {
          url: "/signup_forms",
          abstract: true,
          controller: 'emailListSignupFormCtrl',
          templateUrl: 'autoresponder/components/email_lists/details/signup_forms/main.html',
        })

        .state("email_list.details.signup_forms.settings", {
          url: "",
          controller: 'emailListSignupFormCtrl',
          templateUrl: 'autoresponder/components/email_lists/details/signup_forms/settings.html',
        })

        .state("email_list.details.signup_forms.publish", {
          url: "/publish",
          controller: 'emailListSignupFormCtrl',
          templateUrl: 'autoresponder/components/email_lists/details/signup_forms/publish.html',
        })

The following code is used for the main template in order to render the different options for the signup form. For now, we have only two options:

# app/assets/javascripts/autoresponder/components/email_lists/details/signup_forms/main.html.haml
.row
  .email-list-details.col-md-2.col-md-offset-10
    %ul.nav-menu.text-right
      %li{'ng-class' => "{ 'active': $state.includes('email_list.details.signup_forms.settings') }"}
        %a{:href => "#", 'ui-sref' => 'email_list.details.signup_forms.settings'}
          Settings

      %li{'ng-class' => "{ 'active': $state.includes('email_list.details.signup_forms.publish') }"}
        %a{:href => "", 'ng-click' => 'handlePublish()'}
          Publish

.row
  %ui-view

As you see in the previous template, we are using a function called handlePublish() which is the responsible to enable/disable the published link option; if the user has not provided the required fields(Thank you page url and already subscribed page) yet, then we need to add it in the parent controller called “emailListCtrl”:

# app/assets/javascripts/autoresponder/components/email_lists/emailListCtrl.js
      $scope.availablePublish = function() {
        return ($scope.email_list.thank_you_page_url && $scope.email_list.already_subscribed_url);
      }

      $scope.handlePublish = function() {
        if ($scope.availablePublish()) {
          // we are visiting a sibling called publish
          // because we are in the sibling called '.settings'
          $scope.$state.go('^.publish');
        } else {
          $scope.showNotificationRequiredFields();
        }
      }

      $scope.showNotificationRequiredFields = function() {
        Render.showGrowlNotification('warning', 'Before to publish the form you must select the custom urls');
      }

In the code above, basically we are validating that the url attributes for email list are present, if the condition is not satisfied we’ll show a message to the user and force him to stay in the settings option.

Email list settings before embedding your form:

In settings, we’ll force the user to enter the urls responsible for redirecting the user after they were added to the list or if they were already subscribed to the email list.

# app/assets/javascripts/autoresponder/components/email_lists/details/signup_forms/settings.html.haml
%form.sign-up-form-settings{:name => "EmailListForm",
                            "ng-submit" => "updateEmailList()",
                            :novalidate => ""}
  .row
    .col-md-4

      .form-group
        %label{:for => "thank-you-page"} Thank you page url (*)
        %input#thank-you-page.form-control{:type => "url",
                                           'required' => 'required',
                                           :placeholder => 'http://',
                                           "ng-model" => 'email_list.thank_you_page_url'}/

      .form-group
        %label{:for => "already-subscribed"} Already subscribed page (*)
        %input#already-subscribed.form-control{:type => "url",
                                               'required' => 'required',
                                               :placeholder => 'http://',
                                               'ng-model' => 'email_list.already_subscribed_url'}/

  .row
    .col-md-6
      %button{ type:'submit',
               'ng-disabled' => "EmailListForm.$invalid",
               class: 'btn btn-success'}
        Save settings

Embedding your form in your website:

We’ll put the html code within a textarea you need to embed in your website, in order to add subscribers to your list.

# app/assets/javascripts/autoresponder/components/email_lists/details/signup_forms/publish.html.haml
.row

  .col-md-12

    %textarea#output.form-control{ 'replace-textarea-values'=> '',
                                   :rows => '10',
                                   "ng-model" => "previewer"}
    #hidden{"ng-hide" => "true"}

As you see, we are using the directive “replace-textarea-values” => replaceTextareaValues and the content would look like:

# app/assets/javascripts/autoresponder/components/email_lists/details/signup_forms/replaceTextAreaDirective.js
AutoresponderApp

   .directive(
     'replaceTextareaValues',
     ['$compile', 'templateManager', '$timeout',
     function($compile, templateManager, $timeout) {

     var selectedTemplate = templateManager.loadTemplate('template_one');;

     return {
       restrict: 'A',
       templateUrl: 'autoresponder/components/email_lists/details/signup_forms/publish.html',
       scope: true,
       link: function($scope, elem, attrs) {

         // element included in the template
         var hiddenElem = $('#hidden');;
         $scope.previewer = '';

         function update() {

           if (selectedTemplate) {

             $compile(hiddenElem.html(selectedTemplate))($scope);

             $timeout( function(){
               $scope.previewer = hiddenElem.html();
             }, 0);
           }
         }

         $scope.$watch('template', update);
       }
     };
   }])

So, what are we exactly doing in this directive?
Basically we have a template string with some {{values}} inside and we are interpolating those values depending on a dynamic data; then we are generating the html and putting it inside the textarea content in order to allow the user to embed and copy the generated html code in their website. We are also using a template manager factory which is mentioned in the next paragraph.

Template manager

For now, we are using a simple template to show the user the html code they need to embed on their website. For the time being, we are storing that template string in a factory, but in the future we would save it in the database.

// app/assets/javascripts/autoresponder/components/common/templateManager.js
AutoresponderApp.factory('templateManager', function() {
  return {
    loadTemplate:  function (template_name) {
      var template;
      switch (template_name) {
        case "template_one":
          //lets add the redirect_to for already subscribed and thank you page elements
          template = "<form action='{{subscribe_url}}' id='embeded-subscribe-autoresponder' method='post' name='embeded_subscribe_autoresponder' novalidate='' target='_blank'>\n<label for='subscribe_email'>Email Address</label>\n<input name='subscribe_email' type='email' value=''>\n<label for='subscribe_name'>Full Name</label>\n<input name='subscribe_name_input' type='text' value=''>\n<input type='hidden' name='thank_you_page' value='{{thank_you_page}}'/>\n<input type='hidden' name='already_subscribed_page' value='{{already_subscribed_page}}'/><br>\n<input name='subscribe-submit' type='submit' value='Subscribe'>\n</form>";
          break;
        case "template_two":
          template = "template_two";
          break;
        default:
          template = "";
      }

      return template;
    }
  }
});

We are declaring the Utils factory where we are putting shared/common/general functions. In this ocassion, we are declaring a method that retrieves the current url and use it to set the form action url in the generated html, that the users are going to use in order to add multiple subscribers to their email lists.

// app/assets/javascripts/autoresponder/components/common/utils.js
AutoresponderApp.factory('Utils', function() {
  return {
    // if you need to get if the url includes http or https
    httpProtocolUrlFor: function(url_params) {
      var domain, port;
      domain = window.location.protocol + "//" + window.location.hostname;
      port = window.location.port ? ":" + window.location.port : "";
      return domain + port + url_params;
    },
  }
});

If you’re wondering how it looks so far, I’m attaching a screenshot for that purpose:
Settings and embeding html signup form

Email sign up form controller:

We are using the $sce to avoid the error while interpolating a dynamic url action form.

// app/assets/javascripts/autoresponder/components/email_lists/details/signup_forms/emailSignupCtrl.js
AutoresponderApp
  .controller('emailListSignupFormCtrl', [
    '$scope',
    '$sce',
    'findCurrentEmailListInParent',
    'Utils',
    function (
      $scope,
      $sce,
      findCurrentEmailListInParent,
      Utils
    ) {

      var currentList = findCurrentEmailListInParent.plain();
      var subscribeUrl = "/api/v1/email_lists/subscribe/" + currentList.secure_key;
      var fullSubscribeUrl = Utils.httpProtocolUrlFor(subscribeUrl);

      $scope.subscribe_url = $sce.trustAsResourceUrl(fullSubscribeUrl);
      $scope.thank_you_page = currentList.thank_you_page_url;
      $scope.already_subscribed_page = currentList.already_subscribed_url;
    }
  ])

Adding the funcionality to add subscribers through the embedded form:

First, let’s add the subscribers table through a new brand migration:

# for now we just need these three attributes
rails g model Subscriber name email email_list_id:integer
# rake db:migrate

Let’s create the association between EmailList and Subscriber

# app/models/email_list.rb
has_many :subscribers

Additionally, we are going to add some model validation rules.

# app/models/subscriber.rb
class Subscriber < ActiveRecord::Base
  belongs_to :email_list

  validates_presence_of :email,
                        :email_list_id

  validates_format_of :email, :with => /\A[^@]+@([^@\.]+\.)+[^@\.]+\z/

end

Whether you’re logged in or not, we need to declare an endpoint to accept a request that adds subscribers to your list:
let’s open up the file app/controllers/api/v1/email_lists_controller.rb and include the following content as a reference:

class Api::V1::EmailListsController < Api::BaseController
  respond_to :html, :json, :xml
  before_action :find_email_list, only: :add_public_subscriber
  .
  .
  .
  .
  def add_public_subscriber
    if @email_list
      valid_email_list
    else
      @errors = 'Invalid email list'
    end
  end

  private

    def valid_email_list
      email = params['subscribe_email']
      name = params['subscribe_name_input']
      redirect_subscribed = params['already_subscribed_page']

      subscribe_params = {
        email_list_id: @email_list.try(:id),
        name: name,
        email: email
      }

      if Subscriber.where(email: email).exists?
        redirect_to redirect_subscribed
      else
        handle_subscriber_creation(subscribe_params)
      end
    end

    def handle_subscriber_creation(values)
      redirect = params['thank_you_page']

      subscriber = Subscriber.create(values)

      if subscriber.valid?
        redirect_to redirect
      else
        @errors = 'Email is required'
      end
    end

    def find_email_list
      uuid = params['email_uuid']
      @email_list = EmailList.find_by(secure_key: uuid)
    end

The public view looks as simple as:

# app/views/api/v1/email_lists/add_public_subscriber.html.haml
!!!
%html{:lang => "en"}
  %head
    %title Email Autoresponder
  %body.preload
    %main
      .error-wrapper
        .error-box
          .error-img
            = image_tag 'icon-warning.png'
          .error-copy
            %h2#error-heading
            %p#error-text
              = @errors
    %span#pingdom-check

As you notice, I’m using an image; you can use it or remove it. I’m going to include it in app/assets/images/ in the branch for part-3

Finally, it is time for declaring the new route, and the complete config/routes.rb looks something like:

# config/routes.rb
  constraints do
    namespace :api, path: '/api' do
      namespace :v1 do
        resources :email_lists do
          collection do
            post 'subscribe/:email_uuid', to: 'email_lists#add_public_subscriber'
          end
        end
      end
    end
  end

Don’t forget to restart your server after making those modifications to your routes.

At this point this is what you should see so far:

signup form settings

signup form settings

Publishing signup form

Publishing signup form

So, if you embed your signup form using jsfiddle for instance and submit the form. It should look like this:

https://jsfiddle.net/heridev/5qtmp7pL/

As we want to support cross domain requests, we’re implemeting CORS to the application:

Let’s open up our Gemfile and include the gem called rack-cors:

# Gemfile
gem 'rack-cors', require: 'rack/cors'

Install dependencies:

bundle install

Let’s set up our config/application.rb file:

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module AngularRailsAutoresponder
  class Application < Rails::Application
    config.active_record.raise_in_transactional_callbacks = true

    config.middleware.insert_before 0, "Rack::Cors", debug: true, logger: (-> { Rails.logger }) do
      allow do
        origins '*'
        resource '*',
          headers: :any,
          methods: [:post],
          max_age: 0
      end
    end
  end
end

Now, we are supporting CORS.

All the changes we made in this tutorial are included in the following pull request:

https://github.com/heridev/MailAssemble/pull/3

That’s enough for the post number three. Wait for the next part in which we are going to learn about: Communication between components using events propagation. We’ll implement the undo action by using PaperTrail for versioning when deleting subscribers and a lot more interesting Angular stuff; so stay tuned and as always, thanks for reading.

H.

Community
De Código, Café y Cervezas 06 – ActiveModel::Serializer
Beginner
Administrate review
Best Practices
De Código, Café y Cervezas 07 – ¿Somos profesionales?