Created
August 25, 2012 20:34
-
-
Save victorbstan/3470717 to your computer and use it in GitHub Desktop.
Angular JS and Rails 3 Infinite Scroll Pagination
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- | |
This is the HTML portion of the infinite scroll/pagination with Rails and AngularJS tutorial | |
Required libraries for the tutorial and indicated as "Important", | |
together with implementation specific libraries marked as "Optional" | |
--> | |
<!DOCTYPE html> | |
<html> | |
<title>Clips</title> | |
<!-- Important --> | |
<script src="lib/jquery.js" type="text/javascript"></script> | |
<!-- Optional --> | |
<script src="lib/underscore.js" type="text/javascript"></script> | |
<script src="lib/underscore.string.min.js" type="text/javascript"></script> | |
<!-- Important --> | |
<script src="lib/angular.js" type="text/javascript"></script> | |
<!-- Optional --> | |
<script src="lib/ng_sanitize.js" type="text/javascript"></script> | |
<!-- Important --> | |
<script src="lib/moment.js" type="text/javascript"></script> | |
<!-- Optional --> | |
<script src="lib/urlize.js" type="text/javascript"></script> | |
<script src="lib/truncate.js" type="text/javascript"></script> | |
<!-- Important: Application files --> | |
<script src="filters.js" type="text/javascript"></script> | |
<script src="clips.js" type="text/javascript"></script> | |
</head> | |
<!-- hook AngularJS into your HTML by declaring the application name and controller name --> | |
<!-- if you plan on using the application as a Chrome extension, you need to add "ng-csp" --> | |
<body ng-app="clipper" ng-controller="clips_controller" ng-csp> | |
<!-- snip... --> | |
<!-- AngularJS module knows about "when-scrolled" and "load_data()" is defined in "clips.js" --> | |
<div id="clips-list" when-scrolled="load_data()"> | |
<!-- AngularJS templating makes use of interpolation of "{{...}}" brackets --> | |
<!-- You can defined pre-processing filters, i.e.: "filter:query", | |
if you need to do additional computation on the output data --> | |
<!-- "ng-repeat" indicated to AngularJS that this bit part of the HTML template can be repeated --> | |
<div class="clip-content-wrap {{clip.source}}" ng-repeat="clip in clips | filter:query"> | |
<div class="clip-content" data-id="{{clip.id}}"> | |
<span ng-bind-html="clip.body | truncate | urlize"></span> | |
</div> | |
<div class="clip-time-well"> | |
<!-- Here I use a filter for the date value --> | |
<small>{{clip.created_at | time_ago_in_words}}</small> | |
</div> | |
</div> | |
</div> | |
<!-- snip... --> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This is the JS portion of the AngularJS and Rails infinite scroll tutorial (by making use of pagination) | |
// SETUP | |
// development | |
var DOMAIN = 'http://localhost'; | |
var PORT = 3000; | |
// API endpoints | |
// If your application has routes set up in the 'classical' CRUD interface, | |
// and you respond to .json in your controllers, this should look familiar. | |
var DATA_SOURCE = '/clips.json'; | |
var DATA_INDEX = '/clips/'; | |
// ANGULAR JS | |
// App | |
Clipper = {}; | |
// From the AngularJS website: | |
// Controllers are the behavior behind the DOM elements. | |
// AngularJS lets you express the behavior in a clean readable | |
// form without usual boilerplate of updating the DOM, | |
// registering callbacks or watching model changes. | |
function clips_controller($scope, $http) { | |
var scope = $scope; | |
var http = $http; | |
// format date | |
// implementation detail, can skip | |
var now = new Date(); | |
now = moment(now.getTime()); | |
Clipper.data_collection = []; | |
// processing data for output, | |
// implementation detail, can skip | |
Clipper.flatten_data_collection = function(){ | |
var result = _.flatten(this.data_collection); | |
return result; | |
}; | |
// sort data for output | |
// implementation detail, can skip | |
Clipper.sort_by_created_at = function(data){ | |
var sorted_data = data.sort(function(a,b){ | |
var date_a = moment(a.created_at).unix(); | |
var date_b = moment(b.created_at).unix(); | |
return date_a - date_b; | |
}); | |
return sorted_data.reverse(); | |
}; | |
// data output to view | |
// give it to angular, it will take care of the rest... | |
Clipper.output_data = function(data_array){ | |
$scope.clips = Clipper.sort_by_created_at(data_array); | |
} | |
// default data to show in clips section when user is not logged-in, | |
// or service is not accessible, ie.: before remote API call | |
$scope.clips = [ | |
{body:"Welcome! I am your first clip!", created_at:now} | |
]; | |
// page number, start at page 1 | |
// will become page parameter sent to Rails API | |
var counter = 1; | |
// infinite scroll (load next page) | |
$scope.load_data = function() { | |
// load one page at a time, | |
// strictly speaking, we don't need a loop in this app implementation, | |
// but it's a left-over from the original tutorial, which treated each call to the load_data method | |
// as a chance to load a number of individual items, changing the the 'for' loop increment limit | |
// to a higher number would allow you to batch load items. | |
// Again, in this case, changing the 'for' loop increment limit to a higher numbers would mean | |
// loading more than one page at a time; this might be desireable if you have a short number of | |
// items on each page and you want to buffer by loading more pages at a time, usually not needed. | |
for (var i = 0; i < 1; i++) { | |
// get the data from the API, put together the necessary URL | |
// use the previously defined "counter" as the 'page' parameter for the Rails API | |
$http.get(DOMAIN+":"+PORT+DATA_INDEX+'page/'+counter+'.json').success(function(data) { | |
// add API response data to our array of items | |
Clipper.data_collection.push(data); | |
// format for output | |
// suffice to say, here you will perform whatever action you need | |
// for output preprocessing | |
// implementation detail, change to whatever you need to do with the received data, | |
// as long as you remember to give it to AngularJS for output :) | |
Clipper.output_data(Clipper.flatten_data_collection()); | |
}); | |
counter += 1; // increment page number for next request | |
} | |
}; | |
// initial call to load data (load first page) | |
$scope.load_data(); | |
// the rest of the app... | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# This is the Rails section of the AngularJS & Rails infinite scroller/pagination tutorial | |
# This part is really simple, we just need to make sure we respond with the appropriate | |
# paginatied JSON resource when we get the API call from our AngularJS application | |
# Gems | |
gem 'will_paginate', '~> 3.0' | |
# Routes | |
get "/clips/page/:page", :controller => "clips", :action => "index" | |
resources :clips | |
# Controllers | |
# GET /clips | |
# GET /clips.json | |
# GET /clips/page/2 | |
# GET /clips/page/2.json | |
def index | |
respond_to do |format| | |
@clips = current_user.clips.page(params[:page]).order('created_at DESC') | |
format.html do | |
@clips | |
end | |
format.json do | |
render json: @clips | |
end | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Define filters and more for your AngularJS app | |
// Filters can be applied inline in your views/templates HTML to do additional view output processing | |
angular.module('clipper', ['ngSanitize']). | |
// Infinit scroll/pagination, this "directive" binds to the view | |
// in the application HTML, "when-scrolled" is hooked up to "whenScrolled" here | |
directive('whenScrolled', function() { | |
return function(scope, elm, attr) { | |
var raw = elm[0]; | |
// bind function to the 'scroll' event | |
// we listen to the 'scroll' event and calculate when to call the method attached to "when-scrolled" | |
// if you recall our HTML page, we have: when-scrolled="load_data()" | |
// in our application script we defined "load_data()" | |
elm.bind('scroll', function() { | |
// calculating the time/space continuum needed to trigger the loading of the next pagination | |
if (raw.scrollTop + raw.offsetHeight >= raw.scrollHeight) { | |
// from the AngularJS documentation: | |
// http://docs.angularjs.org/api/ng.$rootScope.Scope#$apply | |
// $apply() is used to execute an expression in angular from outside | |
// of the angular framework. (For example from browser DOM events, setTimeout, | |
// XHR or third party libraries). Because we are calling into the angular | |
// framework we need to perform proper scope life-cycle of | |
// exception handling, executing watches. | |
scope.$apply(attr.whenScrolled); | |
} | |
}); | |
}; | |
}). | |
// The rest are optional filters, | |
// implementation specific, can skip | |
filter('truncate', function() { | |
return function(input) { | |
return truncate(input, 140); | |
}; | |
}). | |
filter('time_ago_in_words', function() { | |
return function(input) { | |
return moment.utc(input).fromNow(); | |
}; | |
}). | |
filter('urlize', function() { | |
return function(input) { | |
return urlize(input, {target:"_blank"}); | |
}; | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment