Here we build a basic page editor.
We are building an Admin, which is a web application that allows a user to update the content on a website. So far we have created a Ruby on Rails application, added libraries such as AngularJS and Twitter Bootstrap, created the Website Javascript app and the Admin Javascript app. Rails is mainly a ReSTful webservice, so it is a collection of resources and each resource has actions associated with it.
The website should be thought of in the same way. It will have Pages, Folders, Images, Users, etc., all of which can be thought of as resources. In the Javascript, each resource will have two modules: one for the Website, one for the Admin. Each module will have its own controllers and at least one service. In Rails, there will be two controllers, one for the Website and one for the Admin.
Usually, the Website module/controller will have read-only actions and the Admin will be read-write. Implementing user authentication is then easier as it can apply to the Admin as a whole.
Creating a Resource
Let’s keep this as simple as possible. In this series, our goal is just to get the bare minimum running so we can see that the app works. Essentially we are going to demonstrate that we can create, view, update, and destroy a resource on the site. In this case, the resource is a page.
Basically, all resources will have this form:
- Database table
- Website Rails controller
- Website AngularJS service
- Website AngularJS view controllers/templates
- Admin Rails controller
- Admin AngularJS service
- Admin AngularJS view controllers/templates
- Updates to Rails routes config
- Updates to Website AngularUI Router config
- Updates to Admin AngularUI Router config
It is a lot of steps and a lot of files, but the benefit of the conventions of these frameworks is that it is all straight forward and helps you to remember all the steps invovled.
Pages database table
Here are the fundamental table columns I use for pages:
- id:primary_key
- slug:string
- title:string
- description:text
- body:text
- is_active:boolean
- created_at:datetime
- updated_at:datetime
One of the best aspects of rails is the migration system for ActiveRecord. With the code below, a migration and a model are automatically created. You don’t have to specify the id, created_at or updated_at since they are assumed. Models are singular, but the generated table is plural. Rails handles this automatically.
[shell]
$ cd /path/to/site
$ rails generate model Page slug:string title:string description:text body:text is_active:boolean
$ rake db:migrate
[/shell]
There should be 5 files that were created or touched. Assuming they are the only new/changed files in your site you want versioned, you can stage all changes with add:
[shell]
$ cd /path/to/site
$ git add .
[/shell]
Pages Controller
There is also a rails generator for controllers. It is helpful, but it creates too many files. I usually just create the controller. Below, I delete the extra files just because I have my own conventions when I make AngularJS apps.
[shell]
$ cd /path/to/site
$ rails generate controller Pages
$ rm app/assets/javascripts/pages.js
$ rm app/assets/stylesheets/pages.scss
$ rm app/helpers/pages_helper.rb
$ rm .generators
$ rm -rf app/views/pages
[/shell]
Time to make the default actions. Rails makes it easy to make ReSTful web services, which is basically all I am using Rails for anymore (thanks, AngularJS).
The first controller is for displaying a page on the site. All we need is an index and show actions. We won’t be creating/updating/deleting pages through this controller.
[shell]
$ cd /path/to/site
$ vi app/controllers/pages_controller.rb
[/shell]
[ruby]
class PagesController < ApplicationController
def index
respond_to do |format|
format.html {
render template: ‘shared/angular’
}
format.json {
# TODO: Make this more specific
pages = Page.all
render json: pages.to_json
}
end
end
def show
respond_to do |format|
format.html {
render template: ‘shared/angular’
}
format.json {
page = Page.find params[:id]
render json: page.to_json
}
end
end
end
[/ruby]
The next controller is the one that allows pages to be created/updated/deleted. Instead of using the generator, lets just copy files. We got this. Since we copied this file, we need to change the name of the class to PagesAdminController. Let’s also change the class we are extending to the AdminController so we can inherit the admin layout.
[shell]
$ cd /path/to/site
$ cp app/controllers/pages_controller.rb app/controllers/pages_admin_controller.rb
$ vi app/controllers/pages_admin_controller.rb
[/shell]
[ruby]
class PagesAdminController < AdminController
def index
respond_to do |format|
format.html {
render template: ‘shared/angular’
}
format.json {
# TODO: Make this more specific
pages = Page.all
render json: pages.to_json
}
end
end
def new
respond_to do |format|
format.html {
render template: ‘shared/angular’
}
end
end
def create
respond_to do |format|
format.json {
page = Page.new form_params
if page.save
response = page
elsif page.errors
response = page.errors
else
response = { error: { message: ‘There was an unknown problem creating this page.’ } }
end
render json: response.to_json
}
end
end
def edit
respond_to do |format|
format.html {
render template: ‘shared/angular’
}
end
end
def update
respond_to do |format|
format.json {
page = Page.find params[:id]
if page.update_attributes form_params
response = page
elsif page.errors
response = page.errors
else
response = { error: { message: ‘There was an unknown problem creating this page.’ } }
end
render json: response.to_json
}
end
end
def show
respond_to do |format|
format.html {
render template: ‘shared/angular’
}
format.json {
page = Page.find params[:id]
render json: page.to_json
}
end
end
def destroy
respond_to do |format|
format.json {
page = Page.find params[:id]
head page.destroy ? :ok : :bad_request
}
end
end
private
def form_params
params.require(:page).permit( :slug, :title, :description, :body, :is_active )
end
end
[/ruby]
Now is a good time to commit our changes. Again, I am assuming everything in the directory (that isn’t in .gitignore) is to be tracked in the repo. If not, you will have to be more specific with git add.
[shell]
$ cd /path/to/site
$ git add .
$ git commit -m “Model and controllers for Pages.”
[/shell]
Now we need to add routes to the controllers.
[shell]
$ cd /path/to/site
$ vi config/routes.rb
[/shell]
For now, we are going to access pages with a URL like “/pages/1” and we are only going to render the index (“/pages/”) and show (“/pages/1”). We are not going to allow create/update/destroy with this controller. So, we are going to restrict the actions using the “only” parameter.
To create a directory in the path, use “scope”. For this site, to edit a page the URL will be like
Add resources :pages to the top level routes and add resources :pages_admin inside the ‘admin’ scope. This will result in a URL “/pages/1” to view the page with ID 1, and /admin/pages/1/edit to edit the page with ID 1. The path “/admin/” is created by the name of the scope. If the “path” attribute wasn’t added to :pages_admin, then the URL would be “/admin/pages_admin/1/edit”
[ruby]
Rails.application.routes.draw do
resources :pages, only: [:index, :show]
scope path: ‘admin’ do
resources :pages_admin, path: ‘/pages’
end
end
[/ruby]
Create an Angular Module for Editing Pages
By using the sprockets asset pipeline, all the javascript files included in application.admin.js will be concatinated (and uglified) into one file. This makes it easy to create individual files for every controller, service, and directive. Each template for views and directives will be included as needed by AngularJS. This means that we will have a lot of files. But if you use an IDE, finding files by name—no matter where they are—is arbitrarily easy. (E.g. in IntelliJ IDEA, use ⌘+Shift+N/Ctrl+Shift+N and type parts of the file, like “APIC” will match “admin.pages.index.ctrl.js”.)
Each view has two files and an entry in the $stateProvider config. In a basic CRUD module in the admin there are two controllers: index and edit. (Since index deals with multiple records, I keep “pages” plural in the controller name: AdminPagesIndexCtrl. Since edit deals with a single record, I keep “page” singular: AdminPageEditCtrl.)
Create new Javascript stubs:
- app/assets/javascripts/admin
- pages/
- admin.pages.js
- admin.pages.index.ctrl.js
- admin.page.edit.ctrl.js
- admin.pages.svc.js
- pages/
Create new HTML templates:
- public/templates/admin
- pages/
- admin.pages.js
- admin.pages.index.ctrl.js
- admin.page.edit.ctrl.js
- pages/
First, create the javascript files in app/assets/javascripts/admin/pages
[shell]
$ cd /path/to/site
$ mkdir -p app/assets/javascripts/admin/pages
$ touch app/assets/javascripts/admin/pages/admin.pages.js
$ touch app/assets/javascripts/admin/pages/admin.pages.index.ctrl.js
$ touch app/assets/javascripts/admin/pages/admin.page.edit.ctrl.js
$ touch app/assets/javascripts/admin/pages/admin.pages.svc.js
[/shell]
[js]
// admin.pages.js
angular.module(‘Admin.Pages’)
.config([
‘$stateProvider’,
function ($stateProvider) {
$stateProvider
.state(‘admin.pages’, {
url: ‘/pages’,
abstract: true,
templateUrl: ‘/templates/admin/pages/admin.pages.layout.html’
})
.state(‘admin.pages.index’, {
url: ‘/?folderId’,
templateUrl: ‘/templates/admin/pages/admin.pages.index.html’,
controller: ‘AdminPagesIndexCtrl’
})
.state(‘admin.pages.new’, {
url: ‘/new/?folderId’,
templateUrl: ‘/templates/admin/pages/admin.page.edit.html’,
controller: ‘AdminPageEditCtrl’
})
.state(‘admin.pages.edit’, {
url: ‘/{pageId:[0-9]+}/edit/’,
templateUrl: ‘/templates/admin/pages/admin.page.edit.html’,
controller: ‘AdminPageEditCtrl’
});
}
]);
[/js]
[js]
// admin.pages.index.ctrl.js
angular.module(‘Admin.Pages’)
.controller(‘AdminPagesIndexCtrl’, [
‘$scope’,
‘AdminPagesService’,
function ($scope, AdminPagesService) {
var getPages = function () {
AdminPagesService.getAll().then(function (response) {
$scope.pages = response.data;
});
};
// INIT
getPages();
}
]);
[/js]
[js]
// admin.page.edit.ctrl.js
angular.module(‘Admin.Pages’)
.controller(‘AdminPageEditCtrl’, [
‘$scope’,
‘$state’,
‘$stateParams’,
‘AdminPagesService’,
function ($scope, $state, $stateParams, AdminPagesService) {
// Simple variable that prevents the page to be created more than once
// if the user clicks the save button again before the webservice responds
var isSaving = false;
// Simple variable that prevents the page from redirecting if there is
// an error saving the page.
var isError = false;
// Take the pageId from $stateParams and find a matching Page from the service
var getPage = function () {
if( $stateParams[‘pageId’] ) {
AdminPagesService.getById($stateParams[‘pageId’]).then(function (response) {
$scope.page = response.data;
});
} else {
$scope.page = {};
}
};
// Since the action after creating and updating a page, create one
// function to pass twice
var updatePage = function (response) {
if( response.data.error ) {
isError = true;
console.error(“There was a problem creating/updating a page:”, response.data.error);
} else {
$scope.page = response.data;
}
isSaving = true;
};
var handleServiceError = function (response) {
console.error(‘There was a problem creating/updating a page.’, arguments);
};
// When the user clicks the ‘save’ button, this method either creates or updates
// the page.
$scope.save = function () {
var promise;
if( ! isSaving ) {
isSaving = true;
isError = false;
if( $scope.page.id ) {
// UPDATE
promise = AdminPagesService.update($scope.page).then(updatePage, handleServiceError);
} else {
promise = AdminPagesService.create($scope.page).then(updatePage, handleServiceError);
}
return promise;
}
};
$scope.saveAndClose = function () {
var promise = $scope.save();
promise.then(function (response) {
if( ! isError ) {
$state.go(‘admin.pages.index’);
}
});
};
// INIT
// Run this method when the controller loads.
getPage();
}
]);
[/js]
[js]
// admin.pages.svc.js
angular.module(‘Admin.Pages’)
.service(‘AdminPagesService’, [
‘$http’,
function ($http) {
var service = {};
service.getAll = function () {
return $http.get(‘/admin/pages.json’);
};
service.getById = function (pageId) {
return $http.get(‘/admin/pages/’+ parseInt(pageId, 10) +’.json’);
};
service.create = function (page) {
return $http.post(‘/admin/pages.json’, { page: page });
};
service.update = function (page) {
return $http.put(‘/admin/pages/’+ parseInt(page.id, 10) +’.json’, { page: page });
};
service.destroy = function (pageId) {
return $http.delete(‘/admin/pages/’+ parseInt(pageId, 10) +’.json’);
};
return service;
}
]);
[/js]
Make the HTML templates:
[shell]
$ cd /path/to/site
$ mkdir -p public/templates/admin/pages
$ touch public/templates/admin/pages/admin.pages.layout.html
$ touch public/templates/admin/pages/admin.pages.index.html
$ touch public/templates/admin/pages/admin.page.edit.html
[/shell]
All templates within the admin.pages UI state will be rendered within this template, inside the ui-view element.
[html]
[/html]
Here is a very simple template for the index, which will list out the existing pages and allow a user to create a new one.
[html]
Pages Admin
[/html]
To begin with, we are just going to create a title and a body for a page. We will add the rest of the fields later.
[html]
[/html]