Lets do the least amount of work that uses the whole technology stack.
This admin will allow a person to update content on a website built with Rails, MySQL, AngularJS, AngularUI, FontAwesome and Bootstrap. Before we add a bunch of features, lets make sure everything plays nice together.
Set a simple goal
The simplest thing is to create a page and add a title and description. That will require the following steps:
- A database table
- An ActiveRecord model
- A Rails controller
- A Rails view
- Routes added to Rails
- An AngularJS service
- AngularJS controllers
- Routes added to Angular UI
- HTML Templates
- AngularJS directives
This is a lot of steps, but once the inital work is done, it is simpler to add features later. The best part of these frameworks is you don’t need to think about how to do the mundane stuff, only the aspects that make your app unique.
Add AngularJS to Rails
This Website/CMS will be using AngularJS and Angular UI Router to the app. Angular UI Router makes it easy to manage views, controllers, templates, states and more of a Javascript web application. Since I am doing both together, it might be confusing what part is AngularJS proper and what part comes from the router.
Add the AngularUI Router library
AngularJS has been wrapped in a gem, and we already installed that gem in the previous post. AngularUI is a collection of different libraries that are helpful in making AngularJS apps. To manage how we use URLs to access controllers, we are going to add the Angular UI Router library. This library has not been wrapped in a gem, so lets add it the old fashioned way. (You could use a system like bower, but I prefer more control over what files I include.) (We already installed AngularUI Bootstrap, which is also wrapped in a gem.)
Since we are using Rails to minify/uglify our code, we can grab the unminified version of Angular UI Router and add it to our source code. The current release of Router is 0.2.15, so we will put it in that directory. Then we will add a line to include it in application.js, before our custom code. Note that curl is using the lowercase “o” option.
[shell]
$ cd /path/to/site
$ mkdir -p app/assets/javascripts/lib/angular-ui/router/0.2.15
$ curl -o app/assets/javascripts/lib/angular-ui/router/0.2.15/angular-ui-router.js http://angular-ui.github.io/ui-router/release/angular-ui-router.js
$ vi app/assets/javascripts/application.js
[/shell]
[js]
// application.js
…
//= require lib/angular-ui/router/0.2.15/angular-ui-router.js
…
[/js]
Angular New Router
Alternately, we can use the new Angular Router that is designed for AngularJS 2, but works with version 1.3. At the time of this writing, Router is still experimental, so we are going to just grab it raw from github. Note that curl is using the uppercase “O” option.
[shell]
$ cd /path/to/site
$ mkdir -p app/assets/javascripts/lib/angular-new-router/0.5.3
$ cd app/assets/javascripts/lib/angular-new-router/0.5.3/
$ curl -O https://raw.githubusercontent.com/angular/router/master/dist/router.es5.js
[/shell]
NG-App and Twitter Bootstrap Container
[shell]
$ cd /path/to/site
$ vi app/assets/views/layouts/application.html.erb
[/shell]
In the application.html.erb layout, lets wrap the [html]<%= yield %>[/html] variable in a div with ng-app to start up the AngularJS web application and specify the container for Twitter Bootstrap.
[html]
…
…
…
…
[/html]
AngularJS View for Rails
For this website, we are going to use Angular UI Router to manage what controllers and templates run when the URL changes. As a person on the site clicks links, the web page doesn’t reload; AngularJS pulls in new HTML and JavaScript. This is great until a person clicks a deep link from somewhere else or refreshes their browser. When this happens, the Rails router handles the request to serve up the HTML that will start the AngularJS app. Since all the controller actions—when asked for HTML—are serving HTML to start the app, the actions don’t have to render their own views. They can all render a shared view. Lets create that file first, then point the controller actions to it. (For this website I am using ERB as the Rails templating language.)
[shell]
$ cd /path/to/site
$ mkdir app/views/shared
$ vi app/views/shared/angular.html.erb
[/shell]
We are going to have all the AngularJS app code in application.html.erb except the special “ui-view” directive that is part of Angular UI Router. All the views that are defined in UI Router will be rendered in this div. This div will be rendered by Rails in the place of [html]<%= yield %>[/html] in application.html.erb.
[html]
[/html]
Default Controllers
To start, we need two controllers: one for the Website and one for the Admin.
[shell]
$ cd /path/to/site
$ touch app/controllers/website_controller.rb
$ touch app/controllers/admin_controller.rb
[/shell]
[ruby]
# website_controller.rb
class WebsiteController < ApplicationController
layout 'application.html.erb'
def index
render template: 'shared/angular'
end
end
[/ruby]
[ruby]
# admin_controller.rb
class AdminController < ApplicationController
layout 'application.admin.html.erb'
def index
render template: 'shared/angular'
end
end
[/ruby]
Rails Routes
Now lets add routes for the two controllers. To create a directory to keep the website admin, create a scope with a path of ‘admin’ and place all your admin controllers in that block. For the pages on the website (not the admin) we are only going to use the index and show actions, so we can specify that with the ‘only’ attribute.
[shell]
$ cd /path/to/site
$ vi config/routes.rb
[/shell]
[ruby]
Rails.application.routes.draw do
# Website routes go here
scope path: ‘admin’ do
# Admin routes go here
end
get ‘admin’ => ‘admin#index’
root ‘website#index’
end
[/ruby]
Split out the Admin App
I prefer to think of the admin as its own app which controls the website app. So lets split the two.
[shell]
$ cd /path/to/site
$ mkdir app/assets/javascripts/admin
$ cp app/assets/javascripts/application.js app/assets/javascripts/application.admin.js
$ cp app/assets/stylesheets/application.css.scss app/assets/stylesheets/application.admin.css.scss
$ cp app/views/layouts/application.html.erb app/views/layouts/application.admin.html.erb
[/shell]
Append new CSS and JS to precompile list
Rails already compiles applicaiton.js and application.css, but it doesn’t know about the two new files we created. In Rails 4, this is found in /config/assets.rb:
[shell]
$ cd /path/to/site
$ vi config/asssets.rb
[/shell]
Add this line:
[ruby]
…
Rails.application.config.assets.precompile += %w( application.admin.js application.admin.css )
…
[/ruby]
Add AngularJS
Create a folder for the Website App, and then create an website.app.js file above that folder. We can then specifically call website.app.js first and all the files within the folder second, thus making sure all code that must run first (e.g. declaring modules) isn’t executed out of order.
[shell]
$ cd /path/to/site
$ mkdir app/assets/javascripts/website
$ vi app/assets/javascripts/website.app.js
[/shell]
[js]
// TODO
[/js]
Wire it up.
[shell]
$ cd /path/to/site
$ vi app/assets/javascripts/application.js
[/shell]
[js]
…
//= require website.app.js
//= require_tree ./website
[/js]
Create the admin app.
[shell]
$ cd /path/to/site
$ mkdir app/assets/javascripts/admin
$ touch app/assets/javascripts/admin.app.js
[/shell]
Wire it up.
[shell]
$ cd /path/to/site
$ vi app/assets/javascripts/application.admin.js
[/shell]
[js]
…
//= require admin.app.js
//= require_tree ./admin
[/js]
Create the Admin App
One caveat to the sprockets asset pipeline is that you can’t specify the order in which require_tree includes files. If files that refer to a module are included ahead of the file that defines the module, the controllers/directives/services you define won’t be available in the app. There are different ways to approach this. My method is to create all the modules in the admin.app.js file. This way, I can see all the modules I am creating in the app in one spot. As we create modules, they will be defined in admin.app.js and/or website.app.js.
As we create the file, lets inject Anguar UI Router. We will use HTML5 Mode, which tells modern browsers not to reload the page when the URL changes. UI Router includes $stateProvider with which you define the routes for the Javascript app. $stateProvider.state() is the Javascript counterpart to routes.rb in Rails. Most paths will be defined in both. (Routes have to be specified in Rails in case a user reloads their browser or enters the app through a deep link. Rails has to render the HTML which calls the Javascript files, which launches the AngularJS app.) For now, we are just defining an “abstract” state for the “/admin” URL.
[shell]
$ cd /path/to/site
$ vi app/assets/javascripts/admin/admin.app.js
[/shell]
[js]
// admin.app.js
angular.module(‘Admin’, [‘ui.router’])
.config([
‘$locationProvider’,
‘$stateProvider’,
function ($locationProvider, $stateProvider) {
$locationProvider.html5Mode({enabled:true, requireBase:false});
$stateProvider
.state(‘admin’, {
url: “/admin”,
abstract: true,
template: ‘
‘
})
}
]);
[/js]
Update the Admin Layout
We need to add the application.admin.js file to application.admin.html.erb and update the name to the AngularJS app:
[shell]
$ cd /path/to/site
$ vi app/views/layouts/application.admin.html.erb
[/shell]
[html]
…
<%= stylesheet_link_tag 'application.admin', media: 'all' %>
<%= javascript_include_tag 'application.admin' %>
…
…
[/html]
Next we will create the Pages modules to create, edit, delete, and view pages on the website.