Welcome back to the part #4 of “How to create your own Single Page Application Autoresponder with Rails 4 and Angular.js”.
What are we going to do in this part?
Now, as we were working on adding subscribers to the email list by using an embedded code, we are going to continue working on subscribers and specifically we are going to implement some views and logic in order to manage subscribers.
It’s worth mentioning some of the subjects that we are covering in this part:
- We are using a paper_trail gem that gives us the advantage of tracking changes to our models for auditing or versioning. This is also used to see how a model looks at any stage in it’s lifecycle, reverting it to any version, or restoring it after it has been destroyed.
- By using Paper trail versioning, we will implement the undo destroy action when deleting subscribers.
- We also are going to use the Angular event propagation when implementing the undo destroy action. In fact, we are going to create a new directive to handle that dom manipulation.
- We will add the destroy action to the email list. While we work on that, we are also implementing ngDialog in order to inform the users about the operation they are about to perform. This will ask the user to confirm by typing a special text before being able to delete that resource.
- We are implementing infinite scroll to list subscribers. This feature involves adding kaminari to control the pagination.
So let’s get started managing all the subscribers email lists.
I would like to start by adding all the code required from the backend side and then focus on the frontend side:
As you read in the introduction, we are using Kaminari for pagination and paper_trail to control the versions which will be able to undo actions. Let us add them to the Gemfile:
# Gemfile
gem 'kaminari'
gem 'paper_trail', '~> 4.0.0'
Install dependencies
bundle install
We’ll install paper_trail running to the command bellow:
bundle exec rails generate paper_trail:install
In this case, we’d want to save only 4 versions for each record to setup, we can create an initializer and set that up for paper_trail:
# config/initializers/paper_trail.rb
PaperTrail.config.version_limit = 3
We are using metadata when saving data in paper_trail so we can save custom fields for specific models. The information that we would want to save is the email_list_id. We can add this column when modifying the existent migration generated by paper_trail:install or to create a new migration. I would recommend choosing the second option:
rails g migration add_email_list_id_to_versions email_list_id:integer
Now, we run the new migrations that we just added:
bundle exec rake db:migrate
Here are the new routes that we’ll use:
# config/routes.rb
# add this routes inside the :v1 namespace do
resources :subscribers do
collection do
post 'validate_email', to: 'subscribers#validate_email'
post 'validate_email_uniqueness', to: 'subscribers#validate_email_uniqueness'
end
end
resource :versions, only: :none do
collection do
post 'undo', to: 'versions#undo'
end
end
Let’s add uniqueness validation for email subscribers:
# app/models/subscriber.rb
validates :email, uniqueness: { scope: :email_list_id }
# code to start versioning all the operations
# For tracking versions by using meta data, we can make some queries like
# PaperTrail::Version.where(:email_list_id => email_list_id)
# For example, to restablish all the subscribers for an email list
# when using meta data, do not forget to create a new migration for it
has_paper_trail meta: { email_list_id: :email_list_id }
In this case, when we want to destroy the email list, we would destroy all the subscribers that belong to that email list. We are modifying the association between email_list and subscriber below:
# app/models/email_list.rb
has_many :subscribers, dependent: :destroy
Now the subscribers controller might look like this:
# app/controllers/api/v1/subscribers_controller.rb
class Api::V1::SubscribersController < Api::BaseController
before_action :find_email_list
before_action :find_subscriber, except: [:index, :create, :validate_email_uniqueness]
PER_PAGE_RECORDS = 100
def index
page_count = params[:page_count].try(:to_i) || 1
subscribers = @email_list.subscribers
.page(page_count)
.per(PER_PAGE_RECORDS)
.order('created_at DESC')
serialized_data = serialize_data(subscribers,
SubscriberSerializer,
root: false)
render_json_dump(subscribers: serialized_data,
page_count: page_count,
tot_subscribers: subscribers.total_count,
tot_pages: subscribers.total_pages)
end
def create
subscriber = @email_list.subscribers.create(subscriber_params)
if subscriber.valid?
render_serialized(subscriber, SubscriberSerializer, root: false)
else
render_error_object(subscriber.errors.messages)
end
end
def show
render_serialized(@subscriber, SubscriberSerializer, root: false)
end
def update
if @subscriber.update(subscriber_params)
render_serialized(@subscriber, SubscriberSerializer, root: false)
else
render_error_object(@subscriber.errors.messages)
end
end
def destroy
if @subscriber.destroy
version = @subscriber.versions.last
render_serialized(version, VersionSerializer, root: false)
else
render_with_error('An error occured while deleting the subscriber')
end
end
def validate_email_uniqueness
already_exists = @email_list.subscribers
.where(email: params[:email])
.exists?
if already_exists
render_with_error('That email is already added in the list')
else
render_json_message('Email list was deleted successfully')
end
end
def validate_email
already_exists = @email_list.subscribers
.where(email: params[:email])
.where("id #{@subscriber.id}")
.exists?
if already_exists
render_with_error('That email is already added in the list')
else
render_json_message('Email list was deleted successfully')
end
end
private
def find_email_list
uuid = params['email_uuid']
@email_list = EmailList.find_by(secure_key: uuid)
end
def find_subscriber
id = params['id']
@subscriber = @email_list.subscribers.find(id)
end
def subscriber_params
params.require(:subscriber)
.permit(
:name,
:email
)
end
end
As you may have noticed, we are using the SubscriberSerializer – so let’s declare it:
# app/serializers/subscriber_serializer.rb
class SubscriberSerializer < ActiveModel::Serializer
attributes :name,
:created_at,
:id,
:email
def created_at
I18n.localize object.created_at, format: :subscriber_since
end
end
For the created_at field, we are using a translation in order to format the created_at date, so let’s add the format in our locales yaml file:
# config/locales/en.yml
en:
time:
am: am
formats:
default: "%a, %d %b %Y %H:%M:%S %z"
long: "%B %d, %Y %H:%M"
short: "%d %b %H:%M"
subscriber_since: ! '%d %b %H:%M, %H:%M hrs'
Implementation of undo actions
# app/controllers/api/v1/versions_controller.rb
class Api::V1::VersionsController < Api::BaseController
before_action :find_element_version
def undo
if is_user_creator?
@current_version.reify.save!
render_json_message('Undo was applied successfully')
else
activerecord_not_found
end
end
private
def is_user_creator?
@current_version.whodunnit.to_i == current_user.id
end
def find_element_version
@current_version = PaperTrail::Version.find(params[:id])
end
end
The code above uses the VersionSerializer, and it looks like this:
# app/serializers/version_serializer.rb
class VersionSerializer "{ 'active': $state.includes('email_list.details.form') }"}
%a{:href => "#", 'ui-sref' => 'email_list.details.form({ emailListId: email_list.id })'}
Email list details
%li{'ng-class' => "{ 'active': $state.includes('email_list.details.signup_forms') }"}
%a{:href => "#", 'ui-sref' => 'email_list.details.signup_forms.settings({ emailListId: email_list.id })'}
Sign up forms
%li{'ng-class' => "{ 'active': $state.includes('email_list.details.subscribers') }"}
%a{:href => "#", 'ui-sref' => 'email_list.details.subscribers.list({ emailListId: email_list.id })'}
Manage subscribers
%li{'ng-class' => "{ 'active': $state.includes('email_list.details.follow_ups') }"}
%a{:href => "#", 'ui-sref' => 'email_list.details.follow_ups({ emailListId: email_list.id })'}
Follow ups
%li{'ng-class' => "{ 'active': $state.includes('email_list.details.stats') }"}
%a{:href => "#", 'ui-sref' => 'email_list.details.stats({ emailListId: email_list.id })'}
Stats
%li{'ng-class' => "{ 'active': $state.includes('email_list.details.subscribers.add') }"}
%a{:href => "#", 'ui-sref' => 'email_list.details.subscribers.add({ emailListId: email_list.id })'}
Add subscribers
%ui-view
Now, we’ll update the email list template with the new updated states:
Additionally, we are changing the markup and adding a new link that is the responsible of destroying the email list. We’ll implement that feature later on this tutorial.
// app/assets/javascripts/autoresponder/components/email_lists/index.html.haml
#email-lists
.row
%button{class: 'btn btn-success pull-right', 'ui-sref' => 'email_list.add'}
Create List
.row{class: 'span12', "ng-repeat" => "list in email_list"}
.col-md-5
%h4
{{list.name}}
%p {{list.created_at}}
.col-md-4
.data.inline-block{"aria-hidden" => "true"}
%h4 0
%p.dim-el Subscribers
.data.inline-block{"aria-hidden" => "true"}
%h4 0.0%
%p.dim-el Opens
.data.inline-block{"aria-hidden" => "true"}
%h4 0.0%
%p.dim-el Clicks
.col-md-3.actions
.btn-group
%button.btn.btn-default{:type => "button",
'ui-sref' => 'email_list.details.subscribers.list({ emailListId: list.id })'} Manage subscribers
%button.btn.btn-default.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-toggle" => "dropdown", :type => "button"}
%span.caret
%span.sr-only Toggle Dropdown
%ul.dropdown-menu
%li
%a{:href => "#", 'ui-sref' => 'email_list.details.signup_forms.settings({ emailListId: list.id })'} Signup Form
%li
%a{:href => "#", 'ui-sref' => 'email_list.details.follow_ups({ emailListId: list.id })'} Follow ups
%li
%a{:href => "#", 'ui-sref' => 'email_list.details.stats({ emailListId: list.id })'} Stats
%li
%a{:href => "#", 'ui-sref' => 'email_list.details.add_subscriber({ emailListId: list.id })'} Add Subscribers
%li.divider{:role => "separator"}
%li
%a{:href => "", 'ng-click' => 'openModal(list)'} Destroy
As we don’t need some files anymore, we can delete them:
rm app/assets/javascripts/autoresponder/components/email_lists/details/add_subscriber.html.haml app/assets/javascripts/autoresponder/components/email_lists/details/subscribers.html.haml
Before moving on, let’s create the new subscribers folder location:
mkdir app/assets/javascripts/autoresponder/components/email_lists/details/subscribers
Now, we can start listing the subscribers:
Here is how the subscribers list template looks like:
// app/assets/javascripts/autoresponder/components/email_lists/details/subscribers/list.html.haml
%div{"infinite-scroll" => "loadMoreSubscribers()",
'infinite-scroll-disabled' => 'inifiteScrollStatus',
"infinite-scroll-distance" => "0"}
.row.subscribers-list
.col-md-4{"ng-repeat" => "subscriber in subscribers"}
.subscriber-element{'ng-click' => 'goToSubscriberDetails(subscriber.id)'}
%p.email
{{ subscriber.email }}
%p.name
{{ subscriber.name }}
Here’s the subscribers controller:
# app/assets/javascripts/autoresponder/components/email_lists/details/subscribers/subscribersCtrl.js
AutoresponderApp
.controller('subscribersCtrl', [
'$scope',
'findCurrentEmailListInParent',
'$location',
'SubscriberService',
function (
$scope,
findCurrentEmailListInParent,
$location,
SubscriberService
) {
var initialSetup = function(){
$scope.page_number = $scope.$stateParams.pageNumber || 1;
$scope.subscribers = [];
$scope.loading_subscribers = false;
// we are initializing this value as false
$scope.inifiteScrollStatus = true;
}
initialSetup();
var loadEmailListSubscribers = function() {
if($scope.loading_subscribers) { return };
$scope.loading_subscribers = true;
var _data = {
email_uuid: findCurrentEmailListInParent.secure_key,
page_count: $scope.page_number
}
return SubscriberService.findAll(_data).then(function(successResponse) {
$scope.loading_subscribers = false
$scope.subscribers = $scope.subscribers.concat(successResponse.subscribers);
$scope.subscribers_count = successResponse.tot_subscribers;
$scope.page_number = successResponse.page_count + 1;
$scope.tot_pages = successResponse.tot_pages;
$scope.inifiteScrollStatus = false;
});
};
$scope.loadMoreSubscribers = function() {
if(showLoadMore()) {
loadEmailListSubscribers();
}
};
var showLoadMore = function() {
return ($scope.page_number "SubscriberAddForm",
"ng-submit" => "addSubscriber()",
:novalidate => ""}
.row
.col-md-4
.form-group
%label{:for => "name"} Name
%input.form-control{:type => "text",
'required' => 'required',
:placeholder => 'Full name',
'name' => 'name',
"ng-model" => 'subscriber.name'}/
.form-group{'ng-class' => "{ 'has-error': SubscriberAddForm.email.$dirty && SubscriberAddForm.email.$invalid }" }
%label{:for => "already-subscribed"} Email
%input.form-control{:type => "email",
'required' => 'required',
'ensure-unique-email-by-list' => "",
:placeholder => 'email',
'name' => 'email',
'ng-model' => 'subscriber.email'}/
.errors{"ng-show" => "SubscriberAddForm.email.$dirty && SubscriberAddForm.email.$invalid"}
%span.help-block{"ng-show" => "SubscriberAddForm.email.$error.unique"} That email is already added in the list
.row
.col-md-6
%button{ type:'submit',
'ng-disabled' => "SubscriberAddForm.$invalid",
class: 'btn btn-success'}
Add Subscriber
Here’s what the addSubscriberCtrl looks like:
# app/assets/javascripts/autoresponder/components/email_lists/details/subscribers/addSubscriberCtrl.js
AutoresponderApp
.controller('addSubscriberCtrl', [
'$scope',
'findCurrentEmailListInParent',
'$location',
'Render',
'SubscriberService',
function (
$scope,
findCurrentEmailListInParent,
$location,
Render,
SubscriberService
) {
$scope.subscriber = {};
$scope.addSubscriber = function() {
var _data;
_data = {};
_data.subscriber = $scope.subscriber;
_.extend(_data, { email_uuid: findCurrentEmailListInParent.secure_key });
SubscriberService.create(_data).then(function(list) {
Render.showGrowlNotification('success', 'The subscriber was created successfully');
return $scope.$state.go('^.list');
}, function(errorResponse) {
return Render.showGrowlNotification('warning', 'An error occurred while creating the subscriber');
});
};
}
])
When creating a new subscriber, we should validate that the email is not already taken in that email list. To do that, we validate the email uniqueness by using a custom directive called “ensureUniqueEmailByList”:
// app/assets/javascripts/autoresponder/components/email_lists/details/subscribers/ensureUniqueEmailByListDirective.js
AutoresponderApp
.directive(
'ensureUniqueEmailByList',
['SubscriberService',
function(SubscriberService) {
return {
require: 'ngModel',
link: function($scope, ele, attrs, c) {
function validateEmailUnique() {
if(this.last) {
var _data = {
email_uuid: $scope.email_list.secure_key,
subscriber_id: $scope.subscriber.id,
email: $scope.subscriber.email
}
SubscriberService.validateEmailUnique(_data).then(function(list) {
c.$setValidity('unique', true);
}, function(errorResponse) {
c.$setValidity('unique', false);
});
}
}
$scope.$watch(attrs.ngModel, validateEmailUnique);
}
}
}])
So, if we try to add the same email in the email list we should see this error:
Now, it is time to implement the Subscriber detail page.
Inside the subscriber details page, we’ll add a button to delete the current subscriber. However, we want to avoid the pop up confirmation by using undo notifications instead.
Preferring undos instead of prompting for confirmation actions.
Applications that uses undos respect the initial human intent by allowing the action. Prompting messages on the other hand suggests by default that the users do not know what they’re doing. It questions them by saying “Are you sure to…”. Most of the time those actions are intended and only a couple of them occur accidentally. The inefficiency of prompting messages is visible when the users are doing the same action multiple times, and then prompts will be shown numerously over and over.
That being said, do we want to use undos in our application? The answer is yes, but first, let us prepare the Angular application to support notifications and let’s show them the center position of the main header:
Let’s start by doing some changes to the application.html.haml layout. Here, we are modifying the header columns by adding a directive to handle notifications. This is how it looks after applying the new modifications:
// app/views/layouts/application.html.haml
!!!
/[if lt IE 7]
/[if IE 7]
/[if IE 8]
/ [if gt IE 8]> "", 'ng-app' =>"AutoresponderApp", 'ng-controller' => "mainCtrl"}
/ "utf-8"}/
%meta{:content => "IE=edge,chrome=1", "http-equiv" => "X-UA-Compatible"}/
%title
%meta{:content => "", :name => "description"}/
%meta{:content => "width=device-width, initial-scale=1", :name => "viewport"}/
%link{:href => "apple-touch-icon.png", :rel => "apple-touch-icon"}/
= stylesheet_link_tag "application", media: "all"
= javascript_include_tag "application", "data-turbolinks-track" => true
= csrf_meta_tags
%body
/[if lt IE 8]
<p class="browserupgrade">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
.navbar.navbar-inverse.navbar-custom.navbar-fixed-top{:role => "navigation"}
.container
.row
.col-md-4
%button.navbar-toggle{"data-target" => ".navbar-collapse", "data-toggle" => "collapse", :type => "button"}
%span.sr-only Toggle navigation
%span.icon-bar
%span.icon-bar
%span.icon-bar
%a.navbar-brand{:href => "#/"} MailAssemble
.col-md-4
.div#system-notifications{'ng-show' => 'undo_action', 'undo-notification' => ''}
%p
%b The {{ message_notification }} has been deleted
%a{'href' => '', 'ng-click' => 'undoAction()'}
undo action
.col-md-4
#main-menu.collapse.navbar-collapse
%ul.nav.navbar-nav.navbar-right
%li
%a{:href => "/#email_list"} Manage lists
%li.dropdown
%a.dropdown-toggle{"data-toggle" => "dropdown", :href => "#"}
Manage lists
%b.caret
%ul.dropdown-menu
%li
%a{:href => "/#email_list"} Manage lists
%li
%a{:href => "/#email_list/add"} Create a list
-if user_signed_in?
%li
= link_to('Log Out', destroy_user_session_path, :method => :delete)
/ /.nav-collapse
.container{:style => "padding-top: 60px"}
%div{:growl => ""}
%ui-view
= yield
We are using the “mainCtrl” which is the highest controller (hierarchically), and the included code for now should look like:
// app/assets/javascripts/autoresponder/components/mainCtrl.js
AutoresponderApp
.controller('mainCtrl', [
'$scope',
'Render',
'versionControlService',
function (
$scope,
Render,
versionControlService
) {
// hiden div notification by default
$scope.undo_action = false;
$scope.$on('version:destroyed', function(evt, next, current) {
// listening to events, this one is called
// within the subscribersCtrl in deleteSubscriber method
// we save the value that we want to use
// in the undoAction
$scope.currentVersion = next;
// we are listening to this event in the undoNotification directive
// so we can show hide the elements
$scope.$emit('version:send_notification');
});
$scope.undoAction = function() {
versionControlService.undo($scope.currentVersion.id).then(function(version) {
// After undoing the action let's hide the element
$scope.undo_action = false;
Render.showGrowlNotification('success', 'The undo action was perfomed successfully');
var state = versionControlService.handleRedirectState($scope.currentVersion);
$scope.$state.go(state, {}, { reload: true });
}, function(errorResponse) {
return Render.showGrowlNotification('warning', 'An error occurred while deleting the subscriber');
});
}
}
])
As you can see above, we are using $emit. This is a good moment to talk a little bit about event propagation in Angular.
Let’s start by defining what are the angular events.
Just like browsers respond to browser-level events, such as a mouse hover or mouse click, Angular can respond appropiately to Angular events. This feature gives us the advantage of being able to communicate between components, controllers, directives and so on. By design we can only listen for Angular events.
Since scopes are hierarchical, we can pass events up and down the scope. For instance, if we want to communicate the entire system from top – bottom, we would want to use a broadcast downwards. But, what if what we wanted is to communicate from child to parent scopes? We can use $emit to pass an event upwards.
Even though the events are powerful, is a good idea to limit the number of notifications sent on a global level.
How to use $emit()?
We’ll use $emit() when we want to send an event from child scopes to parent scopes.
Listening for events
For listening, we can make use of the $on() method, which will register a listener for a particular event name.
For example, we can listen for the event that is fired when the route resolves successfully:
scope.$on(‘$stateChangeSuccess’, function(evt, next, current) {
// the route has resolved successfully
});
At this point, we are using just $emit() but later on in this tutorial we are going to include some code in order to listen when the event “version:send_notification” changes, so for this moment we’ll use $on.
Now, we are going to declare a new service for handling request for PaperTrail::Version:
// app/assets/javascripts/autoresponder/components/common/versionControlService.js
AutoresponderApp
.factory('versionControlService', ['Restangular',
function(
Restangular
) {
var model;
model = 'api/v1/versions';
return {
undo: function(versionId) {
var _params = {
id: versionId
}
return Restangular.all(model + '/undo').post(_params);
},
handleRedirectState: function(version) {
var state;
switch (version.item_type) {
case "Subscriber":
state = 'email_list.details.subscribers.list';
break;
case "EmailList":
state = 'email_list.details.subscribers.list';
break;
default:
state = ''
}
return state;
}
};
}
]);
and we are removing the content from users/index.html.haml view:
// app/views/users/index.html.haml
//empty view
// for more info take a look at the application.html.haml
Showing details for subscribers:
Content for the main template that shows the different options for an individual subscriber:
// app/assets/javascripts/autoresponder/components/email_lists/details/subscribers/show.html.haml
.row
.email-list-details.col-md-3.col-md-offset-9
%ul.nav-menu.text-right
%li{'ng-class' => "{ 'active': $state.includes('email_list.details.subscribers.list') }"}
%a{:href => "#", 'ui-sref' => '^.list'}
List all
%li{'ng-class' => "{ 'active': $state.includes('email_list.details.subscribers.show.details') }"}
%a{:href => "#", 'ui-sref' => '^.show.details'}
Details
%li{'ng-class' => "{ 'active': $state.includes('email_list.details.subscribers.show.activity') }"}
%a{:href => "", 'ui-sref' => '^.show.activity'}
Activity
.row
%ui-view
Template that shows the subscriber details:
// app/assets/javascripts/autoresponder/components/email_lists/details/subscribers/show.details.html.haml
.row.subscriber-details
.row
.image
= image_tag 'default-profile.png', :width => "84"
.body
%h4 {{ subscriber.email }}
%h5 {{ subscriber.name }}
%p Subscribed: {{subscriber.created_at}}
%button{ type:'submit',
"ng-click" => "deleteSubscriber()",
class: 'btn btn-danger'}
Delete Subscriber
.row
.col-md-12
%h4.title
Subscriber Details
%button{ type:'submit',
"ng-click" => "editableForm.$show()",
class: 'btn btn-success'}
Edit
%form{"editable-form" => "", :name => "editableForm", :onaftersave => "updateSubscriber()"}
.row
.col-md-6
%h4 Email Address
%p.fields{'ng-show' => '!editableForm.$visible'}
{{ subscriber.email }}
%p.fields{'ng-show' => 'editableForm.$visible'}
%span{"e-name" => "email",
'onbeforesave' => "validateEmail($data)",
"e-required" => "",
"editable-text" => "subscriber.email"}
.col-md-6
%h4 Name
%p.fields{'ng-show' => '!editableForm.$visible'}
{{ subscriber.name }}
%p.fields{'ng-show' => 'editableForm.$visible'}
%span{"e-name" => "name", "editable-text" => "subscriber.name"}
.row{'ng-show' => 'editableForm.$visible'}
.col-md-12
%div
/ button to show form
%button.btn.btn-default{"ng-click" => "editableForm.$show()", "ng-show" => "!editableForm.$visible", :type => "button"}
Edit
/ buttons to submit / cancel form
%span{"ng-show" => "editableForm.$visible"}
%button.btn.btn-primary{"ng-disabled" => "editableForm.$waiting", :type => "submit"}
Save
%button.btn.btn-default{"ng-click" => "editableForm.$cancel()", "ng-disabled" => "editableForm.$waiting", :type => "button"}
Cancel
Now, if we click on a subscriber this is how it looks so far:
What you can see here is that the margins seem like they are wrong. To fix that we are going to use the railstrap gem. When we installed it, the file app/assets/stylesheets/bootstrap_and_overrides.css.les was created, so we can override some boostrap styles. In this case it is the class .row.
First, we need to include that file to the application.js file at the bottom:
# app/assets/stylesheets/application.css
*= require bootstrap_and_overrides
and the content that we’d add looks like:
// app/assets/stylesheets/bootstrap_and_overrides.css.less
.row {
margin-left: 0px;
margin-right: 0px;
}
As you see, we are using an image in the template that is located in:
app/assets/images/default-profile.png
And for what is missing for now, I’ll add it for you in the pull request in this tutorial so you can download it.
Continuing with the subscriber details section templates for now, let’s just declare an empty template for subscriber activity:
// app/assets/javascripts/autoresponder/components/email_lists/details/subscribers/show.activity.html.haml
%h1
Activity
Here’s what the subscriberDetails controller looks like:
# app/assets/javascripts/autoresponder/components/email_lists/details/subscribers/subscriberDetailsCtrl.js
AutoresponderApp
.controller('subscriberDetailsCtrl', [
'$scope',
'Render',
'$q',
'$http',
'findCurrentEmailListInParent',
'$location',
'SubscriberService',
function (
$scope,
Render,
$q,
$http,
findCurrentEmailListInParent,
$location,
SubscriberService
) {
var data, subscriberId;
_data = {
email_uuid: findCurrentEmailListInParent.secure_key
};
subscriberId = $scope.$stateParams.subscriberId;
var loadSubscriber = function() {
return SubscriberService.findOne(subscriberId, _data).then(function(subscriber) {
$scope.subscriber = subscriber;
});
};
loadSubscriber();
$scope.updateSubscriber = function() {
var _data;
_data = {};
_data.subscriber = $scope.subscriber.plain();
_.extend(_data, {email_uuid: findCurrentEmailListInParent.secure_key});
SubscriberService.update(_data, $scope.subscriber.id).then(function(subscriber) {
Render.showGrowlNotification('success', 'The subscriber was updated successfully');
return $scope.subscriber = subscriber;
}, function(errorResponse) {
return Render.showGrowlNotification('warning', 'An error occurred while updating the subscriber');
});
};
$scope.deleteSubscriber = function() {
var _data;
_data = {};
_data = {
email_uuid: findCurrentEmailListInParent.secure_key,
id: $scope.subscriber.id,
}
SubscriberService.destroy($scope.subscriber.id, _data).then(function(version) {
// using emit to comunicate events
// from children to parents
// if you want to use from parents to children
// use broadcast instead
$scope.$emit('version:destroyed', version.plain());
Render.showGrowlNotification('success', 'The subscriber was deleted sucessfully');
$scope.$state.go('email_list.details.subscribers.list');
}, function(errorResponse) {
return Render.showGrowlNotification('warning', 'An error occurred while deleting the subscriber');
});
};
$scope.validateEmail = function(data) {
var _data = {};
_data = {
email_uuid: findCurrentEmailListInParent.secure_key,
id: $scope.subscriber.id,
email: data
}
var d = $q.defer();
SubscriberService.validateEmailSubscriberUpdate(_data).then(function(successResponse) {
d.resolve()
}, function(errorResponse) {
d.reject(errorResponse.data.error);
});
return d.promise;
}
}
])
Now, we are able to see the subscriber details and actually we can delete it.
But we are not able to edit the details for that subscriber, so we are going to implement the subscriber edition using edition in place with angular-xeditable directive.
Let’s start adding the gem to the Gemfile inside the rails assets group:
gem 'rails-assets-angular-xeditable', '= 0.1.8'
Install dependencies:
bundle install
Include it in the application.js file:
// app/assets/javascripts/application.js
//= require angular-xeditable
Include the angular dependency:
// app/assets/javascripts/autoresponder/app.js
'xeditable',
And if you want to have control on the styles used in xeditable, you can copy the based css styles so you can modify them based on your needs:
/*app/assets/stylesheets/modules/forms.scss*/
/*!
angular-xeditable - 0.1.8
Edit-in-place for angular.js
Build date: 2014-01-10
*/
.editable-wrap {
display: block;
display: inline-block;
margin: 5px 0px 10px 0;
width: 80%;
}
.editable-wrap .editable-controls,.editable-wrap .editable-error {
margin-bottom: 0;
color: red;
}
.editable-wrap .editable-controls>input,.editable-wrap .editable-controls>select,.editable-wrap .editable-controls>textarea {
margin-bottom:0
}
.editable-wrap .editable-input {
display:inline-block;
width: 100%;
height: 34px;
padding: 6px 12px;
font-size: 14px;
line-height: 1.42857143;
color: #555;
background-color: #fff;
background-image: none;
border: 1px solid #ccc;
}
.editable-buttons {
display:inline-block;
vertical-align:top
}
.editable-buttons button {
margin-left:5px
}
.editable-input.editable-has-buttons{
width:auto
}
.editable-bstime .editable-input input[type=text]{
width:46px
}
.editable-bstime .well-small{
margin-bottom:0;
padding:10px
}
.editable-range output{
display:inline-block;
min-width:30px;
vertical-align:top;
text-align:center
}
.editable-color input[type=color]{
width:50px
}
.editable-checkbox label span,.editable-checklist label span,.editable-radiolist label span {
margin-left:7px;
margin-right:10px
}
.editable-hide{
display:none!important
}
.editable-click,a.editable-click {
text-decoration:none;
color:#428bca;
border-bottom:dashed 1px #428bca
}
.editable-click:hover,a.editable-click:hover {
text-decoration:none;
color:#2a6496;
border-bottom-color:#2a6496
}
.editable-empty,.editable-empty:hover,.editable-empty:focus,a.editable-empty,a.editable-empty:hover,a.editable-empty:focus {
font-style:italic;
color:#D14;
text-decoration:none
}
Let’s include the forms styles in the manifest file:
/*app/assets/stylesheets/main.scss*/
@import 'modules/forms';
Now, if we edit a subscriber, it should look like:
So, finally, it is time to start working with undo deletion messages. Let’s start adding some styles for notifications mesages:
/*app/assets/stylesheets/main.scs*/
#system-notifications {
background-color: #f9edbe;
border: 1px solid #f0c36d;
font-size: 13px;
height: 28px;
margin-top: 12px;
padding: 3px 0px 0px 0px;
text-align: center;
a {
text-decoration: underline;
}
}
Let’s implement the directive responsible for showing/hidding undo actions notifications messages:
// app/assets/javascripts/autoresponder/components/common/undoActionNotificationDirective.js
AutoresponderApp
.directive('undoNotification', [ '$timeout', function($timeout) {
var timeoutId;
var decorateLabelType = function(type) {
var object_title;
switch (type) {
case "Subscriber":
object_title = 'subscriber';
break;
case "EmailList":
object_title = "email list";
break;
default:
object_title = "";
}
return object_title;
}
return {
restrict: 'A',
link: function( $scope, elem, attrs) {
$scope.$on('version:send_notification', function(evt, next, current) {
// let's show the element
$scope.undo_action = true;
$scope.message_notification = decorateLabelType($scope.currentVersion.item_type);
// we are canceling the previous timer
// if a new one is created
// in order to avoid closing the notification
// right after deleting an element
$timeout.cancel(timeoutId);
var _this = this;
timeoutId = $timeout(function() {
$scope.undo_action = false;
}, 10000);
});
}
};
}])
Now if we delete a subscriber, we are able to undo that action (yaay!), so you should see something like this after deleting a subscriber:
What’s next?,
In the next and last section, we will implement the destroy option for email lists, and should look like this:
In order to implement that feature, we are using the ngDialog directive.
Let’s start by adding that library to the project:
We need to add that dependency in our Gemfile:
source 'https://rails-assets.org' do
gem 'rails-assets-ngDialog'
end
# dont't forget to run bundler: bundle install
Let’s open up and require ndDialoh in the manifest file:
// app/assets/javascripts/application.js
//= require ngDialog
We also need to include css styles:
# app/assets/stylesheets/application.css
*= require ngDialog
And the dependency to the main app.js file as well:
// app/assets/javascripts/autoresponder/app.js
var AutoresponderApp = angular.module('AutoresponderApp', [
'ngDialog',
.
.
.
]);
Let’s open up the email list controller, and include the ngDialog dependency:
// app/assets/javascripts/autoresponder/components/email_lists/emailListCtrl.js
.controller('emailListCtrl', [
'$scope',
'ngDialog',
'Render',
'EmailListService',
function (
$scope,
ngDialog,
Render,
EmailListService
) {
$scope.openModal = function(current_list) {
$scope.current_list = current_list;
ngDialog.open({
template: 'autoresponder/components/email_lists/warningDestroy.html',
className: 'ngdialog-theme-default',
scope: $scope
});
};
$scope.destroyEmailList = function(emailListId) {
EmailListService.destroy(emailListId).then(function(removed_list) {
Render.showGrowlNotification('success', 'The email list was deleted sucessfully');
ngDialog.closeAll()
return $scope.$state.go('email_list.list', {}, { reload: true });
}, function(errorResponse) {
return Render.showGrowlNotification('warning', 'An error occurred while deleting the email list');
});
}
}
])
We’ll include the destroy method inside the emailService:
// app/assets/javascripts/autoresponder/components/email_lists/emailListService.js
destroy: function(emailListId) {
return Restangular.one(model, emailListId).remove();
},
The template that we want to add will show a warning message. Before deleting the email list, the user will be asked to confirm the operation by typing the word ‘DELETE’ in the input and then he’ll be able to delete the email list:
// app/assets/javascripts/autoresponder/components/email_lists/warningDestroy.html.haml
.row
.row
.col-md-12
%h2.title
Are you sure you want to delete this list?
.row
.col-md-12
%h4
All list settings, subscribers, and associated data will be deleted as well
.row
.col-md-12
%p
%strong Subscribers:
{{ current_list.subscribers_count }}
.row
.col-md-7
%label{'for' => 'delete_confirmation'}
Type DELETE to confirm
%input.form-control{ :name => "default_from_name",
:id => 'delete_confirmation',
'required' => 'required',
:placeholder => 'Type DELETE to confirm.',
'ng-model' => 'input_confirmation',
:type => "text",
:value => ""}/
%button.ngdialog-button.ngdialog-button-secondary{"ng-click" => "closeThisDialog(0)", :type => "button"} No
%button.ngdialog-button.ngdialog-button-primary{ "ng-click" => "destroyEmailList(current_list.id)",
'ng-class' => "{ 'ngdialog-button-secondary': input_confirmation != 'DELETE' }",
"ng-disabled" => "input_confirmation != 'DELETE'",
:type => "button" } Yes
There we have it
Wrapping up:
While working on the implementation of subscribers management, we have learned about communication between controllers by using event propagation and we have also learned how to listen and send those events between controllers.
This guide was to show you how easy it is to implement the undo operations by using Angular and PaperTrail, as well as the benefits of choosing this approach over the common popup confirmation messages “Are you sure?”.
As always, I hope you learned a lot from this post.
Keep in touch and wait for the part number 5 which is about the implemention of follow-ups: we will use background jobs in order to manage all the scheduled emails.
P.S: All the code that we added in this tutorial is included in this pull request:
https://github.com/heridev/MailAssemble/pull/4
H.