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:
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
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.