Welcome to part two of my AngularJS with Ruby on Rails tutorial. Last week we covered front end routing while this week we’ll be separating our assets from the Rails application. Our goal is to have an API-only Rails application with a completely separate AngularJS based front end.
AngularJS and Haml
AngularJS templates are simply HTML with some added HTML5 data attributes, known as directives. These directives tell AngularJS ways in which to modify the DOM or enhance the behaviour of objects using event handlers. This means that all the HTML you’ve written for your Rails app is still perfectly usable.
Except, there is one small problem. If your HTML is like mine it’s done in Haml. Rails does a great job of just-in-time asset compilation when you’re in development mode, which includes the Haml markup, but we really don’t want Rails serving these in production. What can we do? You might consider placing the templates in the assets directory and enabling Haml compilation for assets, but I wanted a solution that would allow me to decouple the front end from the back end.
I settled on creating a separate directory + git repository for the Haml files and two shell scripts, one to build the templates and one to deploy the HTML files to the “shared” folder of my app deploy structure. Then I added a Capistrano task to symlink the templates folder into the current public directory after deploy. This change gives us (Carburetor) the ability to hire front end developers in the future without requiring them to set up a Ruby on Rails environment. On the technical side it also ensures that the templates are sent by the real web server (nginx) instead of the Rails application server (Unicorn). You see, nginx does it much faster.
Blah, blah, blah. I didn’t come here to listen to you drone on. Where’s the code? First of all, that’s very rude. Secondly, you’re right. Here’s the code, but be warned that my shell scripts are ugly.
There’s been a change of plans. I initially posted this with a barely-workable hackish shell script that needed to be run manually for every compile, only supported Haml and had a static directory structure. After looking at it for a while I felt so ashamed that I had to do something better. Since I couldn’t find any other open source tool which does the job, allow me to introduce my autobuild-client script with support for CoffeeScript, SCSS and Haml:
#!/bin/bash
# Target directories without trailing slashes.
input_directory='/home/user/interface/source';
output_directory='/home/user/rails-app/public';
# Manifest file path is relative to output_directory.
stylesheet_manifest_file='/stylesheets/application.css';
javascript_manifest_file='/javascripts/application.js';
# Do you need a certain load order for your js files? Specify here with absolute paths:
loadjs=(
[0]=$input_directory'/javascripts/vendor/angular-1.1.2.js'
[1]=$input_directory'/javascripts/vendor/angular-resource-1.1.2.js'
[2]=$input_directory'/javascripts/vendor/angular-sanitize-1.1.2.js'
[3]=$output_directory'/javascripts/app.js'
);
# Escaped version of the input directory is used to strip it from matches.
escaped_input_directory=$(echo $input_directory | sed 's/////g');
# Clear the manifest files.
ss_mf=$output_directory$stylesheet_manifest_file;
js_mf=$output_directory$javascript_manifest_file;
echo '' > $ss_mf;
# Function to search for a value in an array.
in_array() {
local val;
for val in "${@:2}"; do [[ "$val" == "$1" ]] && return 0; done
return 1;
}
# Function to concatenate JS files for easy single file include.
concat_scripts() {
echo '' > $js_mf;
concatenated_scripts=([0]=$output_directory'/javascripts/application.js');
for input_file in "${loadjs[@]}"; do
if !($(in_array "$input_file" "${concatenated_scripts[@]}")); then
concatenated_scripts=(${concatenated_scripts[@]} "$input_file");
#echo "Prioritizing addition of $input_file to js manifest.";
cat $input_file >> $js_mf;
echo "" >> $js_mf;
fi
done
for input_file in $(find $input_directory -name "*.js"); do
if !($(in_array "$input_file" "${concatenated_scripts[@]}")); then
concatenated_scripts=(${concatenated_scripts[@]} $input_file);
#echo "Adding $input_file to js manifest.";
cat $input_file >> $js_mf;
echo "" >> $js_mf;
fi
done
for input_file in $(find $output_directory -name "*.js"); do
if !($(in_array "$input_file" "${concatenated_scripts[@]}")); then
concatenated_scripts=(${concatenated_scripts[@]} $input_file);
#echo "Adding $input_file to js manifest.";
cat $input_file >> $js_mf;
echo "" >> $js_mf;
fi
done
}
# Start by compiling any files which have been modified since last time the script was run.
for input_file in $(find $input_directory -not -wholename '*.git*' -not -wholename '*.sass-cache*' -name "*.*.*"); do
input_format=$(echo $input_file | sed 's/^.*.([^.]*)$/1/');
output_file=`echo $input_file | sed 's/^.*/([^/]*).'$input_format'/1/'`;
output_subpath=`echo $input_file | sed 's/'$escaped_input_directory'/(.*)/'$output_file'.*//1/'`;
output_dir=$output_directory$output_subpath;
output_path=$output_dir'/'$output_file;
if [ ! -d "$output_dir" ]; then
mkdir -p $output_dir;
fi
compile=0
if [ -e $output_path ]; then
compile=1
if [ `stat -c %Y $input_file` -gt `stat -c %Y $output_path` ]; then
compile=0
fi
fi
if [ $compile -eq 0 ]; then
if [ $input_format = "haml" ]; then
echo "Hamling "$input_file" to "$output_path;
haml $input_file $output_path;
fi
if [ $input_format = "coffee" ]; then
echo "Coffing "$input_file" to "$output_path;
coffee --compile -p $input_file > $output_path;
fi
if [ $input_format = "scss" ]; then
echo "Sassing "$input_file" to "$output_path;
sass $input_file $output_path;
fi
fi
# Here we add import commands to the stylesheet manifest for each file. This is also for easy includes.
if [ $input_format = "scss" ]; then
#echo "Adding $output_subpath/$output_file to stylesheet development manifest.";
echo "@import '"$output_subpath"/"$output_file"';" >> $output_directory"/"$stylesheet_manifest_file;
fi
done
# Then concatenate all the scripts.
concat_scripts;
echo "AutoBuild is now watching $input_directory for changes.";
# Then monitor the directory for changes.
while inp=$(inotifywait -qre MODIFY $input_directory); do
file_format=$(echo $inp | sed 's/^.*.([^.]*)$/1/');
input_file=$(echo $inp | sed 's/s.*s//');
output_subdir=$(echo $inp | sed 's/'$escaped_input_directory'(.*)/s.*/1/');
output_file=$(echo $inp | sed 's/(.*)s.*s(.*).'$file_format'/2/');
output_path=$(echo $output_directory""$output_subdir"/"$output_file);
if [ $file_format = "haml" ]; then
echo "Hamling "$input_file" to "$output_path;
haml $input_file $output_path;
fi
if [ $file_format = "coffee" ]; then
echo "Coffing "$input_file" to "$output_path;
coffee --compile -p $input_file > $output_path;
concat_scripts;
fi
if [ $file_format = "scss" ]; then
echo "Sassing "$input_file" to "$output_path;
sass $input_file $output_path;
fi
done
This script only has to be run once. It will then monitor the input directory for changes and compile any updated files. You can include these files in your application by linking to the manifest files (here named application.css and application.js). This saves you the trouble of updating your templates each time you create or remove a file (and from having to keep a separate include for production than development).
I’ve created a new repository on GitHub for this script which you can find here. I am releasing it under the MIT license for anyone to use as they please. I hope your front end developers enjoy it.
The deploy script (this requires the closure compiler and yui-compressor):
#!/bin/bash
echo "Minifying JS."
java -jar compiler.jar --warning_level QUIET --language_in ECMASCRIPT5 compiled/javascripts/application.js > application.min.js &&
echo "Uploading JS." &&
scp application.min.js user@example.com:apps/example/shared/javascripts/application.js
echo "" > application.css;
echo "Concatenating stylesheets."
for input_file in $(find -not -name "application.min.css" -not -name "application.css" -name "*.css"); do
cat $input_file >> application.css;
done
echo "Compressing stylesheet."
yui-compressor application.css > application.min.css
echo "Uploading stylesheet."
scp application.min.css user@example.com:apps/example/shared/stylesheets/application.css
echo "Uploading templates."
scp -r compiled/templates deployer@voltagecrm.com:apps/voltar/shared/
Finally the Capistrano task:
namespace :your_app do
desc "Symlink the AngularJS templates into the public dir"
task :symlink_templates, roles: :app do
run "ln -nfs #{shared_path}/templates #{release_path}/public/templates"
run "ln -nfs #{shared_path}/javascripts #{release_path}/public/javascripts"
run "ln -nfs #{shared_path}/stylesheets #{release_path}/public/stylesheets"
end
after "deploy:finalize_update", "your_app:symlink_templates"
end
All in all nothing too complicated (edit: at least, not until I updated the build script). However, it doesn’t tell you how to actually create the AngularJS templates, does it? Right! I try to avoid duplicating material when it isn’t necessary. Here is the official tutorial and here are all the specific details you could possibly want about the AngularJS API. My tutorial is on the conversion of your Ruby on Rails application to AngularJS. I apologize if I didn’t make this clear enough at the start.
What I will tell you is the things you’re going have to replace. First and likely easiest is the paths. You might create your own path helpers for your front end, but you can’t use the Rails ones anymore. If your routing is consistent then simple string replacements will do you fine. Second is your helpers. Every view helper you’ve used has to be replaced with an alternate version in JavaScript (or CoffeeScript). The built in Rails form helpers will need to be replaced with their equivalent HTML/Haml.
Now that you’re starting to convert your views into AngularJS templates you’ll likely need access to data from your Rails application. How might that be done? Check back a week from now (Monday, February 25th) for the next installation of this blog series which will describe how to communicate with your server via a JSON API.