Tuesday, October 28, 2014
AngularJS on Rails 4 - Part 2
Let’s pick up where we left off. If you haven’t already, make sure you go through Part 1 to create your base Rails app with the API setup.
You can either continue using the code you have created on part 1 or you can catch up by checking out the tagged code:
$ git clone https://github.com/cavneb/angular_casts$ cd angular_casts$ git checkout step-1$ bundle install$ rake db:migrate; rake db:migrate RAILS_ENV=test$ rake test$ rake screencast_sync:railscastsThere are a couple of different ways we can add Angular into our application. Ryan Bates suggests using the angular-rails gem. Even though this is an excellent gem which is well maintained, it’s good to know how to do this without a gem.
In our app we are going to link our scripts using a CDN. We can find the CDN for Angular at https://ajax.googleapis.com/ajax/libs/angularjs/1.0.6/angular.min.js. We will also be adding Angular Resource via the CDN as well.
Update your layout file:
app/views/layouts/application.html.erb 12345678910111213141516We want to keep our code organized by placing our Angular controllers, filters, services, directives, etc. in the app/assets/javascripts folder. Create the following directories:
app/assets/javascripts/angular/controllersapp/assets/javascripts/angular/directivesapp/assets/javascripts/angular/servicesHere’s a shortcut to do this:
$ mkdir -p app/assets/javascripts/angular/controllers \ app/assets/javascripts/angular/directives \ app/assets/javascripts/angular/servicesNow let’s create the main javascript file which will drive our Angular application.
app/assets/javascripts/app.js.coffee window.App = angular.module('AngularCasts', ['ngResource'])In this file we create a new module called AngularCasts and assign it to window.App. We also add the dependency of ngResource which provides simple REST client functionality.
Next, we need to update our JavaScript manifest to include our Angular scripts. The order of these is important due to the latter ones depending on the ones prior to them.
app/assets/javascripts/application.js //= require app//= require_tree ./angularThis is quite a change from what exists in the manifest already. We will add jQuery later, but via CDN. You’ll see why later in this post.
Next, we need to create a controller. This will allow us to set up a route to a view.
$ rails g controller home indexThis set up the HomeController and added the action index. Before we modify this view, let’s update our layout to acts as an Angular app. This is done by adding the directive ng-app to our tag:
app/views/layouts/application.html.erb ...Now let’s update our index view with some simple Angular code:
app/views/home/index.html.erbHello {{yourName}}!
Update your routes file to use this view as the root.
config/routes.rb AngularCasts::Application.routes.draw do scope :api do get "/screencasts(.:format)" => "screencasts#index" get "/screencasts/:id(.:format)" => "screencasts#show" end root to: "home#index"endNote that the line get 'home#index' was removed. This is not needed because the root path directs to it.
Start up your server and open up http://localhost:3000. Type in your name into the text field. If the content changes as you type, it worked! You now have a functional Angular application!
In order for us to tell the page that it should use the App module, we need to add the module name to the ng-app directive. Set the value of the attribute to AngularCasts:
app/views/layouts/application.html.erb 12345678910111213141516171819Now our view knows to use the AngularCasts module.
Let’s create a controller that will be used to list out the episodes. Create a new coffeescript file at app/assets/javascripts/angular/controllers/screencasts_ctrl.js.coffee
app/assets/javascripts/angular/controllers/screencasts_ctrl.js.coffee App.controller 'ScreencastsCtrl', ['$scope', ($scope) -> $scope.message = "Angular Rocks!"]On line 1, we create a new Angular controller belonging to App named ScreencastsCtrl. The controller will be referenced in our view as ScreencastsCtrl. For more information on Angular controllers, read http://docs.angularjs.org/guide/dev_guide.mvc.understanding_controller.
Let’s update our view to display the message.
app/views/home/index.html.erbMessage: {{message}}
Here we have bound the contents of the div to the controller ScreencastsCtrl. Refresh the browser and you should see ‘Message: Angular Rocks!’.
Lets add the much needed CSS to our application. Copy the following into app/assets/stylesheets/home.css.scss.
app/assets/stylesheets/home.css.scss 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879body { font-size: 12px; font-family: Helvetica, sans-serif; background-color: #ddd; margin: 0px;}header { background-color: #4F4F4F; color: #fff; position: absolute; height: 36px; top: 0; left: 0; right: 0; font-size: 18px; line-height: 36px; font-weight: bold; padding-left: 15px;}#screencast-ctrl { background-color: #fff; position: absolute; top: 37px; width: 100%; bottom: 0; overflow: auto;}#screencast-list-container { background-color: #fff; position: absolute; min-height: 700px; width: 300px; top: 37px; left: 0; bottom: 0; overflow: auto; -webkit-overflow-scrolling: touch; ul { margin: 0px; list-style: none; padding: 0px; li { cursor: pointer; border-bottom: 1px solid #ddd; padding: 0 10px; } } h3 { font-size: 14px; small { font-size: 12px; color: #ccc; font-weight: normal; } &.active { color: red; } }}#screencast-view-container { position: absolute; border-left: 1px solid #d0d0d0; top: 37px; left: 300px; right: 0; bottom: 0; background-color: #fff; min-height: 700px; padding: 5px 25px; #player { border: 1px solid #000; max-width: 800px; }}Refresh the browser. Ooooh!
Our Angular controller is going to access the data from our API using ngResource. ngResource enables interation with RESTful server-side data sources.
Angular services are singletons that carry out specific tasks common to web apps. Services are commonly used to perform the XHR interaction with the server. To learn about the differences between services and factories, read this. Let’s start off by creating a service at screencast.js.coffee:
app/assets/javascripts/angular/services/screencast.js.coffee App.factory 'Screencast', ['$resource', ($resource) -> $resource '/api/screencasts/:id', id: '@id']Now tell the controller to use this service:
app/assets/javascripts/controllers/screencasts_ctrl.js.coffee App.controller 'ScreencastsCtrl', ['$scope', 'Screencast', ($scope, Screencast) -> $scope.screencasts = Screencast.query()]Update the index view with the following:
app/views/home/index.html.erb-
{{screencast.title}} ({{screencast.duration}})
Now refresh the page. If all worked well, you should see a list of screencasts on the left side. When we reloaded the page, a GET request was sent to /api/screencasts, populating the screencasts attribute in our scope. This is the power of Angular ngResource.
We are doing great! Now we have a list of screencasts on the side which are clickable. However, we don’t do anything when they are clicked. What we want to do is show the screencast in the main section along with some additional screencast information.
Start off by adding the HTML code which will be used to display the main content. This is done inside the index.html.erb file:
app/views/home/index.html.erb 123456789101112131415161718-
{{screencast.title}} ({{screencast.duration}})
{{selectedScreencast.title}}
{{selectedScreencast.summary}}
Published at {{selectedScreencast.published_at | date: 'mediumDate'}} - {{selectedScreencast.link}}
On lines 10-17, we have added a div which shows the screencast title and summary. On line 10, we use the ng-show directive which only displays the div if selectedScreencast exists.
Go ahead and refresh the page. You should not see any changes. Click on a screencast. Still no changes.
In order for us to show the main content with the screencast information, we need to do a few things. The first thing we need to do is add an ng-click directive to our screencast list:
app/views/home/index.html.erb ...-
{{screencast.title}} ({{screencast.duration}})
Now when the list item is clicked the function showScreencast will be triggered with the screencast being passed in to it. Now let’s update our controller with this function:
app/assets/javascripts/angular/controllers/screencasts_ctrl.js.coffee App.controller 'ScreencastsCtrl', ['$scope', 'Screencast', ($scope, Screencast) -> # Attributes accessible on the view $scope.screencasts = Screencast.query() $scope.selectedScreencast = null # Set the selected screencast to the one which was clicked $scope.showScreencast = (screencast) -> $scope.selectedScreencast = screencast]Refresh your browser and click on a screencast. As my wife would incorrectly say: “Waalah!”
After doing a bit of looking around, I found that Flow Player offered the easiest and cleanest way to show videos. Let’s add the dependent scripts and css links to our layout:
app/views/layouts/application.html.erb 12345678910111213141516171819202122Remember how we removed jQuery from our Gemfile and javascript manifest? The reason is because we didn’t want it concatenated with the other scripts. FlowPlayer depends on jQuery and so jQuery needs to be available prior to Flow Player being loaded. We also can load it via CDN. Good times for all.
FlowPlayer requires triggering a function flowplayer() to show the video. We could add this into our controller, but we love to learn. Let’s create a directive which listens to the controller and triggers the flowplayer function when showScreencast is called.
Create the directive at app/assets/javascripts/angular/directives/flow_player.js.coffee
app/assets/javascripts/angular/directives/flow_player.js.coffee 123456789101112App.directive 'flowPlayer', -> (scope, element, attrs) -> # Trigger when the selectedScreencast function is called # with a screencast scope.$watch 'selectedScreencast', (screencast) -> if screencast # See http://flowplayer.org/docs/ element.flowplayer playlist: [[mp4: screencast.video_url]] ratio: 9 / 14Now add the directive into our view:
app/views/home/index.html.erb 1234567891011...{{selectedScreencast.title}}
{{selectedScreencast.summary}}
Published at {{selectedScreencast.published_at | date: 'mediumDate'}} - {{selectedScreencast.link}}
Refresh your browser and go nuts!
One final thing that I would like to see is some sort of indicator which lets us know which video is playing on the screencast list. This can be done via CSS and some simple code.
In our CSS file, we have already added some style for an active screencast. Any H3 tag on the side with the class of active will show as red. Try it out by adding the class to our view:
app/views/home/index.html.erb ...-
{{screencast.title}} ({{screencast.duration}})
Refresh the page. You should now see that every screencast link on the left is red.
For us to make it show for the active screencast only we have to make a few changes to our view and controller. Update the view to use the ng-class directive:
app/views/home/index.html.erb 1234567891011...-
{{screencast.title}} ({{screencast.duration}})
Note that on line 5 we added the 2nd attribute &index. This is available via the ng-repeat directive and is the index value of the array (integer). We also modified line 6 to use the ng-class directive which only shows “active” if the $index is equal to $scope.selectedRow.
Now update the controller to work with these changes:
app/assets/javascripts/controllers/screencasts_ctrl.js.coffee 123456789101112131415App.controller 'ScreencastsCtrl', ['$scope', 'Screencast', ($scope, Screencast) -> # Attributes accessible on the view $scope.selectedScreencast = null $scope.selectedRow = null # Gather the screencasts and set the selected one to the first on success $scope.screencasts = Screencast.query -> $scope.selectedScreencast = $scope.screencasts[0] $scope.selectedRow = 0 # Set the selected screencast to the one which was clicked $scope.showScreencast = (screencast, row) -> $scope.selectedScreencast = screencast $scope.selectedRow = row]We have added the new param row to the showScreencast function and set this to $scope.selectedRow. Refresh your browser and see how things have changed.
View the working app at http://angular-casts.herokuapp.com/.
I know there are no front-end tests for this tutorial. This was intentional. They were too hard. I spent hours upon hours trying to get the tests working with $httpBackend and service testing, etc. I tried karma and testem. Too much time. Too little benefit. If you are better at this stuff than I am please send me an email or pull request or something with the changes you’ve made to include the tests. I can always add a Part 3 which is about nothing other than testing the frontend.
Thank you all who have patiently waited for me to finish this post. I learned never to release a post before it has been proofread and run through several times. Next time I’m going to do a screencast.. just like the old days.
No comments: