Thinkster

Now that we have the basic front-end for our Flapper News coded up with Angular, we're going to start work on our backend. Because Angular is able to handle the templating and rendering, our backend server will mainly serve as a communication layer with our database. Over the next few sections, we are going to focus on modeling our data needs and creating a REST API that our Angular app can use to interact with this data.

If you haven't already done so, make sure you have rails, and sqlite3 installed on your system. At the time of writing, Rails is currently on version 4.2.7.

Generating a new Rails app

Rails by default ships with turbolinks and jquery. We won't be needing either of those packages since we're using Angular, so let's generate a new rails app without any javascript at all using the following command:

rails new flapper-news --skip-javascript

This will create a folder flapper-news with our Rails application. If we go into the folder and run rails s you should be able to see the our Rails app running at http://localhost:3000.

The Anatomy of a Rails Project

Here's a quick overview of what each folder/file in our Rails project is:

  • app/ - This folder is where most of your application-specific code will go into.
  • bin/ - This folder contains executable scripts for your rails project. These are the files that get executed when you run bundle, rails, or rake from the command line.
  • config/ - This is for your application's configuration files. The environment folder contains environment-specific (development, production, test) configuration, and all files in the initializers folder will be run when your application boots.
  • db/ - This folder contains files for managing your database. Generated migrations will be put into the migrate folder, and a master copy of your application's database schema is contained in schema.rb, which is automatically updated when you run migrations.
  • lib/ - This folder contains code that doesn't belong in the app or vendor folder. Any .rake files in lib/tasks will be made available to the rake command.
  • log/ - This is where you will find logs produced by your Rails project.
  • public/ - This folder is where static files will go. It is recommended to use the app/assets folder to serve assets via the asset pipeline.
  • test/ - This folder is for testing related files.
  • tmp/ - This folder contains temporary files used by your Rails project.
  • vendor/ - This folder is for third-party code. Any code included in the project that isn't managed by you (e.g. code from bower or locally bundled gems) should be put in this folder, and you should avoid editing vendored files.
  • .gitignore - A manifest of files to be ignored by git
  • Gemfile - Contains the RubyGems required by this project.
  • Gemfile.lock - Contains the specific version of all gems and their dependencies used in this project in order to assure the same version is installed on all systems. This file gets updated automatically when you run the bundle command.
  • README.rdoc - The README file for your project. Should include instructions on how to get your application up and running.
  • Rakefile - The Rakefile for running tasks for your project.
  • config.ru - The rackup file for your project. Rails is a Rack based framework, and uses this file to boot your web server.

Importing our Angular project

Since we created the rails project with the --skip-javascript flag, you may have noticed that our project doesn't have a javascripts folder or an application.js (This file is supposed to contain all the javascript used by our project). Let's go ahead and create the app/assets/javascripts folder, along with a file application.js with //= require_tree . to restore the default behavior of the javascripts folder. Then we can drop in our app.js from the Angular app we created earlier into the javascripts folder so that it'll be included in application.js

Create app/assets/javascripts/application.js
Add //= require_tree . to application.js
Move app.js into app/assets/javascripts. Feel free to rename app.js to something else if it bothers you that there's an application.js and app.js in the same folder, but we'll keep referring to it as app.js.

We'll then need to include the javascript tag in our application layout at app/views/layouts/application.html.erb.

In the <head> section of our layout, add <%= javascript_include_tag 'application' %> on the line after the stylesheet_link_tag like so:
<head>
  <title>FlapperNews</title>
  <%= stylesheet_link_tag    'application', media: 'all' %>
  <%= javascript_include_tag 'application' %>
  <%= csrf_meta_tags %>
</head>
While we're in app/views/layouts/application.html.erb, let's go ahead and copy over the rest of our head and entire body section from our angular project. Notice that we're replacing the yield statement, this is because we'll be rendering the same layout for our angular app. The resulting layout file should look like:
<!DOCTYPE html>
<html>
<head>
  <title>FlapperNews</title>
  <link href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" rel="stylesheet">

  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.7/angular.min.js"></script>
  <script src="//cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.10/angular-ui-router.js"></script>
  <%= stylesheet_link_tag    'application', media: 'all' %>
  <%= javascript_include_tag 'application' %>
  <%= csrf_meta_tags %>
</head>
<body ng-app="flapperNews">
  <div class="row">
    <div class="col-md-6 col-md-offset-3">
      <ui-view></ui-view>
    </div>
  </div>

  <script type="text/ng-template" id="/home.html">
    <div class="page-header">
      <h1>Flapper News</h1>
    </div>

    <div ng-repeat="post in posts | orderBy:'-upvotes'">
      <span class="glyphicon glyphicon-thumbs-up"
        ng-click="incrementUpvotes(post)"></span>
      {{post.upvotes}}
      <span style="font-size:20px; margin-left:10px;">
        <a ng-show="post.link" href="{{post.link}}">
          {{post.title}}
        </a>
        <span ng-hide="post.link">
          {{post.title}}
        </span>
      </span>
      <span>
        <a href="#/posts/{{$index}}">Comments</a>
      </span>
    </div>

    <form ng-submit="addPost()"
      style="margin-top:30px;">
      <h3>Add a new post</h3>

      <div class="form-group">
        <input type="text"
        class="form-control"
        placeholder="Title"
        ng-model="title"></input>
      </div>
      <div class="form-group">
        <input type="text"
        class="form-control"
        placeholder="Link"
        ng-model="link"></input>
      </div>
      <button type="submit" class="btn btn-primary">Post</button>
    </form>
  </script>

  <script type="text/ng-template" id="/posts.html">
    <div class="page-header">
      <h3>
        <a ng-show="post.link" href="{{post.link}}">
          {{post.title}}
        </a>
        <span ng-hide="post.link">
          {{post.title}}
        </span>
      </h3>
    </div>

    <div ng-repeat="comment in post.comments | orderBy:'-upvotes'">
      <span class="glyphicon glyphicon-thumbs-up"
        ng-click="incrementUpvotes(comment)"></span>
      {{comment.upvotes}} - by {{comment.author}}
      <span style="font-size:20px; margin-left:10px;">
        {{comment.body}}
      </span>
    </div>
    <form ng-submit="addComment()"
      style="margin-top:30px;">
      <h3>Add a new comment</h3>

      <div class="form-group">
        <input type="text"
        class="form-control"
        placeholder="Comment"
        ng-model="body"></input>
      </div>
      <button type="submit" class="btn btn-primary">Post</button>
    </form>

  </script>

</body>
</html>

Now that our application layout is set up, we need to make sure Rails is serving our AngularJS application instead of the default Rails landing page. We can do this by creating an action in ApplicationController which will only render the layout, and then route requests to the root of our Rails app to that action.

Generally you want to avoid adding actions to ApplicationController, as all controllers on our backend will inherit from ApplicationController. An alternative option would be to create another controller with a single action instead.

Add an angular action to ApplicationController that renders the layout:
  def angular
    render 'layouts/application'
  end
end
Add a route for root to the action we just created:
root to: 'application#angular'