├── .gitignore ├── .ruby-gemset ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile ├── about.md ├── app_overview.md ├── apple-touch-icon.png ├── back.md ├── bootstrap.md ├── colophon.md ├── config.json ├── contributing.md ├── crud_recipe.md ├── favicon.ico ├── find_and_browse.md ├── front.md ├── images ├── FlashMessage.png ├── SearchUI.png └── SearchUIWorking.png ├── main.md ├── original_styles.css ├── sass ├── colors.scss ├── skin.scss ├── styles.scss └── typography.scss ├── styles.css └── view_recipe.md /.gitignore: -------------------------------------------------------------------------------- 1 | git_repos 2 | book 3 | cache 4 | *.sw? 5 | .DS_Store 6 | .sass-cache/ 7 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | angular-rails-book 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.2.2 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | ruby '2.2.2' 4 | 5 | gem 'bookingit' 6 | gem 'redcarpet', '3.1.1' 7 | gem 'sass' 8 | gem 'rake' 9 | 10 | # sigh 11 | gem 'rails', '4.0.3' 12 | gem 'pg' 13 | #gem 'sass', '3.2.19' 14 | gem 'sass-rails', '~> 4.0.0' 15 | gem 'uglifier', '>= 1.3.0' 16 | gem 'coffee-rails', '~> 4.0.0' 17 | gem 'jquery-rails' 18 | gem 'jbuilder', '~> 1.2' 19 | 20 | gem 'bower-rails' 21 | gem 'angular-rails-templates' 22 | 23 | gem "foreman" 24 | group :production, :staging do 25 | gem "rails_12factor" 26 | gem "rails_stdout_logging" 27 | gem "rails_serve_static_assets" 28 | end 29 | 30 | group :test, :development do 31 | gem "rspec" 32 | gem "rspec-rails", "~> 2.0" 33 | gem "factory_girl_rails", "~> 4.0" 34 | gem "capybara" 35 | gem "database_cleaner" 36 | gem "selenium-webdriver" 37 | gem 'teaspoon-jasmine' 38 | gem 'phantomjs' 39 | end 40 | 41 | group :doc do 42 | gem 'sdoc', require: false 43 | end 44 | 45 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actionmailer (4.0.3) 5 | actionpack (= 4.0.3) 6 | mail (~> 2.5.4) 7 | actionpack (4.0.3) 8 | activesupport (= 4.0.3) 9 | builder (~> 3.1.0) 10 | erubis (~> 2.7.0) 11 | rack (~> 1.5.2) 12 | rack-test (~> 0.6.2) 13 | activemodel (4.0.3) 14 | activesupport (= 4.0.3) 15 | builder (~> 3.1.0) 16 | activerecord (4.0.3) 17 | activemodel (= 4.0.3) 18 | activerecord-deprecated_finders (~> 1.0.2) 19 | activesupport (= 4.0.3) 20 | arel (~> 4.0.0) 21 | activerecord-deprecated_finders (1.0.4) 22 | activesupport (4.0.3) 23 | i18n (~> 0.6, >= 0.6.4) 24 | minitest (~> 4.2) 25 | multi_json (~> 1.3) 26 | thread_safe (~> 0.1) 27 | tzinfo (~> 0.3.37) 28 | angular-rails-templates (0.2.0) 29 | railties (>= 3.1) 30 | sprockets (~> 2) 31 | tilt 32 | arel (4.0.2) 33 | bookingit (0.4.1) 34 | gli 35 | mustache 36 | redcarpet (= 3.1.1) 37 | bower-rails (0.9.2) 38 | builder (3.1.4) 39 | capybara (2.4.4) 40 | mime-types (>= 1.16) 41 | nokogiri (>= 1.3.3) 42 | rack (>= 1.0.0) 43 | rack-test (>= 0.5.4) 44 | xpath (~> 2.0) 45 | childprocess (0.5.6) 46 | ffi (~> 1.0, >= 1.0.11) 47 | coffee-rails (4.0.1) 48 | coffee-script (>= 2.2.0) 49 | railties (>= 4.0.0, < 5.0) 50 | coffee-script (2.4.1) 51 | coffee-script-source 52 | execjs 53 | coffee-script-source (1.9.1.1) 54 | database_cleaner (1.4.1) 55 | diff-lcs (1.2.5) 56 | erubis (2.7.0) 57 | execjs (2.5.2) 58 | factory_girl (4.5.0) 59 | activesupport (>= 3.0.0) 60 | factory_girl_rails (4.5.0) 61 | factory_girl (~> 4.5.0) 62 | railties (>= 3.0.0) 63 | ffi (1.9.8) 64 | foreman (0.78.0) 65 | thor (~> 0.19.1) 66 | gli (2.13.1) 67 | hike (1.2.3) 68 | i18n (0.7.0) 69 | jbuilder (1.5.3) 70 | activesupport (>= 3.0.0) 71 | multi_json (>= 1.2.0) 72 | jquery-rails (3.1.2) 73 | railties (>= 3.0, < 5.0) 74 | thor (>= 0.14, < 2.0) 75 | json (1.8.3) 76 | mail (2.5.4) 77 | mime-types (~> 1.16) 78 | treetop (~> 1.4.8) 79 | mime-types (1.25.1) 80 | mini_portile (0.6.2) 81 | minitest (4.7.5) 82 | multi_json (1.11.0) 83 | mustache (1.0.1) 84 | nokogiri (1.6.6.2) 85 | mini_portile (~> 0.6.0) 86 | pg (0.18.2) 87 | phantomjs (1.9.8.0) 88 | polyglot (0.3.5) 89 | rack (1.5.3) 90 | rack-test (0.6.3) 91 | rack (>= 1.0) 92 | rails (4.0.3) 93 | actionmailer (= 4.0.3) 94 | actionpack (= 4.0.3) 95 | activerecord (= 4.0.3) 96 | activesupport (= 4.0.3) 97 | bundler (>= 1.3.0, < 2.0) 98 | railties (= 4.0.3) 99 | sprockets-rails (~> 2.0.0) 100 | rails_12factor (0.0.3) 101 | rails_serve_static_assets 102 | rails_stdout_logging 103 | rails_serve_static_assets (0.0.4) 104 | rails_stdout_logging (0.0.3) 105 | railties (4.0.3) 106 | actionpack (= 4.0.3) 107 | activesupport (= 4.0.3) 108 | rake (>= 0.8.7) 109 | thor (>= 0.18.1, < 2.0) 110 | rake (10.4.2) 111 | rdoc (4.2.0) 112 | json (~> 1.4) 113 | redcarpet (3.1.1) 114 | rspec (2.99.0) 115 | rspec-core (~> 2.99.0) 116 | rspec-expectations (~> 2.99.0) 117 | rspec-mocks (~> 2.99.0) 118 | rspec-collection_matchers (1.1.2) 119 | rspec-expectations (>= 2.99.0.beta1) 120 | rspec-core (2.99.2) 121 | rspec-expectations (2.99.2) 122 | diff-lcs (>= 1.1.3, < 2.0) 123 | rspec-mocks (2.99.3) 124 | rspec-rails (2.99.0) 125 | actionpack (>= 3.0) 126 | activemodel (>= 3.0) 127 | activesupport (>= 3.0) 128 | railties (>= 3.0) 129 | rspec-collection_matchers 130 | rspec-core (~> 2.99.0) 131 | rspec-expectations (~> 2.99.0) 132 | rspec-mocks (~> 2.99.0) 133 | rubyzip (1.1.7) 134 | sass (3.2.19) 135 | sass-rails (4.0.5) 136 | railties (>= 4.0.0, < 5.0) 137 | sass (~> 3.2.2) 138 | sprockets (~> 2.8, < 3.0) 139 | sprockets-rails (~> 2.0) 140 | sdoc (0.4.1) 141 | json (~> 1.7, >= 1.7.7) 142 | rdoc (~> 4.0) 143 | selenium-webdriver (2.46.2) 144 | childprocess (~> 0.5) 145 | multi_json (~> 1.0) 146 | rubyzip (~> 1.0) 147 | websocket (~> 1.0) 148 | sprockets (2.12.3) 149 | hike (~> 1.2) 150 | multi_json (~> 1.0) 151 | rack (~> 1.0) 152 | tilt (~> 1.1, != 1.3.0) 153 | sprockets-rails (2.0.1) 154 | actionpack (>= 3.0) 155 | activesupport (>= 3.0) 156 | sprockets (~> 2.8) 157 | teaspoon (1.0.2) 158 | railties (>= 3.2.5, < 5) 159 | teaspoon-jasmine (2.2.0) 160 | teaspoon (>= 1.0.0) 161 | thor (0.19.1) 162 | thread_safe (0.3.5) 163 | tilt (1.4.1) 164 | treetop (1.4.15) 165 | polyglot 166 | polyglot (>= 0.3.1) 167 | tzinfo (0.3.44) 168 | uglifier (2.7.1) 169 | execjs (>= 0.3.0) 170 | json (>= 1.8.0) 171 | websocket (1.2.2) 172 | xpath (2.0.0) 173 | nokogiri (~> 1.3) 174 | 175 | PLATFORMS 176 | ruby 177 | 178 | DEPENDENCIES 179 | angular-rails-templates 180 | bookingit 181 | bower-rails 182 | capybara 183 | coffee-rails (~> 4.0.0) 184 | database_cleaner 185 | factory_girl_rails (~> 4.0) 186 | foreman 187 | jbuilder (~> 1.2) 188 | jquery-rails 189 | pg 190 | phantomjs 191 | rails (= 4.0.3) 192 | rails_12factor 193 | rails_serve_static_assets 194 | rails_stdout_logging 195 | rake 196 | redcarpet (= 3.1.1) 197 | rspec 198 | rspec-rails (~> 2.0) 199 | sass 200 | sass-rails (~> 4.0.0) 201 | sdoc 202 | selenium-webdriver 203 | teaspoon-jasmine 204 | uglifier (>= 1.3.0) 205 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Angular on Ruby on Rails 2 | 3 | This is the source of http://angular-rails.com. To build it, you'll need to do a few things: 4 | 5 | 1. `bundle install` or `gem install bookingit` to install the bookingit gem 6 | 1. `git clone https://github.com/davetron5000/receta git_repos/receta` to clone the repository containing the source code examples in the book 7 | 1. `createuser -d receta` to create a new PostgreSQL user as required by receta/config/database.yml 8 | 1. `pushd git_repos/receta && bundle install && popd` to install all gems that are required for the `receta` project 9 | 1. `bookingit build` to generate the HTML and CSS for the book 10 | 11 | Once this is done, open `book/index.html` in your browser. Whenever you change something, re-run `bookingit build`. 12 | 13 | ## Changes to the source 14 | 15 | `bookingit` uses git tags and such to generate the source and diffs in the book. This allows me to know that everything is working and control what's 16 | being displayed. Unfortunately, if something needs to change, this breaks all the tags and SHA-1s. I haven't sorted out a good way to fix this yet. 17 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | task :default do 2 | sh('sass --update sass/styles.scss:styles.css') { |ok,_| fail 'problem running sass' unless ok } 3 | sh('bookingit build ../angular-rails.com/') { |ok,_| fail 'problem running bookingit' unless ok } 4 | end 5 | -------------------------------------------------------------------------------- /about.md: -------------------------------------------------------------------------------- 1 | # About the Author 2 | 3 | I'm David Bryant Copeland, otherwise known as [@davetron5000][twitter] on 4 | Twitter, and am the author of [“Build Awesome Command-Line Applications in Ruby 2”][clibook] 5 | and [“The Senior Software Engineer”][swengbook] (which is a book about 6 | everything you need to know as a developer other than the coding part :). 7 | 8 | [twitter]: http://twitter.com/davetron5000 9 | [clibook]: http://pragprog.com/book/dccar2/build-awesome-command-line-applications-in-ruby-2 10 | [swengbook]: http://theseniorsoftwareengineer.com/ 11 | 12 | I currently live in Washington, DC and am a Director of Engineering for retail innovator [Stitch Fix][stitchfixblog], working with a small team building software (mostly Rails apps) to support our business. 13 | We use Angular for several of our internal applications. 14 | To read more about how we're using it, see my blog post [Unassuming Apps w/ AngularJS][angular-post]. 15 | 16 | [angular-post]: http://technology.stitchfix.com/blog/2014/08/12/unambitious-angular-apps/ 17 | 18 | [stitchfixblog]: http://technology.stitchfix.com/blog/ 19 | 20 | Prior to that, I supported several Rails internal APIs for LivingSocial, and 21 | before *that* helped build the engineering team at energy-efficiency startup 22 | Opower. 23 | 24 | I focus on practical use of technology, as best documented on my blog in 25 | either [Production is all that matters][prodpost] or [Responsible 26 | Refactoring][refactorpost]. That's why this book is about getting your code 27 | deployed and not what is theoretically possible. Details matter to me. 28 | 29 | [refactorpost]: http://www.naildrivin5.com/blog/2013/08/08/responsible-refactoring.html 30 | [prodpost]: http://www.naildrivin5.com/blog/2013/06/16/production-is-all-that-matters.html 31 | -------------------------------------------------------------------------------- /app_overview.md: -------------------------------------------------------------------------------- 1 | # The Application We'll Build 2 | 3 | To frame all the setup needed to get an Angular app running with Rails, 4 | we'll build a simple application to manage recipes. The application will 5 | be a store of recipes that can be searched. A recipe is a title 6 | and some text representing the instructions. 7 | 8 | We'll build the application in four steps: 9 | 10 | 1. Create a skeleton that does very little but can be deployed. 11 | 2. Walk through the implementation of a basic search by name. 12 | 3. Walk briskly through a test-driven implementation of viewing a recipe. 13 | 4. Run through the implementation of creating, editing, and deleting recipes. 14 | 15 | A basic “CRUD” app isn't going to be the best demonstration of Angular's 16 | power, but it's easy to understand, especially for us Rails developers, and will 17 | allow us to just focus on getting things set up. 18 | 19 | The most difficult and tedious part of using a new technology is the initial setup, so let's do that first. 20 | -------------------------------------------------------------------------------- /apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davetron5000/angular-rails-book/32b73e1d2a894268d01c11dfc4dd8dacfe8feee5/apple-touch-icon.png -------------------------------------------------------------------------------- /back.md: -------------------------------------------------------------------------------- 1 | # In Conclusion 2 | 3 | This concludes our whirlwind tour of what to do to set up the basics of an 4 | Angular app, powered by Rails. With the setup we've gone through, you can now 5 | crank out features quickly and easily—the hard work has been done. 6 | 7 | With that said, I want to leave you with a few design tips, and a basic workflow I've found useful when building Angular apps with Rails. 8 | 9 | ## Design Tips 10 | 11 | Because we are “off the Rails”, we have to make more design decisions than we might normally. 12 | 13 | * Complexity in JavaScript is far more painful than in Ruby, so if you can make your Angular code simpler by doing a bit more work in Rails, do it (for example, using JBuilder to produce JSON with camel-cased keys). 14 | * While Angular is supremely flexible, its components do have happy paths - try to stick to those where you can. 15 | * Test your navigation. Since you're using JavaScript to capture user actions and transitioning between views, it's more likely to break. 16 | * Enforce business rules at the Rails layer always. You don't have to do so in a nice way, but do do it. 17 | 18 | It's also useful to have a solid workflow in place. 19 | 20 | ## Decent Workflow 21 | 22 | We saw a few different workflows in this tutorial, but I want to leave with a 23 | workflow I've found effective: 24 | 25 | 1. Create a browser-based test to exercise the expected paths of the UI. This 26 | is largely the “happy path”, but might also include validations and other 27 | exceptional flows that the user will likely experience. 28 | 2. Write your Angular controller (test-first, of course), which will lead you to fleshing out 29 | what the back-end needs to look like. 30 | 3. Implement the back-end. 31 | 4. Implement the view in a basic way to get your browser-based test to pass. 32 | 5. Add whatever bells-and-whistles or other UI stuff you want, using your 33 | browser-based test to make sure you've haven't broken anything. 34 | 35 | With that, go forth and make awesomely rich applications, powered by the best 36 | web framework around! 37 | -------------------------------------------------------------------------------- /bootstrap.md: -------------------------------------------------------------------------------- 1 | # Creating a Skeleton App 2 | 3 | Even the most basic Rails app requires additional configuration beyond simply 4 | running `rails new`. For example, you may have certain gems you know you'll 5 | need from the start. 6 | 7 | An Angular application will need even more than that, and while the setup we're 8 | about to see can (and should) be automated with a [rails app template][rails-app-template], 9 | it's important to see these steps and understand why we're doing them. 10 | 11 | [rails-app-template]: http://technology.stitchfix.com/blog/2014/01/06/rails-app-templates/ 12 | 13 | Our goal here is start from scratch and have an application that renders a 14 | view containing a piece of data provided by Angular. That will be sufficient 15 | for us to validate that we're moving in the right direction. 16 | 17 | The basic steps are: 18 | 19 | 1. Create an empty Rails app 20 | 2. Add some basic gems we know we'll need 21 | 3. Set up [Bower][bower] to manage our front-end dependencies 22 | 4. Write just enough code to serve content via Angular 23 | 5. Deploy to a production server to validate the asset pipeline is working 24 | 25 | [bower]: http://bower.io 26 | 27 | ## An empty Rails app 28 | 29 | Our app will be called “Receta”, which is Spanish for “recipe”. We're going 30 | to use Postgres as our database (although we won't be using anything 31 | Postgres-specific), and we're going to skip `bundle install` for now. Note if you would prefer to use MySQL, it should work fine 32 | and if you plan on deploying to Heroku, the table we'll need for this app will work fine on both, so don't install Postgres 33 | unless you really want to. 34 | 35 | ```shell 36 | > rails new receta --skip-bundle --quiet --database=postgresql 37 | > cd receta 38 | ``` 39 | 40 | Now that we have our empty app, let's add a few basic gems, configure the 41 | database, and make sure we have a clean base from which to work. 42 | 43 | ## Basic gems 44 | 45 | We're going to use RSpec and Capybara here, as well as the Selenium driver for 46 | browser-based testing, so let's add those gems to our `Gemfile`. RSpec comes in via the `rspec-rails` gem, which also makes using RSpec in Rails a bit simpler. Note that we're also 47 | pinning version 3.2.19 of the `sass` gem. Currently, a bug in sprockets and/or SASS prevents 48 | everything from working as designed, so we need to stay on this version for now. 49 | 50 | ```diff 51 | diff --git a/Gemfile b/Gemfile 52 | index 90bd53a..75cb4c5 100644 53 | --- a/Gemfile 54 | +++ b/Gemfile 55 | @@ -21,12 +21,18 @@ gem 'coffee-rails', '~> 4.0.0' 56 | # Use jquery as the JavaScript library 57 | gem 'jquery-rails' 58 | 59 | -# Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks 60 | -gem 'turbolinks' 61 | - 62 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 63 | gem 'jbuilder', '~> 1.2' 64 | +gem 'sass', '3.2.19' 65 | +group :test, :development do 66 | + gem "rspec-rails", "~> 2.0" 67 | + gem "factory_girl_rails", "~> 4.0" 68 | + gem "capybara" 69 | + gem "database_cleaner" 70 | + gem "selenium-webdriver" 71 | +end 72 | + 73 | group :doc do 74 | # bundle exec rake doc:rails generates the API under doc/api. 75 | gem 'sdoc', require: false 76 | ``` 77 | 78 | Also note that for Capybara and Selenium to work, you must [install Firefox](https://www.mozilla.org/en-US/firefox/new/), as it's required for the browser-based testing to work. 79 | 80 | Capybara and 81 | Selenium should be unsurprising choices, as these are common means of doing 82 | integration/acceptance/browser tests. When creating a rich client 83 | application, browser testing is even more important than normal, so we want to make sure 84 | we easily have the ability to launch a browser and click around. 85 | 86 | RSpec is not the default testing framework with Rails, although it *is* popular. 87 | It's not required for Angular development, but I'm recommending it here because 88 | its API is similar to that of Jasmine, which we'll be using for testing our front-end code. 89 | 90 | This means the “shape” of your front-end tests will mirror those of your back-end tests, which will decrease your mental load as you 91 | switch back and forth. If you are philosophically opposed to RSpec, you are free to use MiniTest or whatever you want. 92 | 93 | We're also removing Turbolinks, since it's designed for a different type of 94 | application than that one you'd make with Angular. 95 | 96 | Now, we can install our gems: 97 | 98 | > bundle install 99 | 100 | Once this is done, let's set up our database so we can run the app. 101 | 102 | > vim config/database.yml # set the user/password for your local database 103 | > bin/rake db:create 104 | > rails s 105 | 106 | Visit http://localhost:3000 to make sure your app is running, then quit the 107 | server with `Ctrl-C`. 108 | 109 | Now that we have the Rails side of things squared away, let's set up Bower, 110 | which we'll use to manage our front-end library dependencies. 111 | 112 | ## Front-end dependency management with Bower 113 | 114 | Because Rails doesn't provide a way to manage front-end assets, such as JavaScript libraries, fonts, or CSS, the community has 115 | taken to bundling popular packages in RubyGems. Using the “Engines” feature of Rails, these assets can be placed in the asset 116 | pipeline by installing a RubyGem. Rails even does this with JQuery. 117 | 118 | We're not going to rely on RubyGems for our assets. We're going to use [Bower][bower] 119 | to manage them instead. Bower was created by Twitter specifically to manage front-end assets, and 120 | almost every imaginable library—including the Angular modules we'll be using—is available via Bower. 121 | 122 | [bower]: http://bower.io 123 | 124 | The same cannot be said for RubyGems. While it's nice that it's at least _possible_ to use 125 | RubyGems to manage front-end assets, doing so has two problems. 126 | 127 | First, it creates an abstraction between our `Gemfile` and our 128 | assets that creates confusion. What version of JQuery is bundled with Rails? In the Rails 4.0.3 app we just created, I see 129 | version 3.1.0 of the `jquery-rails` gem. According to the GitHub page for that gem, it bundles JQuery 1.10.2. This is 130 | unnecessarily confusing. Further, some gems don't even advertise the version of the asset they bundle. I've even seen one that 131 | bundles an off-release version of a JavaScript library. 132 | 133 | The second reason is actually more important, and that's that not every front-end asset is available as a RubyGem. This means 134 | that we will need a second system to manage *those*. And because we'll be using a lot more libraries and assets than in a 135 | “normal” Rails app, we need to keep an eye on the dependencies between these libraries. With some libraries managed as RubyGems 136 | and some in another system, we lose that ability. 137 | 138 | Bower can do all of this for us, and the `bower-rails` gem even provides a clean `Gemfile`-like way to declare our dependencies. 139 | 140 | First, we have to install Bower, which is a JavaScript command-line application, that you must install using `npm`, the "Node Package Manager". I am not 141 | making this up. 142 | 143 | Installing Node and NPM depends on your operating system. For Mac OS X, using [homebrew], it looks like this: 144 | 145 | ```shell 146 | > brew install node 147 | ``` 148 | 149 | If you aren't using homebrew, or aren't using OS X, you'll need to consult the [Node installation instructions][node-install]. 150 | 151 | [homebrew]: http://brew.sh/ 152 | [node-install]: http://nodejs.org/download/ 153 | 154 | Once you have NPM, install Bower as follows (might need sudo): 155 | 156 | ```shell 157 | > npm install -g bower 158 | ``` 159 | 160 | Now that you have Bower installed, we'll install the `bower-rails` gem, which will bridge our Rails application with Bower. Add `bower-rails` to our `Gemfile`: 161 | 162 | git://receta.git/Gemfile#add-bower^1..add-bower 163 | 164 | Now, let's install it. 165 | 166 | ```shell 167 | > bundle install 168 | ``` 169 | 170 | Bower works similarly to Bundler, and the Gem we just installed gives us a few 171 | handy tasks: 172 | 173 | sh://receta#rake -T bower 174 | 175 | Our dependencies go in a file called `Bowerfile`, located in the root directory of your project, 176 | that looks very much like a 177 | `Gemfile`. The dependencies we want to bring in first will be Angular and 178 | Twitter Bootstrap. Bootstrap isn't required, but we're going to use it here 179 | to keep the CSS we have to write to a minimum while still resulting in a 180 | pleasant-enough interface. 181 | 182 | git://receta.git/Bowerfile#add-bowerfile 183 | 184 | Now, we can install our dependencies via the `bower:install` Rake task: 185 | 186 | > rake bower:install 187 | bower.js files generated 188 | /usr/local/share/npm/bin/bower install 189 | bower bootstrap-sass-official#* not-cached git://github.com/twbs/bootstrap-sass.git#* 190 | bower bootstrap-sass-official#* resolve git://github.com/twbs/bootstrap-sass.git#* 191 | bower angular#* cached git://github.com/angular/bower-angular.git#1.2.13 192 | bower angular#* validate 1.2.13 against git://github.com/angular/bower-angular.git#* 193 | bower bootstrap-sass-official#* download https://github.com/twbs/bootstrap-sass/archive/v3.1.1.tar.gz 194 | bower angular#* new version for git://github.com/angular/bower-angular.git#* 195 | bower angular#* resolve git://github.com/angular/bower-angular.git#* 196 | bower angular#* download https://github.com/angular/bower-angular/archive/v1.2.14.tar.gz 197 | bower bootstrap-sass-official#* extract archive.tar.gz 198 | bower angular#* extract archive.tar.gz 199 | bower bootstrap-sass-official#* resolved git://github.com/twbs/bootstrap-sass.git#3.1.1 200 | bower angular#* resolved git://github.com/angular/bower-angular.git#1.2.14 201 | bower bootstrap-sass-official#* install bootstrap-sass-official#3.1.1 202 | bower angular#* install angular#1.2.14 203 | 204 | bootstrap-sass-official#3.1.1 bower_components/bootstrap-sass-official 205 | 206 | angular#1.2.14 bower_components/angular 207 | 208 | Bower installs dependencies in `vendor/assets/bower_components`, which you 209 | should check into your repository. 210 | 211 | > git add vendor/assets 212 | > git commit -m 'angular and bootstrap' 213 | 214 | This location may seem strange, but it allows you to separate Bower-managed 215 | third-party libraries from non-Bower-managed ones, if you should find the need 216 | for a library that isn't available via Bower (although that would be highly 217 | unusual). 218 | 219 | Since `vendor/assets/bower_components` *isn't* Rails standard, you'll need to 220 | add it to the asset path so these files get picked up. While we're here, we'll also 221 | add a few lines of configuration to get the glyphicons working while we're at it. 222 | 223 | We'll do this in `config/application.rb`, like so: 224 | 225 | git://receta.git/config/application.rb#angular-and-bootstrap..deal-with-glyphicons 226 | 227 | Lastly, you'll need to reference these files in `application.js` and 228 | `application.css.scss`, respectively (we'll also remove the reference to Turbolinks 229 | while we're here). First, rename `application.css` to `application.css.scss`, because we'll need to use SASS directives to get 230 | everything to work for Bootstrap's latest version (note that they frequently break things on minor versions, so please let me 231 | know if this no longer works). 232 | 233 | ```diff 234 | diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js 235 | index d6925fa..29f41b7 100644 236 | --- a/app/assets/javascripts/application.js 237 | +++ b/app/assets/javascripts/application.js 238 | @@ -12,5 +12,5 @@ 239 | // 240 | //= require jquery 241 | //= require jquery_ujs 242 | -//= require turbolinks 243 | +//= require angular/angular 244 | //= require_tree . 245 | diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css 246 | index 3192ec8..2cac3ad 100644 247 | --- a/app/assets/stylesheets/application.css.scss 248 | +++ b/app/assets/stylesheets/application.css.scss 249 | @@ -9,5 +9,6 @@ 250 | * compiled file, but it's generally better to create a new file per style scope. 251 | * 252 | *= require_self 253 | *= require_tree . 254 | */ 255 | + 256 | +@import "bootstrap-sass-official/assets/stylesheets/bootstrap-sprockets"; 257 | +@import "bootstrap-sass-official/assets/stylesheets/bootstrap"; 258 | 259 | ``` 260 | 261 | *(Note that previous version of `bootstrap-sass-official` would've required you to required `bootstrap.css` and not 262 | `_bootstrap.css`. Seems that the maintainers are flip-flopping on their naming conventions).* 263 | 264 | The reason these `require` lines are so long is due to an “impedance mismatch” 265 | between Bower and the Rails asset pipeline. Bower isn't much more than a 266 | simple way to download a Git repository, and there isn't much standardization 267 | across various front-end components. 268 | 269 | So, even though everything's installed in `vendor/assets/bower_components/ASSET_NAME`, 270 | what's underneath could be anything. 271 | To bring those into our app so they are served by the asset pipeline, we 272 | have to specify the exact file or files within the package. This often 273 | requires hunting around for what was downloaded to find the right path. 274 | Hopefully, a future version of Rails will provide some help here, but until 275 | then, Bower is a fairly clean solution. 276 | 277 | There is an alternative solution called [Rails Assets](https://rails-assets.org/), which 278 | converts any Bower package to a Rubygem on the fly, and integrates with your `Gemfile`. The 279 | reason we're not using that for this book is that Rails Assets does not handle assets packaged in a private repository, 280 | whereas Bower (and thus `bower-rails`) does. While our demo app doesn't use private 281 | assets, we want to present a complete solution that can handle any needs you're likely to have. At the time of this writing, Bower 282 | and `bower-rails` is it. 283 | 284 | Now that we have our initial set of front-end dependencies downloaded, let's 285 | write *just* enough code to use them, so we can be sure all the moving parts 286 | are working together. 287 | 288 | ## Tiniest Angular app ever 289 | 290 | We'll need a basic controller to render a view that will get Angular 291 | bootstrapped, and we'll need a tiny bit of JavaScript to create our Angular 292 | application. 293 | 294 | First, we'll add a new route to `routes.rb` to send our root route to a 295 | controller. 296 | 297 | git://receta.git/config/routes.rb#basic-setup-verified^^..basic-setup-verified 298 | 299 | We'll then create a basic controller called `HomeController` 300 | 301 | git://receta.git/app/controllers/home_controller.rb#basic-setup-verified 302 | 303 | We'll also need a barebones Angular app, which we'll put in `app.coffee`: 304 | 305 | git://receta.git/app/assets/javascripts/app.coffee#basic-setup-verified 306 | 307 | Finally, we'll create the view that uses some Bootstrap CSS as well as just 308 | enough Angular to demonstrate that it's working. In this case, we'll create a 309 | text field that, as we type, will update the heading of our panel. 310 | 311 | git://receta.git/app/views/home/index.html.erb#basic-setup-verified 312 | 313 | Now, when we run our app and visit the root URL, we should see our basic 314 | Angular app working. 315 | 316 | > rails s 317 | 318 | If you get the Rails error page with a message like so: 319 | 320 | couldn't find file 'bootstrap/glyphicons-halflings-regular.eot' 321 | 322 | This is because later versions of the bootstrap package include sprockets directives that bake in an assumption about how you've 323 | installed Bootstrap. Namely, the directives tell Rails to find the glyphicons fonts in `bootstrap/`, which doesn't exist in any of the asset paths we have configured. So, we must add it. 324 | 325 | Open `config/application.rb` and add this line: 326 | 327 | ```ruby 328 | config.assets.paths << Rails.root.join("vendor","assets","bower_components","bootstrap-sass-official","assets","fonts") 329 | ``` 330 | 331 | This may seem like more annoying configuration, but this is a one-time only activity that enables better project management for 332 | the life our application. It's a tradeoff, but a worthwhile one. 333 | 334 | Before we get too far into development, we need to deploy this to production. Since 335 | we've been making heavier-than-normal changes to asset-related aspects of our 336 | app, we should verify now that everything works in production mode. This way, 337 | as we do more and more front-end work and bring in more and more libraries 338 | and assets, we can can have a better idea of what went wrong if something 339 | *does* go wrong. 340 | 341 | ## Production deployment 342 | 343 | We're going to use [Heroku][heroku] to deploy. Heroku is a cloud-based “platform as a service” vendor that is free and easy to 344 | use. We can get a Rails app deployed with a small database for free just be doing a `git push`. The Heroku environment is also 345 | fairly unforgiving. If our application works on Heroku, it'll work anywhere. 346 | 347 | If you don't have a Heroku account, sign up for one (it's free). You should also install 348 | the [Heroku Tool Belt][heroku-cli], which will allow management of your app 349 | from the command line. 350 | 351 | [heroku]: http://heroku.com 352 | [heroku-cli]: https://toolbelt.heroku.com/ 353 | 354 | After signing up for Heroku, log in using the Tool Belt: 355 | 356 | ```shell 357 | heroku login 358 | ``` 359 | 360 | Once you're signed up and logged in, create a new application: 361 | 362 | ```shell 363 | heroku create 364 | ``` 365 | 366 | Before deploying, however, we need to add a few more gems to our `Gemfile`. 367 | These are mostly Heroku-specific, although they aren't hurting anything if you 368 | don't want to use Heroku. 369 | 370 | git://receta.git/Gemfile#..heroku-gems 371 | 372 | Install those gems, commit `Gemfile` and `Gemfile.lock`, and push to the 373 | Heroku repo 374 | 375 | > bundle install 376 | > git add Gemfile Gemfile.lock 377 | > git commit -m 'heroku deployment' 378 | > git push heroku master 379 | 380 | Since this is the first push to a new app, it will take a while. Once it's done, open your app in your browser. 381 | 382 | > heroku open 383 | 384 | You should see your app and it should work the same way it did locally. 385 | 386 | We've now completely validated that Bower, along with our configuration, will correctly bring in the assets we need to get 387 | started, in both development and production. 388 | 389 | At this point, we can start to actually build our application. The first 390 | thing we'll do is create the search screen that allows us to find and browse 391 | recipes. This will drive the vast majority of the configuration we need to get our app running with Angular. 392 | 393 | -------------------------------------------------------------------------------- /colophon.md: -------------------------------------------------------------------------------- 1 | # Colophon 2 | 3 | This site was written in [Markdown][markdown], using [vim][vim]. The web version was generated by [bookingit]. 4 | 5 | The title text is [P22 Underground Petite Caps][p22pc] with other headings set in [P22 Underground][p22]. The body 6 | text uses [Cronos Pro][cronos] with source code set in [Anonymous Pro][anon]. 7 | 8 | [markdown]: https://daringfireball.net/projects/markdown/ 9 | [vim]: http://vim.org 10 | [bookingit]: http://github.com/davetron5000/bookingit 11 | [p22pc]: https://typekit.com/fonts/p22-underground-petite-caps 12 | [p22]: https://typekit.com/fonts/p22-underground 13 | [cronos]: https://typekit.com/fonts/cronos-pro 14 | [anon]: https://typekit.com/fonts/anonymous-pro 15 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "AngularJS with Ruby on Rails", 3 | "subtitle": "Zero to deployment in less than 10,000 words", 4 | "author": "David Bryant Copeland", 5 | "url": "http://angular-rails.com", 6 | "license": { 7 | "type": "by-nc-sa", 8 | "typeDescription": "Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License" 9 | }, 10 | "front_matter": [ 11 | "front.md", 12 | "app_overview.md" 13 | ], 14 | "main_matter": [ 15 | "bootstrap.md", 16 | "find_and_browse.md", 17 | "view_recipe.md", 18 | "crud_recipe.md" 19 | ], 20 | "back_matter": [ 21 | "back.md", 22 | "about.md", 23 | "contributing.md", 24 | "colophon.md" 25 | ], 26 | "rendering": { 27 | "git_repos_basedir": "git_repos", 28 | "stylesheets": "styles.css", 29 | "languages": { 30 | }, 31 | "syntax_theme": "solarized_light" 32 | }, 33 | "typekit": { 34 | "id": "hiq5jip" 35 | }, 36 | "clicky": { 37 | "id": 100722319 38 | }, 39 | "favicon": true, 40 | "apple-mobile-web": true 41 | } 42 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This book is licensed under a Creative Commons Attribute-NonCommerical-ShareAlike license, and the source for the book can be found 4 | in [its repo][repo]. Feel free to submit pull requests to correct any mistakes you find. 5 | 6 | [repo]: https://github.com/davetron5000/angular-rails-book/tree/master 7 | 8 | ## Changes to code 9 | 10 | *Most* of the code in this tutorial comes from the [receta Rails app][receta-repo]. If you look at the Markdown source of this site, you'll see much of 11 | the code is pulled in like so: 12 | 13 | We'll do this in `config/application.rb`, like so: 14 | 15 | git: //receta.git/config/application.rb#angular-and-bootstrap..deal-with-glyphicons 16 | 17 | [bookingit][bookingit] is a custom application I've written that interprets this URL and produces diffs or shows code based on branches in the git 18 | repository. 19 | 20 | [receta-repo]: https://github.com/davetron5000/receta 21 | [bookingit]: https://github.com/davetron5000/bookingit 22 | 23 | So, the best way to submit a PR for code changes is to do so against receta's repo, **creating your PR from the branch in question**. So, if you wanted 24 | to change something about the above code, you would create your branch off of the `deal-with-glyphicons` branch. 25 | 26 | When in doubt, just ask, and if you submit a PR from master, that's OK, I can sort out merging it. I'd rather get your contribution than require you to 27 | learn advanced Git jockeying. 28 | 29 | ## Changes to everything else 30 | 31 | I'm so sorry for any typos and grammar issues—I've tried to proof this, but if you find something, I'm happy to accept your changes. 32 | 33 | I'm **not** looking for alternate ways to do things, or large changes in the narrative (though the license allows you to do that on your own, if you like!) 34 | 35 | When in doubt, open a PR with a question and we can start from there. 36 | -------------------------------------------------------------------------------- /crud_recipe.md: -------------------------------------------------------------------------------- 1 | # Running Through Remaining Features 2 | 3 | In the last section, we worked “inside out”, starting with `RecipeController` 4 | and integrating everything at the end, driven by a browser-based test. 5 | 6 | This time, let's work “outside in” by starting with a browser test that will mimic user behavior. We'll make a test that creates 7 | a recipe, edits it, and then deletes it, so we can test everything at once. 8 | 9 | git://receta.git/spec/features/edit_spec.rb#edit-recipe-test 10 | 11 | The test, of course, fails: 12 | 13 | git://receta.git/#edit-recipe-test!rspec spec/features/edit_spec.rb!nonzero 14 | 15 | To make it work, we'll need to: 16 | 17 | * add functionality to our back-end controller 18 | * add functionality to our Angular controllers 19 | * create views 20 | 21 | ## Rails controller 22 | 23 | First, we'll add some tests for our controller to get started: 24 | 25 | git://receta.git/spec/controllers/recipes_controller_spec.rb#..back-end-crud 26 | 27 | This is nothing but the happy path, mostly to speed us along here. 28 | 29 | Now, let's make these tests pass: 30 | 31 | git://receta.git/app/controllers/recipes_controller.rb#..back-end-crud 32 | 33 | You'll notice that we are skipping the authenticity token verification. While 34 | there is a way to insert the one that Rails generates into all HTTP requests, 35 | this is a one-time use token. If we make more than one HTTP POST to our 36 | app we'll need a new token each time. There currently isn't a good way 37 | to generate one, so we have to skip the check for this. 38 | 39 | Don't forget to add the routes for the new actions: 40 | 41 | git://receta.git/config/routes.rb#..back-end-crud 42 | 43 | Back to our test, everything seems to be working: 44 | 45 | git://receta.git/#back-end-crud!rspec spec/controllers/recipes_controller_spec.rb 46 | 47 | With the back-end working, let's turn our attention to the front end, which will call this code. 48 | 49 | ## Angular controllers 50 | 51 | We'll have `RecipeController` handle these operations. It has the `Recipe` resource we created using Angular's `$resource` 52 | service, so it should be pretty straightforward to add new functions. 53 | 54 | First, we'll create a test for creating, saving, and deleting. One wrinkle in our design is that for the case of creating a new 55 | recipe, there won't be an id in the URL, so we don't want to fetch anything from the backend. At this point, it just means we 56 | need to setup the controller so that there's no `recipeId` in the URL, and no expectation of doing a `GET` to the backend. 57 | 58 | Once we've changed `setupController()` accordingly, all we need to do is setup expectations of the HTTP calls to our backend, and 59 | then call the functions on `scope` that we'll need. 60 | 61 | git://receta.git/spec/javascripts/controllers/RecipeController_spec.coffee#..front-end-crud 62 | 63 | You'll notice that `save()` is handling both creating a new recipe and updating an existing one. This will make it easier to 64 | re-use our view for both purposes, since the “Save” button we'll create can just call `save()`, allowing the controller to work 65 | out just how to talk to the back-end. 66 | 67 | To make this test pass, we'll need to: 68 | 69 | * Check `$routeParams` for a recipe id and, if we don't find one, create an empty recipe 70 | * Override Angular's defaults for saving a resource to use the Rails-standard “PUT” 71 | * Implement `save()` and `delete()`. 72 | * Add a few methods to help with navigating 73 | 74 | Let's do it: 75 | 76 | git://receta.git/app/assets/javascripts/controllers/RecipeController.coffee#front-end-crud 77 | 78 | You'll notice we made a `create()` method when setting up our `Recipe` resource. By default, Angular allows both 79 | `recipe.$save()` and `Recipe.save(recipe)`, the latter being what we'd use to create a new recipe. Since Rails wants a `POST` for 80 | create and a `PUT` for update, we have to change the defaults. 81 | 82 | You'll also notice that we added a few functions for navigation. `edit()` takes the user to the edit form, and `cancel()` takes 83 | the user back to wherever makes sense based on the current operation. 84 | 85 | This implementation also shows a deviation from what we get with Rails. With Angular, there's no built-in way to route our code 86 | based on the which “CRUD” operation is being performed. We have to examine 87 | the inputs and figure it out for ourselves. 88 | 89 | Now, let's see if our tests pass: 90 | 91 | git://receta.git/#front-end-crud!rake teaspoon 92 | 93 | So far, so good. The only thing left is to create and wire up the views. 94 | 95 | ## Views 96 | 97 | As you could see from our test, we're going to create two new routes: `/recipes/new` and `/recipes/:recipeId/edit`, both handled 98 | by `RecipeController` and a new view, `form.html`. 99 | 100 | First, let's set up the routes in `app.coffee`: 101 | 102 | git://receta.git/app/assets/javascripts/app.coffee#..crud-views 103 | 104 | Next, we'll create `form.html`: 105 | 106 | git://receta.git/app/assets/javascripts/templates/form.html#crud-views 107 | 108 | Now, we'll need to add a link to create a recipe as well as one to edit in `index.html`: 109 | 110 | git://receta.git/app/assets/javascripts/templates/index.html#..crud-views 111 | 112 | These links require new functions called `newRecipe()` and `edit()`, which we'll add to `RecipesController.coffee`: 113 | 114 | git://receta.git/app/assets/javascripts/controllers/RecipesController.coffee#..b788ed96 115 | 116 | And lastly, we'll need links to edit and delete a recipe in `show.html`: 117 | 118 | git://receta.git/app/assets/javascripts/templates/show.html#..crud-views 119 | 120 | Whew! Let's see if it all works by re-running our browser-based test: 121 | 122 | git://receta.git/#crud-views!rspec spec/features/edit_spec.rb 123 | 124 | Voila! It works! 125 | 126 | ## Wrapping Up 127 | 128 | We went quite quickly through this part of the app, mostly to just see what an entire “CRUD” app would look like as well as some 129 | differences between what Rails gives us and what Angular doesn't. 130 | 131 | It may seem like we've written a lot of extra code and tests to do something that would be far simpler in Rails. In a sense, this 132 | is true, but what Angular lacks in creating CRUD applications, it more than 133 | makes up for when creating a richer user experience. 134 | 135 | The last section is going to be some reflection on what we've just learned and 136 | where we could go from here. 137 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davetron5000/angular-rails-book/32b73e1d2a894268d01c11dfc4dd8dacfe8feee5/favicon.ico -------------------------------------------------------------------------------- /find_and_browse.md: -------------------------------------------------------------------------------- 1 | # Building the First Feature 2 | 3 | Now that our application is set up with Angular and we have a way to manage 4 | assets, we can start building an actual feature. As mentioned earlier, we're 5 | going to use tests to drive our work, but since this is the first feature 6 | we're doing, it'll help to have a little bit of code in place before setting 7 | up testing. 8 | 9 | Although TDD is an effective practice, it is difficult to use it 10 | to drive the _creation_ of an application, especially where new technology is 11 | concerned. Doing so creates too many new changes at once, making it difficult 12 | to diagnose problems - did we mess up our testing configuration, or are there 13 | deeper problems with the application code under test? 14 | 15 | Instead of wrestling with these issues, we'll take a few baby steps, starting 16 | with our UI and just a bit of application code. We can verify this is working manually so that when we set up 17 | our testing environment, we can focus on getting that working without *also* focusing on making 18 | our application work. 19 | 20 | This is what we're going to do: 21 | 22 | 1. Create a basic search UI 23 | 2. Write a small amount of code to make it work 24 | 3. Set up our JavaScript testing environment 25 | 4. Connect the back-end 26 | 27 | ## Basic UI 28 | 29 | Since our application's views are not generally going to be served by Rails, the majority of our markup will live outside of a 30 | Rails view. With Angular, we'll map routes to views and controllers, like so: 31 | 32 | ```coffeescript 33 | app.config([ '$routeProvider', 34 | ($routeProvider)-> 35 | $routeProvider 36 | .when('/', 37 | templateUrl: "index.html" 38 | controller: 'SomeController' 39 | ) 40 | .when('/recipes/new', 41 | templateUrl: "new.html" 42 | controller: 'SomeOtherController' 43 | ) 44 | ]) 45 | ``` 46 | 47 | This presents us with somewhat of a problem. Angular is going to use the value of `templateUrl` to try to fetch the file we've 48 | specified via AJAX. This might actually work in development, but will certainly fail in production (and in subtle ways). 49 | This is because the asset pipeline works different in production mode. 50 | 51 | In production mode, the asset pipeline will control the path *and name* of assets it's serving. Specifically, it will generate a 52 | hash for each asset and include that hash in the name. That means that if Angular requests `/assets/index.html`, it will get a 53 | 404, because the file's actual name will be something like `/assets/9834f200909a098a0a9a-index.html`. More subtly, 54 | `/assets/index.html` would work in Rails 3, but no longer works in Rails 4. 55 | 56 | My initial search to solve this problem led me to a blog post (don’t actually do this) that recommended using ERB, so that `app.coffee` could have access 57 | to the `asset_path` helper, which accounts for the location and naming, like so: 58 | 59 | ```coffeescript 60 | # This is app/assets/javascripts/app.coffee.erb 61 | 62 | #= depend_on_asset index.html 63 | #= depend_on_asset new.html 64 | app.config([ '$routeProvider', 65 | ($routeProvider)-> 66 | $routeProvider 67 | .when('/', 68 | templateUrl: "<%= asset_path('index.html') %>" 69 | controller: 'SomeController' 70 | ) 71 | .when('/recipes/new', 72 | templateUrl: "<%= asset_path('new.html') %>" 73 | controller: 'SomeOtherController' 74 | ) 75 | ]) 76 | ``` 77 | 78 | As of Rails 4.0.3 there is a bug with Sprockets that prevents this from 79 | working—Sprockets won't see that your templates have changed and `app.js` 80 | won't be recompiled to reference the updated templates. 81 | 82 | Even if that bug goes away, this solution still isn't great. If you serve 83 | your assets from a content delivery network (CDN) the browser will be unable 84 | to access the templates at all. 85 | 86 | The reason is that Angular will request those assets at runtime, from the 87 | browser, and since your application isn't being served from your CDN, the 88 | browser, as a security measure, will refuse to allow Angular to read those 89 | assets. 90 | 91 | One solution to **that** problem is to configure Cross Origin 92 | Resource-Sharing (CORS), but this can be tricky to set up (or impossible, depending on your CDN). It is also very difficult to 93 | debug if it's not working properly. 94 | 95 | What we'd like to do is skip all of this entirely. Angular caches templates after it requests them the first time, so we really just 96 | need to pre-populate that cache. This way, Angular won't need to request *any* assets, thus eliminating both the asset pipeline 97 | problem as well as the same-origin security policy. 98 | 99 | Fortunately, the gem `angular-rails-templates` exists to do just that. 100 | 101 | Let's add it to our `Gemfile`: 102 | 103 | git://receta.git/Gemfile#..add-angular-templates 104 | 105 | Running `bundle install` will download the gem for us. **Note** there is currently an incompatibility 106 | between Sprockets 3.0 and angular-rails-templates. (see [this GitHub issue](https://github.com/pitr/angular-rails-templates/issues/93) for details). If you are on 0.1.4 or earlier, you should update your angular-rails-templates, which has a dependency on Sprockets 2.x. Or, you can downgrade Sprockets to 2.x manually in the Gemfile. 107 | 108 | In addition to this gem, we're also going to need the `angular-route` module, which enables the routing we saw above. We'll add 109 | it to our `Bowerfile` first: 110 | 111 | git://receta.git/Bowerfile#..add-angular-route 112 | 113 | Once we run `rake bower:install` to download `angular-route` and `angular-rails-templates`, we'll need to reference them in our `application.js` file so they're available to the app (note that older version of `angular-rails-templates` did not require referencing in the `application.js` file, so be sure you are on the latest version): 114 | 115 | ```diff 116 | diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js 117 | index 29f41b7..0c9339a 100644 118 | --- a/app/assets/javascripts/application.js 119 | +++ b/app/assets/javascripts/application.js 120 | @@ -13,4 +13,5 @@ 121 | //= require jquery 122 | //= require jquery_ujs 123 | //= require angular/angular 124 | +//= require angular-route/angular-route 125 | +//= require angular-rails-templates 126 | //= require_tree . 127 | ``` 128 | 129 | Now, let's write just enough Angular code to serve up a template, so we can work out the UI. The first thing to do is to replace 130 | `app/views/home/index.html.erb` with the markup needed to “boot” Angular when the view is rendered by Rails: 131 | 132 | git://receta.git/app/views/home/index.html.erb#static-template-renders 133 | 134 | The use of `ng-app` tells Angular which application should be loaded, and the `ng-view` directive tells it where to render views. The 135 | `view-container` div, along with the `view-frame` and `animate-view` classes can be used to add view transition animations if we 136 | want, but we can ignore them for now. 137 | 138 | Next, we need to implement the `receta` Angular app so it renders a view. Since we need the `angular-routes` module as well as 139 | the Angular module provided by `angular-rails-templates`, our app definition will need to include them as dependencies. We're 140 | also going to put our controllers in their own module, so the `controllers` module will be a third dependency. 141 | 142 | Our entire `app/assets/javascripts/app.coffee` now looks like so: 143 | 144 | git://receta.git/app/assets/javascripts/app.coffee#static-template-renders 145 | 146 | The last thing to do is to create `index.html`. `angular-rails-templates` will look for templates in 147 | `app/assets/javascripts/templates` by default, so we'll put the file there. It 148 | will initially just have static markup that demonstrates our UI. 149 | 150 | git://receta.git/app/assets/javascripts/templates/index.html#static-template-renders 151 | 152 | Once we start our app, we'll see the view rendered, which should look like so: 153 | 154 | ![Search UI](images/SearchUI.png) 155 | 156 | Now, let's get the front-end working. 157 | 158 | ## Code to make it work 159 | 160 | Normally, we'd start by writing a test, but since this is our very first feature, let's write the production code first so we 161 | don't have to be distracted by setting up the testing framework and its requisite configuration. 162 | 163 | Our controller is going to serve two purposes initially. First, it needs to respond to the “Search” button and conduct the 164 | search. Second, it needs to provide search results to be rendered. 165 | 166 | Since we don't have a back-end yet, we'll use canned data to get started. Further, since we know that the back-end will 167 | ultimately be conducting the search, we aren't going to design our controller and view around Angular filters. Instead, we'll 168 | simply expose the attribute `recipes` that will contain whatever the search results happen to be. 169 | 170 | Finally, we'll design the controller to look for a url parameter called “keywords” and, if present, use that to conduct a search, 171 | as opposed to doing the search in the `search()` action. The `search()` action will simply route the application to `/` with 172 | the keywords in the query string. This allows our search results to be bookmarkable, which a very nice thing to do for our users. 173 | 174 | Let's look at our controller code. 175 | 176 | git://receta.git/app/assets/javascripts/app.coffee#front-end-canned-search 177 | 178 | First, we put in a canned list of recipes to search through. Next, we've filled out the controller with the basics of 179 | implementing the search. Since we need access to the query string, as well as the ability to change the current route/url, we'll 180 | add `$routeParams` and `$location` to our controller's dependencies. 181 | 182 | Note the form of Angular dependency-injection we're using. If we used name-based injection, like so 183 | 184 | ```coffeescript 185 | controllers.controller('RecipesController', 186 | ($scope,$routeParams,$location)-> 187 | ``` 188 | 189 | The function's argument names would be lost during minification. Meaning, everything would work great in development and not 190 | work at all in production. 191 | It's a bit more verbose to do it with the string array, but 192 | it's guaranteed to work through the asset pipeline and any minification or obfuscation that happens to the JavaScript. 193 | 194 | The controller itself isn't terribly exciting. Our `search()` function simply re-routes to ourself with the keywords in the 195 | query string, and the controller's body does a simple substring lookup of our canned data (this line will ultimately change to 196 | talk to the backend). 197 | 198 | Now, let's look at our view. We need to bind the value of the search field to a model, and use that value as the argument to 199 | search, which we must trigger from the “Search” button. We also will remove our duplicated markup in favor of markup for one 200 | result, wrapped in an `ng-repeat` directive. 201 | 202 | git://receta.git/app/assets/javascripts/templates/index.html#..front-end-canned-search 203 | 204 | Note that we're also using `ng-if` to hide the results section entirely if there aren't any results. 205 | 206 | Now when we reload the page, our search works! 207 | 208 | ![Search UI Working](images/SearchUIWorking.png) 209 | 210 | Before we move on, let's deploy to production to make sure that all of our configuration around the Angular templates is working 211 | when the asset pipeline is in production mode. 212 | 213 | ```shell 214 | > git push heroku master 215 | > heroku open 216 | ``` 217 | 218 | Once your browser opens, you should see the app working the same as it did in your environment–our configuration is good. 219 | 220 | Before we write too much more code, we need to get some tests in place. First, we'll create a browser-based acceptance test to verify that 221 | the search feature works. That test won't be coupled to how the app is implemented, so we can use it to validate that we've hooked up the backend correctly. We'll use unit tests of our Angular controller to drive that development. 222 | 223 | ## Tests 224 | 225 | To conduct our browser-based tests, we'll use Capybara and Selenium. First, we'll need to create `spec/spec_helper.rb` to setup 226 | and configure our back-end and browser-based tests. 227 | 228 | git://receta.git/spec/spec_helper.rb#search-feature-spec 229 | 230 | There's nothing particularly special in here, just what RSpec would normally set up for you. The only different bit is toward 231 | the end, where we change how the database is managed for browser-based tests. Normally, tests run in a database transaction 232 | that's rolled back after the test completes. For the browser-based tests, that won't work because the browser is running in a 233 | different process and can't see the effects of database changes that are in an uncommitted transaction. 234 | 235 | With that set up, we'll create a simple feature spec to test the search: 236 | 237 | git://receta.git/spec/features/search_spec.rb#search-feature-spec 238 | 239 | Since our test relies on the canned data that we hard-coded, it passes. 240 | 241 | git://receta.git/#search-feature-spec!rake spec 242 | 243 | With this in place, we can now connect our front-end to the back-end, set up the same canned data from within our feature spec, and 244 | expect the exact same results. 245 | 246 | ## Back-end 247 | 248 | We have three main steps to connect our Angular controller to our backend: 249 | 250 | 1. Implement the search on the backend to return results in JSON 251 | 2. Have our Angular controller make an AJAX request to our backend 252 | 3. Have our feature spec insert the same test data that our Angular controller is currently hard-coding 253 | 254 | ### Implement the search 255 | 256 | Since this isn't a Rails tutorial, we're going to go pretty fast here. We're going to need a `Recipe` model, a 257 | `RecipesController`, and a JSON view of the search results. 258 | 259 | A Recipe is just going to be a name and some text, so we'll use the Rails generator to get us going. We're going to skip 260 | fixtures (and FactoryGirl) for now. 261 | 262 | ```shell 263 | > rails g model Recipe name:string instructions:text --no-fixture --no-fixture-replacement 264 | invoke active_record 265 | create db/migrate/20140309184135_create_recipes.rb 266 | create app/models/recipe.rb 267 | invoke rspec 268 | create spec/models/recipe_spec.rb 269 | > rake db:migrate 270 | > rake db:migrate RAILS_ENV=test 271 | ``` 272 | 273 | Our `Recipe` model doesn't have any functionality, so we'll leave the auto-generated test alone. What we need now is a 274 | controller. Let's use the rails generator again to create the necessary files. 275 | 276 | Since there won't be an HTML view, there's no reason to have view specs, helpers, or assets, so we'll skip those when we run `rails g` 277 | 278 | ```shell 279 | > rails g controller recipes index --no-view-specs --no-helper --no-assets 280 | ``` 281 | 282 | The generator creates a silly 283 | entry in our `routes.rb` file, so we'll replace it with a more resource-oriented configuration: 284 | 285 | git://receta.git/config/routes.rb#create-recipe-controller 286 | 287 | To implement the `index` method, we need two tests - one that should expect results and one that won't. We'll also need to 288 | tell our spec to render the views, so that we can parse the JSON that's returned and make sure it looks good. 289 | 290 | git://receta.git/spec/controllers/recipes_controller_spec.rb#recipes-index 291 | 292 | We'll then make the test pass by doing the search in the controller, and create the appropriate JSON views. First, the 293 | controller: 294 | 295 | git://receta.git/app/controllers/recipes_controller.rb#recipes-index 296 | 297 | We're not explicitly looking for any particular format. Since we'll be requesting JSON, Rails will try to find a view that can 298 | serve up JSON. To do that, we'll create a view using [JBuilder]. Even though it might be a bit more code than allowing Rails to 299 | convert the `Recipe` to JSON using its built-in serializers, I find using JBuilder creates a nice separation point between the 300 | front-end and back-end. This gives us flexibility to write idiomatic code on both sides, even when those idioms diverge (for 301 | example, Angular code tends to favor camel-case, whereas Ruby tends to favor snake-case). 302 | 303 | [JBuilder]: https://github.com/rails/jbuilder 304 | 305 | First, we'll create `app/views/recipes/index.json.jbuilder`: 306 | 307 | git://receta.git/app/views/recipes/index.json.jbuilder#recipes-index 308 | 309 | All this does is defer each recipe to a partial, which is in `app/views/recipes/_recipe.json.jbuilder` and looks like so: 310 | 311 | git://receta.git/app/views/recipes/_recipe.json.jbuilder#recipes-index 312 | 313 | With all this in place, our test passes: 314 | 315 | git://receta.git/#recipes-index!rake db:migrate RAILS_ENV=test ; rspec spec/controllers/recipes_controller_spec.rb 316 | 317 | Now that we have our back-end implemented, let's hook it up by having our Angular controller call it. 318 | 319 | ### Have angular controller call the back-end 320 | 321 | Since this is the first real code we're writing in Angular, this is where we'll set up our unit testing. We're going to use 322 | `teaspoon`, which is a test runner for JavaScript that uses the asset pipeline, Jasmine, and PhantomJS. We'll add teaspoon and 323 | PhantomJS to our `Gemfile` (we're also going to remove a bunch of silly comments and gems that we don't 324 | need): 325 | 326 | git://receta.git/Gemfile#75552b7..setup-teaspoon 327 | 328 | Once we run `bundle install`, we need to bootstrap teaspoon, which can be done with the Rails generator it includes: 329 | 330 | ```shell 331 | > rails generate teaspoon:install --coffee 332 | ``` 333 | 334 | This gets us most of the way there, however we may experience issues running our JavaScript tests later, especially if you are on Rails 4.1 or later. In Rails 335 | 4.1, the asset pipeline became a lot fussier about missing assets. This is generally a good thing because it gives us better confidence that our assets 336 | will work in production. The problem is that teaspoon—even in command-line mode—runs JavaScript and CSS through PhantomJS, which is acting as a web 337 | browser, and the asset pipeline may produce an error that it can't find `teaspoon.css`. 338 | 339 | At the time of this writing, there is no good solution other than to simply add those files to `config/initializers/assets.rb`: 340 | 341 | ```ruby 342 | Rails.application.config.assets.precompile += %w( 343 | teaspoon.css 344 | teaspoon-teaspoon.js 345 | teaspoon-jasmine.js 346 | ) 347 | ``` 348 | 349 | Note that depending on the point release of Rails, Sprockets, and Teaspoon, you may either not see this error, or may see different errors. As of now, 350 | the best thing to do is keep adding files to `config/initializers/assets.rb` until the errors stop. I'm sorry. 351 | 352 | Back to the task at hand, we're also going to need two more Angular modules. We need `angular-mocks` to help with testing and `angular-resource` to 353 | implement the AJAX calls. First, we add them to `Bowerfile`: 354 | 355 | git://receta.git/Bowerfile#75552b7..setup-teaspoon 356 | 357 | Once we run `rake bower:install` to download them, we need to add `angular-resource` to `application.js`: 358 | 359 | git://receta.git/app/assets/javascripts/application.js#75552b7..setup-teaspoon 360 | 361 | Finally, we'll need to include `ngResource`—the module provided by `angular-resource`—in our `app.coffee`. While we're there, we'll inject 362 | `$resource`—the function bundled in the `ngResource` module—into our controller: 363 | 364 | git://receta.git/app/assets/javascripts/app.coffee#75552b7..setup-teaspoon 365 | 366 | Since `angular-mocks` is only needed for tests, we *won't* put it in `application.js`. Teaspoon allows Sprockets directives in 367 | our test files, and it generated `spec/javascripts/spec_helper.coffee` for us, which is included in all tests. We'll add this 368 | line to the file: 369 | 370 | ```coffeescript 371 | #= require angular-mocks/angular-mocks 372 | ``` 373 | 374 | The last step in setting up our front-end testing is to write a basic spec that uses our controller. The boilerplate for this 375 | test is somewhat substantial compared to a Rails controller test. This is a function both of JavaScript as a language and the 376 | way Angular is designed. 377 | 378 | Since all Angular modules are functions that are given their dependencies, when we test those modules, we'll want to intercept 379 | those dependencies so we can use them in our tests. Although we could create mocks for many of our controller dependencies, 380 | Angular provides mock implementations for us, and will pass those, by 381 | default, to our controller. 382 | 383 | If we need to examine them in a test (for example to assert that our 384 | controller set the location to a particular path) we'll need to get a 385 | reference to those mock instances. At the very least, we need access to `$scope`, which is 386 | how our controller provides data to the views (think of it as the `assigns` of 387 | an Angular controller test). 388 | 389 | To do this, we'll create a function called `setupController()` that uses the 390 | Angular-provided method `$inject`, which will allow us access to the mock 391 | dependencies. We'll call this method in a `beforeEach` so our controller is 392 | always ready to go before each test. 393 | 394 | git://receta.git/spec/javascripts/controllers/RecipesController_spec.coffee#setup-teaspoon 395 | 396 | We need to declare our variables at the top so their references can “escape” 397 | the closure created by `$inject`. Also note that the reason we're making a 398 | function, and not just putting this code at a top-level `beforeEach` is that 399 | different tests will need to manipulate the `routeParams`, and that can only 400 | be done during injection. 401 | 402 | 403 | By the time our test runs, the controller will be up and running as if the user had just visited the page. In this case, 404 | `recipes` will be empty, so we assert that. Notice that we can't use `expect(scope.recipes).toBe([])` because `toBe` creates a 405 | very strong requirement that the result and the expected value are identical objects. Instead, we're using `toEqualData`, which 406 | is a matcher we've created in `spec_helper.coffee`: 407 | 408 | git://receta.git/spec/javascripts/spec_helper.coffee#setup-teaspoon 409 | 410 | This does a “value” match, which will make our lives much easier when 411 | asserting equality between objects. 412 | 413 | Note that this has changed since Teaspoon 1.x came out. Teaspoon 1.x requires Jasmine 2.x, and the way 414 | custom matchers are defined in Jasmine 2.x is different from what was here previously. 415 | 416 | Let's run our JavaScript tests to validate our setup. 417 | 418 | git://receta.git/#setup-teaspoon!rake teaspoon 419 | 420 | Everything works! Now, we just need a test that our controller calls the backend. 421 | 422 | Our controller will need three tests: 423 | 424 | * Check that on initialization with no keywords, `recipes` is empty (i.e. the test we already have) 425 | * Check that on initialization *with* keywords, we call the backend and populate `recipes` with the results 426 | * Verify that clicking `search()` redirects us back to ourselves with the correct query string 427 | 428 | In order to test that we call the backend, we'll ask Angular to give us access to the mock HTTP backend it uses during tests. We 429 | can use that mock instance to mock a real back-end response. 430 | 431 | Because the initialization of our controller will make this call, we'll need to set up our mock expectations inside 432 | `setupController()`. We'll add a second parameter—`results`—and arrange for `$httpBackend` to return it when the right AJAX request is made. 433 | 434 | ```coffeescript 435 | describe "RecipesController", -> 436 | scope = null 437 | ctrl = null 438 | location = null 439 | routeParams = null 440 | resource = null 441 | 442 | # access injected service later 443 | httpBackend = null 444 | 445 | setupController = (keywords,results)-> 446 | inject(($location, $routeParams, $rootScope, $resource, $httpBackend, $controller)-> 447 | scope = $rootScope.$new() 448 | location = $location 449 | resource = $resource 450 | routeParams = $routeParams 451 | routeParams.keywords = keywords 452 | 453 | # capture the injected service 454 | httpBackend = $httpBackend 455 | 456 | if results 457 | request = new RegExp("\/recipes.*keywords=#{keywords}") 458 | httpBackend.expectGET(request).respond(results) 459 | 460 | ctrl = $controller('RecipesController', 461 | $scope: scope 462 | $location: location) 463 | ) 464 | ``` 465 | 466 | We also need to tell `httpBackend` to verify that there are no unmet expectations nor are there unexpected requests: 467 | 468 | ```coffeescript 469 | afterEach -> 470 | httpBackend.verifyNoOutstandingExpectation() 471 | httpBackend.verifyNoOutstandingRequest() 472 | ``` 473 | 474 | Finally, we write our tests. We'll set up two contexts, “controller initialization” and “search()”. In “controller 475 | initialization”, we have two tests. The first is the one we already have: 476 | 477 | 478 | ```coffeescript 479 | describe 'controller initialization', -> 480 | describe 'when no keywords present', -> 481 | beforeEach(setupController()) 482 | 483 | it 'defaults to no recipes', -> 484 | expect(scope.recipes).toEqualData([]) 485 | ``` 486 | 487 | The second is our back-end test. We'll call `setupController()` with the keywords we want in the `routeParams` and the list of 488 | recipes to return from the back-end: 489 | 490 | ```coffeescript 491 | describe 'with keywords', -> 492 | keywords = 'foo' 493 | recipes = [ 494 | { 495 | id: 2 496 | name: 'Baked Potatoes' 497 | }, 498 | { 499 | id: 4 500 | name: 'Potatoes Au Gratin' 501 | } 502 | ] 503 | beforeEach -> 504 | setupController(keywords,recipes) 505 | httpBackend.flush() 506 | 507 | it 'calls the back-end', -> 508 | expect(scope.recipes).toEqualData(recipes) 509 | ``` 510 | 511 | The call to `httpBackend.flush()` resolves all asynchronous promises. We'll expect this to set the controller's `recipes` to the 512 | recipes we passed to `setupController()`. 513 | 514 | Finally, we test that `search()` sets the URL correctly: 515 | 516 | ```coffeescript 517 | describe 'search()', -> 518 | beforeEach -> 519 | setupController() 520 | 521 | it 'redirects to itself with a keyword param', -> 522 | keywords = 'foo' 523 | scope.search(keywords) 524 | expect(location.path()).toBe('/') 525 | expect(location.search()).toEqualData({keywords: keywords}) 526 | ``` 527 | 528 | (note that `location.search()` has nothing to do with our controller method called `search()`. `location.search()` represents the query string as a JavaScript object) 529 | 530 | Let's run the test and watch it fail: 531 | 532 | git://receta.git/#angular-recipes-controller-test!rake teaspoon!nonzero 533 | 534 | The test is failing just how we'd expect: there are no HTTP requests to flush, `recipes` is empty, and there is an unmet expected HTTP request. Now, let's fix it. 535 | 536 | The `angular-resource` module makes this very simple. We create a resource for our recipes, and then call the `query()` method 537 | (generated for us by Angular). 538 | 539 | git://receta.git/app/assets/javascripts/app.coffee#angular-controller-test-pass^1..angular-controller-test-pass 540 | 541 | Now, let's re-run our tests: 542 | 543 | git://receta.git/#angular-controller-test-pass!rake teaspoon 544 | 545 | Everything's passing! At this point, our front-end will call through to our 546 | Rails controller, parse the JSON it gets, and display the right results. Our 547 | browser-based test can assert this for us, provided it sets up the database 548 | the same way we initially faked the data in our Angular controller. 549 | 550 | ### Re-run our feature spec 551 | 552 | We can now remove `recipes` from `app.coffee` since we're talking to the real back-end. The only problem is that `search_spec.rb` 553 | is relying on that data being there to verify the feature is working. All we have to do is populate the database with the same 554 | data. We'll create it in a `before` block. 555 | 556 | git://receta.git/spec/features/search_spec.rb#backend-integrated^1..backend-integrated 557 | 558 | Now, if everything's working, our test should still pass. 559 | 560 | git://receta.git/#backend-integrated!rspec spec/features/search_spec.rb 561 | 562 | It's passing! We've now successfully test-driven our first feature with Angular! 563 | 564 | But, the TDD cycle isn't complete. We've gone from a failing test (red) to a passing one (green), but we haven't refactored, yet. 565 | 566 | The code we've written is minimal, however the implementation of `RecipesController` doesn't belong in `app.coffee`. We'd like 567 | our front-end code to be organized like our back-end code, with different modules in different files. 568 | 569 | Let's extract the implementation of `RecipesController` out of 570 | `app.coffee` and into `app/assets/javascripts/controllers/RecipesController.coffee`. If we do that, and our test passes, it's a 571 | good refactor. 572 | 573 | First, we'll remove the controller from `app.coffee`, but notice that we leave in the module declaration: 574 | 575 | git://receta.git/app/assets/javascripts/app.coffee#extract-controller-to-file^1..extract-controller-to-file 576 | 577 | We'll place the controller code in `app/assets/javascripts/controllers/RecipesController.coffee`, and notice that we have _repeated_ the module declaration (or so it seems): 578 | 579 | git://receta.git/app/assets/javascripts/controllers/RecipesController.coffee#extract-controller-to-file 580 | 581 | Because the asset pipeline wraps 582 | all CoffeeScript files in self-executing functions, our controller file won't have access to the `controllers` module we declared 583 | in `app.coffee`. We can still get access to it by calling `angular.module` in the controller file. Since it will be evaluated *after* `app.coffee`, we can be sure the module itself exists inside the bowels of Angular. The important 584 | difference is that in our controller file, we omit the second parameter. This is how Angular knows we just want access to the 585 | previously-declared module and aren't trying to declare a new module that has 586 | the same name (which, incidentally, generates a runtime error). 587 | 588 | If all went well, our tests should still be passing. First, we run our unit tests: 589 | 590 | git://receta.git/#backend-integrated!rake teaspoon 591 | 592 | Now, run our feature test: 593 | 594 | git://receta.git/#backend-integrated!rspec spec/features/search_spec.rb 595 | 596 | Wonderful! We now have a repeatable process for test-driving our Angular-and-Rails-powered web application. 597 | 598 | In the next chapter, we'll use what we've set up to test-drive the “view” feature. 599 | -------------------------------------------------------------------------------- /front.md: -------------------------------------------------------------------------------- 1 | # AngularJS, Rails, and Production 2 | 3 | The latest wave of JavaScript frameworks present an interesting problem to experienced Rails developers. On their own, these 4 | frameworks can only do so much, since they are designed to run on the client-side, in a browser. This means that for 5 | any real application you might create, you need a back-end. 6 | 7 | Rails is a terrific back-end, however it wasn't designed with a JavaScript framework like AngularJS in mind. Rails' view of the world is serving up pages and HTML. Rich Javascript-based single-page applications aren't something Rails encourages. 8 | 9 | [AngularJS][angular] is a terrific front-end. It supports testability, clean code organization, and has a simple object model. It's supremely powerful but, of 10 | course, wasn't designed with Rails in mind—it's back-end agnostic. 11 | 12 | [angular]: http://angularjs.org 13 | 14 | The result is that there is no “Rails Way” for managing the front-end assets needed to create a rich JavaScript application using AngularJS. 15 | 16 | This mini-book is going to change that. We're going to see step-by-step instructions for building a Rails app with Angular, and 17 | getting it deployed to production. That means we'll properly configure the asset pipeline, provide a solution for managing 18 | front-end assets, and see how to integrate testing the front-end code into your app. 19 | 20 | This is not a Rails or Angular tutorial. If you'd like to learn Angular, I would suggest completing their [tutorial]. Don't just read it, type out the code. Run the code. Tweak the code. It only takes an hour or so, and it'll give the confidence you need to start using Angular (that's what I did). 21 | 22 | Once you're familiar enough with Angular to start building an app, you'll find there are a lot of unanswered questions: 23 | 24 | [tutorial]: http://docs.angularjs.org/tutorial/step_00 25 | 26 | * How do I serve Angular assets via the Rails Asset Pipeline? 27 | * How does Angular's templating system work with Rails? 28 | * How do I connect Angular to my Rails controllers? 29 | * How do I test my Angular code within a Rails app? 30 | * How do I manage all these JavaScript dependencies? 31 | * What challenges will there be in the production environment not present in the development environment? 32 | 33 | I was able to find answers by piecing together information from Stack Overflow and various blog posts. You shouldn't have to do that. 34 | 35 | ## How this book is organized 36 | 37 | This mini-book is broken up into four major parts: 38 | 39 | * Creating a basic skeleton to validate Angular is setup and configured 40 | * A step-by-step walkthrough of a simple feature 41 | * A more brisk walkthrough of using TDD to implement a feature 42 | * A very fast runthrough of building a few more features 43 | 44 | The technologies we'll be using: 45 | 46 | * CoffeeScript 47 | * RSpec (for back-end tests) 48 | * Jasmine (for front-end tests) 49 | * Capybara & Selenium (for browser-based acceptance tests) 50 | * Bower (to manage front-end dependencies) 51 | * Twitter Bootstrap (for styling–not required, but simplifies this tutorial) 52 | * Heroku (for deployment–not required, but is an easy way to validate production behavior) 53 | 54 | When we're done, you'll know exactly what you need to do to get Angular into your Rails app in a way that supports sustained 55 | development and deployment. You'll be able to get to your code more quickly and confidently. 56 | 57 | ## What you'll need 58 | 59 | The code in this book is pulled from a [git repository][repo] containing the Rails app 60 | we'll be building. The console output we'll see is generated directly from 61 | that code, so you should be able to follow along easily. 62 | 63 | [repo]: http://github.com/davetron5000/receta 64 | 65 | I'd still recommend you follow along at your terminal and editor. Even though you will 66 | just be typing in what you see, it'll get your fingers used to the workflow 67 | and code involved in a a Rails-powered Angular app. 68 | 69 | All you'll need: 70 | 71 | * Ruby 2 or later 72 | * Rails 4.2 (The code examples should work on Rails 4.0 and 4.1, but it's easier to keep this updated to the latest version of Rails) 73 | * Some sort of database installed (I'd recommend Postgres, but MySQL should work fine) 74 | * An account on [Heroku], where we'll deploy the app 75 | 76 | The app we'll see in this book was originally developed on OS X, so some terminal 77 | commands might be different if you are on Windows. Please [let me know][contact] 78 | what those differences are. 79 | 80 | [Heroku]: http://heroku.com 81 | [contact]: http://github.com/davetron5000 82 | 83 | With that out of the way, let's get started! 84 | -------------------------------------------------------------------------------- /images/FlashMessage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davetron5000/angular-rails-book/32b73e1d2a894268d01c11dfc4dd8dacfe8feee5/images/FlashMessage.png -------------------------------------------------------------------------------- /images/SearchUI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davetron5000/angular-rails-book/32b73e1d2a894268d01c11dfc4dd8dacfe8feee5/images/SearchUI.png -------------------------------------------------------------------------------- /images/SearchUIWorking.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davetron5000/angular-rails-book/32b73e1d2a894268d01c11dfc4dd8dacfe8feee5/images/SearchUIWorking.png -------------------------------------------------------------------------------- /main.md: -------------------------------------------------------------------------------- 1 | # Main stuff here 2 | -------------------------------------------------------------------------------- /original_styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | width: 634px; 3 | margin: 0 auto; 4 | overflow-x: hidden; 5 | background: #efffef; 6 | background: -webkit-linear-gradient(top, #fff, #dfefdf); 7 | background-repeat: no-repeat; 8 | background-attachment: fixed; 9 | } 10 | 11 | body, p, li, td { 12 | font-family: 'Baskerville', serif; 13 | font-size: 18px; 14 | line-height: 1.4; 15 | } 16 | .page { 17 | background: white; 18 | padding: 18px; 19 | -webkit-box-shadow: 0 0 4px rgba(0, 0, 0, 0.2); 20 | -moz-box-shadow: 0 0 4px rgba(0, 0, 0, 0.2); 21 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.2); 22 | } 23 | 24 | @media all and (max-width : 700px) { 25 | body { 26 | width: 90%; 27 | } 28 | body, p, li, td { 29 | font-size: 22px; 30 | } 31 | } 32 | 33 | a, a:visited { 34 | color: black; 35 | } 36 | 37 | a:hover { 38 | color: #232; 39 | } 40 | 41 | .toc a,.toc a:visited { 42 | text-decoration: none; 43 | } 44 | .toc a:hover { 45 | text-decoration: underline; 46 | } 47 | 48 | .toc ol ol { 49 | list-style: lower-alpha; 50 | } 51 | .toc ol li ol li { 52 | line-height: 1.2; 53 | } 54 | 55 | ol.front-matter { 56 | list-style: upper-roman; 57 | margin-bottom: 0; 58 | } 59 | ol.main-matter { 60 | margin-top: 0; 61 | margin-bottom: 0; 62 | list-style: decimal; 63 | } 64 | ol.back-matter { 65 | margin-top: 0; 66 | list-style: upper-alpha; 67 | } 68 | 69 | li.chapter { 70 | font-size: 110%; 71 | } 72 | li.section { 73 | font-size: 100%; 74 | } 75 | 76 | p.byline { 77 | text-align: left; 78 | font-size: 90%; 79 | margin-top: 0; 80 | padding-top: 0; 81 | } 82 | 83 | header { 84 | background: black; 85 | color: white; 86 | padding-left: 18px; 87 | padding-right: 18px; 88 | padding-bottom: 12.6px; 89 | padding-top: 25.2px; 90 | margin-left: -18px; 91 | margin-right: -18px; 92 | margin-top: -18px; 93 | } 94 | 95 | header h1 { 96 | margin-top: 0; 97 | color: white; 98 | font-family: 'freight-sans-pro', 'Avenir', sans-serif; 99 | } 100 | 101 | h1 { 102 | font-weight: 700; 103 | font-size: 40px; 104 | margin-bottom: 0; 105 | } 106 | 107 | header h1 a, header h1 a:visited { 108 | text-decoration: none; 109 | color: white; 110 | } 111 | h2, h3, h4, h5, h5 { 112 | font-family: 'Avenir', sans-serif; 113 | } 114 | 115 | h3 { 116 | border-bottom: solid thin #888; 117 | font-size: 20px; 118 | } 119 | 120 | h2 { 121 | text-align: center; 122 | } 123 | 124 | h2.subtitle { 125 | border-bottom: none; 126 | font-family: 'freight-sans-pro', sans-serif; 127 | font-weight: normal; 128 | font-size: 24px; 129 | font-style: italic; 130 | text-align: center; 131 | } 132 | 133 | pre,code { 134 | font-family: "anonymous-pro", monospace; 135 | } 136 | 137 | @media all and (max-width : 700px) { 138 | code { 139 | word-wrap: break-word; 140 | } 141 | article.code-listing code { 142 | word-wrap: normal; 143 | } 144 | } 145 | 146 | code { 147 | border: solid thin #777; 148 | background: #ddd; 149 | padding-left: 4px; 150 | padding-right: 4px; 151 | border-radius: 5px; 152 | } 153 | 154 | pre { 155 | margin: 0; 156 | } 157 | 158 | article.code-listing { 159 | border-radius: 9px; 160 | padding: 9px; 161 | overflow-x: hidden; 162 | } 163 | 164 | article.code-listing footer h1{ 165 | font-size: 12px; 166 | font-family: "anonymous-pro", monospace; 167 | border-bottom: none; 168 | margin: 0; 169 | padding: 0; 170 | text-align: right; 171 | } 172 | 173 | pre code { 174 | border-radius: 9px; 175 | border: dashed thin #657b83; 176 | padding: 0; 177 | font-size: 14px; 178 | overflow-x: scroll; 179 | } 180 | 181 | pre code:hover { 182 | border: solid thin #657b83; 183 | } 184 | 185 | pre code[class~='language-shell'] { 186 | background: black; 187 | background: -webkit-linear-gradient(top, #000, #232); 188 | background-repeat: no-repeat; 189 | background-attachment: fixed; 190 | color: white; 191 | border: none; 192 | } 193 | 194 | img { 195 | width: 100%; 196 | display: block; 197 | position: relative; 198 | background-color: #fff; 199 | -webkit-box-shadow: 0 0 4px rgba(0, 0, 0, 0.2), inset 0 0 50px rgba(0, 0, 0, 0.1); 200 | -moz-box-shadow: 0 0 4px rgba(0, 0, 0, 0.2), inset 0 0 50px rgba(0, 0, 0, 0.1); 201 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.2), inset 0 0 50px rgba(0, 0, 0, 0.1); 202 | } 203 | 204 | footer ol { 205 | list-style: none; 206 | padding: 0; 207 | margin: 0; 208 | } 209 | footer ol li { 210 | display: inline; 211 | } 212 | footer ol li.previous { 213 | float: left; 214 | } 215 | footer ol li.next { 216 | float: right; 217 | } 218 | footer .clearfix { 219 | clear: both; 220 | } 221 | footer .copyright { 222 | font-size: 80%; 223 | } 224 | footer .copyright img { 225 | width: auto; 226 | } 227 | -------------------------------------------------------------------------------- /sass/colors.scss: -------------------------------------------------------------------------------- 1 | $paper: white; 2 | $ink: black; 3 | $background: #efffef; 4 | 5 | $background-gradient: lighten($background,5%); 6 | -------------------------------------------------------------------------------- /sass/skin.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background: $background; 3 | background: -webkit-linear-gradient(top, $background-gradient, $background); 4 | background-repeat: no-repeat; 5 | background-attachment: fixed; 6 | } 7 | 8 | header { 9 | color: $paper; 10 | background: $ink; 11 | background: -webkit-linear-gradient(top, $ink,#242424); 12 | background-repeat: no-repeat; 13 | h1 a, h1 a:visited { 14 | color: $paper; 15 | } 16 | } 17 | 18 | .page { 19 | background: $paper; 20 | color: $ink; 21 | -webkit-box-shadow: 0 0 4px rgba(0, 0, 0, 0.2); 22 | -moz-box-shadow: 0 0 4px rgba(0, 0, 0, 0.2); 23 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.2); 24 | } 25 | 26 | a, a:visited { 27 | color: $ink; 28 | } 29 | 30 | a:hover { 31 | color: lighten($ink,5%); 32 | } 33 | 34 | .toc a,.toc a:visited { 35 | text-decoration: none; 36 | } 37 | .toc a:hover { 38 | text-decoration: underline; 39 | } 40 | code { 41 | border: solid thin #777; 42 | background: #ddd; 43 | padding: $sz5; 44 | padding-right: $sz5; 45 | border-radius: $sz5; 46 | } 47 | 48 | pre code { 49 | border-radius: $sz4; 50 | border: solid thin #657b83; 51 | } 52 | 53 | @media all and (max-width : 700px) { 54 | pre code { 55 | border-radius: $sz5; 56 | } 57 | } 58 | 59 | pre code[class~='language-shell'] { 60 | background: black; 61 | background: -webkit-linear-gradient(top, #000, #121); 62 | background-repeat: no-repeat; 63 | color: white; 64 | border: none; 65 | } 66 | 67 | img { 68 | width: 100%; 69 | display: block; 70 | position: relative; 71 | background-color: #fff; 72 | -webkit-box-shadow: 0 0 4px rgba(0, 0, 0, 0.2), inset 0 0 50px rgba(0, 0, 0, 0.1); 73 | -moz-box-shadow: 0 0 4px rgba(0, 0, 0, 0.2), inset 0 0 50px rgba(0, 0, 0, 0.1); 74 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.2), inset 0 0 50px rgba(0, 0, 0, 0.1); 75 | } 76 | 77 | -------------------------------------------------------------------------------- /sass/styles.scss: -------------------------------------------------------------------------------- 1 | @import "colors"; 2 | @import "typography"; 3 | @import "skin"; 4 | 5 | // TOC Overrides 6 | .toc ol li ol li { 7 | line-height: $line-height * 0.85; 8 | } 9 | 10 | ol.front-matter { 11 | list-style: upper-roman; 12 | margin-bottom: 0; 13 | ol { 14 | list-style: lower-roman; 15 | } 16 | } 17 | ol.main-matter { 18 | margin-top: 0; 19 | margin-bottom: 0; 20 | list-style: decimal; 21 | } 22 | ol.back-matter { 23 | margin-top: 0; 24 | list-style: upper-alpha; 25 | } 26 | 27 | li.chapter { font-size: $f6; } 28 | li.section { font-size: $f7; } 29 | 30 | // Navigation footer 31 | footer ol { 32 | list-style: none; 33 | padding: 0; 34 | margin: 0; 35 | } 36 | footer ol li { 37 | display: inline; 38 | } 39 | footer ol li.previous { 40 | float: left; 41 | } 42 | footer ol li.next { 43 | float: right; 44 | } 45 | footer .clearfix { 46 | clear: both; 47 | } 48 | footer .copyright { 49 | font-size: $f8; 50 | } 51 | footer .copyright img { 52 | width: auto; 53 | } 54 | -------------------------------------------------------------------------------- /sass/typography.scss: -------------------------------------------------------------------------------- 1 | // Font Library 2 | $baskerville: 'Baskerville'; 3 | $avenir: 'Avenir'; 4 | $anonymous: 'anonymous-pro'; 5 | $freight: 'freight-text-pro'; 6 | $hypatia: 'hypatia-sans-pro'; 7 | $underground-pc: 'p22-underground-pc'; 8 | $underground: 'p22-underground'; 9 | $cronos: 'cronos-pro'; 10 | 11 | // Font Assignments 12 | $serif: $freight, courier, serif; 13 | $sans: $underground, sans-serif; 14 | $monospace: $anonymous, monospace; 15 | $header: $underground-pc, courier, sans-serif; 16 | 17 | // Sizing 18 | $chars-per-line: 60; 19 | 20 | // Modular Font Scale 21 | /* 22 | $f-2: 123.364px; 23 | $f-1: 104.717px; 24 | */ 25 | $f1: 76.245px; 26 | $f2: 64.720px; 27 | $f3: 47.123px; 28 | $f4: 40.000px; 29 | $f5: 29.124px; 30 | $f6: 24.722px; 31 | $f7: 18.000px; 32 | $f8: 15.279px; 33 | $f9: 11.125px; 34 | $f10: 9.443px; 35 | $body-font-size: $f6; 36 | /* 37 | $f10: 6.876px; 38 | $f11: 5.836px; 39 | $f12: 4.250px; 40 | */ 41 | 42 | $line-height: 1.4; 43 | $text-width: $body-font-size * $chars-per-line / 2; 44 | 45 | // Some sizes 46 | $sz1: $body-font-size * $line-height * 2; 47 | $sz2: $body-font-size * $line-height; 48 | $sz3: $body-font-size * $line-height / 2; 49 | $sz4: $body-font-size * $line-height / 4; 50 | $sz5: $body-font-size * $line-height / 8; 51 | 52 | b, strong { 53 | font-weight: 600; 54 | } 55 | 56 | h1 { font-size: $f2; } 57 | h2 { 58 | font-size: $f3; 59 | text-align: center; 60 | } 61 | h3 { 62 | font-size: $f5; 63 | border-bottom: solid thin #888; 64 | } 65 | h4 { font-size: $f7; } 66 | h5 { font-size: $f8; } 67 | h6 { font-size: $f9; } 68 | 69 | h1, h2, h3, h4, h5, h6 { 70 | font-family: $sans; 71 | margin-top: 0; 72 | text-rendering: optimizeLegibility; 73 | } 74 | h1, h2, h3 { 75 | margin-bottom: $sz3; 76 | } 77 | 78 | h4, h5, h6 { 79 | margin-bottom: -1 * $sz3; 80 | } 81 | 82 | header { 83 | padding-left: $sz2; 84 | padding-right: $sz2; 85 | padding-bottom: $sz3; 86 | padding-top: $sz2; 87 | margin-left: -1 * $sz2; 88 | margin-right: -1 * $sz2; 89 | margin-top: -1 * $sz2; 90 | margin-bottom: $sz2; 91 | text-align: center; 92 | h1 { 93 | margin-top: 0; 94 | margin-bottom: 0; 95 | font-family: $header; 96 | font-size: $f5; 97 | a, a:visited { 98 | text-decoration: none; 99 | } 100 | } 101 | .byline { 102 | font-size: $f7; 103 | margin-top: 0; 104 | padding-top: 0; 105 | } 106 | } 107 | @media all and (max-width : 700px) { 108 | h1 { font-size: $f4; } 109 | h2 { font-size: $f5; } 110 | h3 { font-size: $f7; } 111 | h4 { font-size: $f8; } 112 | h5 { font-size: $f9; } 113 | h6 { font-size: $f10; } 114 | header { 115 | h1 { 116 | font-size: $f7; 117 | } 118 | .byline { 119 | font-size: $f8; 120 | } 121 | } 122 | 123 | } 124 | 125 | h2.subtitle { 126 | border-bottom: none; 127 | font-family: $header; 128 | font-weight: 100; 129 | font-size: $f5; 130 | text-align: center; 131 | } 132 | 133 | 134 | body, p, li, td { 135 | font-family: $cronos; 136 | font-size: $body-font-size; 137 | line-height: $line-height; 138 | text-rendering: optimizeLegibility; 139 | } 140 | @media all and (max-width : 700px) { 141 | body, p, li, td { 142 | } 143 | } 144 | 145 | body { 146 | width: $text-width; 147 | margin: 0 auto; 148 | overflow-x: hidden; 149 | } 150 | 151 | .page { 152 | padding: $sz2; 153 | } 154 | 155 | @media all and (max-width : 700px) { 156 | body { 157 | width: 95%; 158 | } 159 | .page { 160 | padding: $sz4; 161 | } 162 | } 163 | 164 | pre,code { 165 | font-family: $monospace; 166 | } 167 | pre code { 168 | padding: 0; 169 | font-size: $f5; 170 | overflow-x: scroll; 171 | } 172 | 173 | p code { 174 | font-size: $f7; 175 | } 176 | @media all and (max-width : 700px) { 177 | code { 178 | word-wrap: break-word; 179 | } 180 | article.code-listing code { 181 | word-wrap: normal; 182 | } 183 | } 184 | 185 | pre { 186 | margin: 0; 187 | } 188 | 189 | article.code-listing { 190 | border-radius: $sz3; 191 | padding: $sz3; 192 | overflow-x: hidden; 193 | pre code { 194 | font-size: $f7; 195 | } 196 | } 197 | @media all and (max-width : 700px) { 198 | article.code-listing { 199 | padding: $sz4; 200 | } 201 | 202 | } 203 | 204 | article.code-listing footer h1{ 205 | font-size: $f8; 206 | font-family: $monospace; 207 | border-bottom: none; 208 | margin: 0; 209 | padding: 0; 210 | text-align: right; 211 | } 212 | 213 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | $f-2: 123.364px; 3 | $f-1: 104.717px; 4 | */ 5 | /* 6 | $f10: 6.876px; 7 | $f11: 5.836px; 8 | $f12: 4.250px; 9 | */ 10 | b, strong { 11 | font-weight: 600; } 12 | 13 | h1 { 14 | font-size: 64.72px; } 15 | 16 | h2 { 17 | font-size: 47.123px; 18 | text-align: center; } 19 | 20 | h3 { 21 | font-size: 29.124px; 22 | border-bottom: solid thin #888; } 23 | 24 | h4 { 25 | font-size: 18px; } 26 | 27 | h5 { 28 | font-size: 15.279px; } 29 | 30 | h6 { 31 | font-size: 11.125px; } 32 | 33 | h1, h2, h3, h4, h5, h6 { 34 | font-family: "p22-underground", sans-serif; 35 | margin-top: 0; 36 | text-rendering: optimizeLegibility; } 37 | 38 | h1, h2, h3 { 39 | margin-bottom: 17.3054px; } 40 | 41 | h4, h5, h6 { 42 | margin-bottom: -17.3054px; } 43 | 44 | header { 45 | padding-left: 34.6108px; 46 | padding-right: 34.6108px; 47 | padding-bottom: 17.3054px; 48 | padding-top: 34.6108px; 49 | margin-left: -34.6108px; 50 | margin-right: -34.6108px; 51 | margin-top: -34.6108px; 52 | margin-bottom: 34.6108px; 53 | text-align: center; } 54 | header h1 { 55 | margin-top: 0; 56 | margin-bottom: 0; 57 | font-family: "p22-underground-pc", courier, sans-serif; 58 | font-size: 29.124px; } 59 | header h1 a, header h1 a:visited { 60 | text-decoration: none; } 61 | header .byline { 62 | font-size: 18px; 63 | margin-top: 0; 64 | padding-top: 0; } 65 | 66 | @media all and (max-width: 700px) { 67 | h1 { 68 | font-size: 40px; } 69 | 70 | h2 { 71 | font-size: 29.124px; } 72 | 73 | h3 { 74 | font-size: 18px; } 75 | 76 | h4 { 77 | font-size: 15.279px; } 78 | 79 | h5 { 80 | font-size: 11.125px; } 81 | 82 | h6 { 83 | font-size: 9.443px; } 84 | 85 | header h1 { 86 | font-size: 18px; } 87 | header .byline { 88 | font-size: 15.279px; } } 89 | h2.subtitle { 90 | border-bottom: none; 91 | font-family: "p22-underground-pc", courier, sans-serif; 92 | font-weight: 100; 93 | font-size: 29.124px; 94 | text-align: center; } 95 | 96 | body, p, li, td { 97 | font-family: "cronos-pro"; 98 | font-size: 24.722px; 99 | line-height: 1.4; 100 | text-rendering: optimizeLegibility; } 101 | 102 | body { 103 | width: 741.66px; 104 | margin: 0 auto; 105 | overflow-x: hidden; } 106 | 107 | .page { 108 | padding: 34.6108px; } 109 | 110 | @media all and (max-width: 700px) { 111 | body { 112 | width: 95%; } 113 | 114 | .page { 115 | padding: 8.6527px; } } 116 | pre, code { 117 | font-family: "anonymous-pro", monospace; } 118 | 119 | pre code { 120 | padding: 0; 121 | font-size: 29.124px; 122 | overflow-x: scroll; } 123 | 124 | p code { 125 | font-size: 18px; } 126 | 127 | @media all and (max-width: 700px) { 128 | code { 129 | word-wrap: break-word; } 130 | 131 | article.code-listing code { 132 | word-wrap: normal; } } 133 | pre { 134 | margin: 0; } 135 | 136 | article.code-listing { 137 | border-radius: 17.3054px; 138 | padding: 17.3054px; 139 | overflow-x: hidden; } 140 | article.code-listing pre code { 141 | font-size: 18px; } 142 | 143 | @media all and (max-width: 700px) { 144 | article.code-listing { 145 | padding: 8.6527px; } } 146 | article.code-listing footer h1 { 147 | font-size: 15.279px; 148 | font-family: "anonymous-pro", monospace; 149 | border-bottom: none; 150 | margin: 0; 151 | padding: 0; 152 | text-align: right; } 153 | 154 | body { 155 | background: #efffef; 156 | background: -webkit-linear-gradient(top, white, #efffef); 157 | background-repeat: no-repeat; 158 | background-attachment: fixed; } 159 | 160 | header { 161 | color: white; 162 | background: black; 163 | background: -webkit-linear-gradient(top, black, #242424); 164 | background-repeat: no-repeat; } 165 | header h1 a, header h1 a:visited { 166 | color: white; } 167 | 168 | .page { 169 | background: white; 170 | color: black; 171 | -webkit-box-shadow: 0 0 4px rgba(0, 0, 0, 0.2); 172 | -moz-box-shadow: 0 0 4px rgba(0, 0, 0, 0.2); 173 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.2); } 174 | 175 | a, a:visited { 176 | color: black; } 177 | 178 | a:hover { 179 | color: #0d0d0d; } 180 | 181 | .toc a, .toc a:visited { 182 | text-decoration: none; } 183 | 184 | .toc a:hover { 185 | text-decoration: underline; } 186 | 187 | code { 188 | border: solid thin #777; 189 | background: #ddd; 190 | padding: 4.32635px; 191 | padding-right: 4.32635px; 192 | border-radius: 4.32635px; } 193 | 194 | pre code { 195 | border-radius: 8.6527px; 196 | border: solid thin #657b83; } 197 | 198 | @media all and (max-width: 700px) { 199 | pre code { 200 | border-radius: 4.32635px; } } 201 | pre code[class~='language-shell'] { 202 | background: black; 203 | background: -webkit-linear-gradient(top, black, #112211); 204 | background-repeat: no-repeat; 205 | color: white; 206 | border: none; } 207 | 208 | img { 209 | width: 100%; 210 | display: block; 211 | position: relative; 212 | background-color: #fff; 213 | -webkit-box-shadow: 0 0 4px rgba(0, 0, 0, 0.2), inset 0 0 50px rgba(0, 0, 0, 0.1); 214 | -moz-box-shadow: 0 0 4px rgba(0, 0, 0, 0.2), inset 0 0 50px rgba(0, 0, 0, 0.1); 215 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.2), inset 0 0 50px rgba(0, 0, 0, 0.1); } 216 | 217 | .toc ol li ol li { 218 | line-height: 1.19; } 219 | 220 | ol.front-matter { 221 | list-style: upper-roman; 222 | margin-bottom: 0; } 223 | ol.front-matter ol { 224 | list-style: lower-roman; } 225 | 226 | ol.main-matter { 227 | margin-top: 0; 228 | margin-bottom: 0; 229 | list-style: decimal; } 230 | 231 | ol.back-matter { 232 | margin-top: 0; 233 | list-style: upper-alpha; } 234 | 235 | li.chapter { 236 | font-size: 24.722px; } 237 | 238 | li.section { 239 | font-size: 18px; } 240 | 241 | footer ol { 242 | list-style: none; 243 | padding: 0; 244 | margin: 0; } 245 | 246 | footer ol li { 247 | display: inline; } 248 | 249 | footer ol li.previous { 250 | float: left; } 251 | 252 | footer ol li.next { 253 | float: right; } 254 | 255 | footer .clearfix { 256 | clear: both; } 257 | 258 | footer .copyright { 259 | font-size: 15.279px; } 260 | 261 | footer .copyright img { 262 | width: auto; } 263 | -------------------------------------------------------------------------------- /view_recipe.md: -------------------------------------------------------------------------------- 1 | # Test-Driving the Next Feature 2 | 3 | Now that we have Angular setup, including a way to manage front-end assets, 4 | run tests, and deploy our application to production, the hard part is done. In 5 | this chapter, let's use what we've set up, along with some TDD, to implement 6 | the ability to click on a recipe in the results list and view its 7 | instructions. 8 | 9 | In a classic Rails app, we'd have a method called `show` in our 10 | `RecipesController`, which would be routed-to from `/recipes/:id`. We'll do 11 | something similar in our Angular app, however we won't add this feature to the 12 | existing `RecipesController.coffee`, but a new controller called 13 | `RecipeController`. 14 | 15 | There's no advantage to having the existing `RecipesController.coffee` 16 | handle the viewing of an individual recipe, other than saving us a bit of 17 | setup in the test. My feeling is that when there's no advantage over adding 18 | code to an existing class or file, it's always better to make a new class or 19 | file. 20 | 21 | Generally, what we need to do here is: 22 | 23 | 1. Create our Angular controller 24 | 2. Create our backend Rails controller 25 | 3. Write a browser-based test for the feature 26 | 4. Create a view 27 | 28 | ## Angular controller 29 | 30 | First, we'll update our Angular app config to route `/recipes/:recipeId` to 31 | the yet-to-be-created `RecipeController`: 32 | 33 | git://receta.git/app/assets/javascripts/app.coffee#..add-recipe-controller 34 | 35 | Now, let's create a bare-bones version of our controller: 36 | 37 | git://receta.git/app/assets/javascripts/controllers/RecipeController.coffee#add-recipe-controller 38 | 39 | And the boilerplate needed for our test: 40 | 41 | git://receta.git/spec/javascripts/controllers/RecipeController_spec.coffee#add-recipe-controller 42 | 43 | What `RecipeController` needs to do is: 44 | 45 | * Load the recipe identified by `:recipeId` in the URL and set it to some variable in `$scope` 46 | * If the recipe doesn't exist, handle that error somehow 47 | 48 | Let's get the happy path working first: 49 | 50 | git://receta.git/spec/javascripts/controllers/RecipeController_spec.coffee#..recipe-controller-test 51 | 52 | This is similar to what we had in `RecipesController_spec.coffee`. Because 53 | the HTTP call to our backend happens on controller startup, we create a 54 | function `setupController()` that handles mocking out the HTTP calls. It 55 | takes a single parameter—`recipeExists`—to allow us to control whether or not 56 | the backend sends a 404 or a real recipe. 57 | 58 | Since none of this is implemented yet, our test should fail. Let's try it: 59 | 60 | git://receta.git/#recipe-controller-test!rake teaspoon!nonzero 61 | 62 | Sure enough, our test fails exactly how we'd like: `$scope.recipe` isn't 63 | defined for either case, and no HTTP calls were made, despite our expectation 64 | that they would be. 65 | 66 | Let's make it pass. We'll use Angular's `$resource` service to create the 67 | same resource we did in `RecipesController`, but use the `get` method, which 68 | does what we want. 69 | 70 | git://receta.git/app/assets/javascripts/controllers/RecipeController.coffee#..recipe-controller-test-passes 71 | 72 | Now, we see if this makes our tests pass: 73 | 74 | git://receta.git/#recipe-controller-test-passes!rake teaspoon 75 | 76 | It does! 77 | 78 | Although our code does technically handle the case of a missing recipe, it 79 | doesn't handle it very well. We'd like to pass onto the user some indication 80 | that things went wrong. In Rails, we'd use the flash as a way to provide such 81 | information. 82 | 83 | In Angular, we can certainly create our own flash by just assigning 84 | `{ error: “Recipe not found”}` to `$scope.flash`. Instead, let's use a 85 | pre-made module that will handle flash messages, but also allow us to display 86 | them in our views. `angular-flash` is that component, so let's install it. 87 | 88 | First, we add it to `Bowerfile`: 89 | 90 | git://receta.git/Bowerfile#..install-angular-flash 91 | 92 | Then, install it: 93 | 94 | > rake bower:install 95 | 96 | To make sure the asset pipeline picks up this new dependency, we'll need to 97 | add it to `application.js` as well: 98 | 99 | git://receta.git/app/assets/javascripts/application.js#..angular-flash-in-application-js 100 | 101 | Note the slightly different path to the file we want–this is the lack of 102 | standardization across front-end components rearing its ugly head. 103 | 104 | Finally, we add it as a module dependency to our app. `angular-flash` comes with 105 | two modules, one for the flash data itself, and another for the view 106 | components. 107 | 108 | git://receta.git/app/assets/javascripts/app.coffee#..install-angular-flash 109 | 110 | We'll see how the view components work a bit later, but for now, our 111 | controller can depend on a component called `flash`. `flash` allows us to set 112 | errors, warnings, informational messages, and success messages. 113 | 114 | Back to our test, we want to assert that the flash receives an error message that the 115 | recipe couldn't be found. 116 | 117 | git://receta.git/spec/javascripts/controllers/RecipeController_spec.coffee#..test-recipe-controller-for-flash 118 | 119 | Notice that we're taking advantage of Angular's 120 | alternate dependency injection naming convention. We want our test to use an object called 121 | `flash` to make assertions, but since this component isn't provided by 122 | Angular, its name—for dependency injection purposes—is also `flash`, meaning we'd 123 | need to use a different name for the flash in our tests. Angular allows us to 124 | name the _parameter_ with leading and trailing underscores, e.g. `_flash_`. 125 | When we do this, Angular understands that the object `flash` should be injected. This means 126 | that the name of the object that “escapes” the closure can be named `flash`. Ah, JavaScript! 127 | 128 | Now, when we run the test, we should see a simple expectation failure on the message. 129 | 130 | git://receta.git/#test-recipe-controller-for-flash!rake teaspoon!nonzero 131 | 132 | With a clearly failing test, we just need to add the flash as a dependency, and use it. 133 | 134 | git://receta.git/app/assets/javascripts/controllers/RecipeController.coffee#..test-for-flash-passes 135 | 136 | We add `flash` to the list of injected dependencies, and then set the error message in our failure callback. Sure enough, the test passes: 137 | 138 | git://receta.git/#test-for-flash-passes!rake teaspoon 139 | 140 | Our Angular controller is done! We still need a view, a browser-based test, and the backend. Let's do the Rails backend next. 141 | 142 | ## Rails controller 143 | 144 | In the Rails world, it is canonical to have the same controller have the code for both `index` 145 | and `show`, so what we need to do here is implement `show`. 146 | 147 | First, let's add the new route to `config/routes.rb`: 148 | 149 | git://receta.git/config/routes.rb#..failing-recipe-show 150 | 151 | We'll add an empty `show` method to the controller as well: 152 | 153 | git://receta.git/app/controllers/recipes_controller.rb#..failing-recipe-show 154 | 155 | Finally, we'll write tests for when the recipe exists and for when it doesn't: 156 | 157 | git://receta.git/spec/controllers/recipes_controller_spec.rb#..failing-recipe-show 158 | 159 | This should result in a failing test, which it does: 160 | 161 | git://receta.git/#failing-recipe-show!rspec spec/controllers/recipes_controller_spec.rb!nonzero 162 | 163 | To make this pass, we'll fetch the recipe: 164 | 165 | git://receta.git/app/controllers/recipes_controller.rb#..recipe-show-passes 166 | 167 | 168 | and implement a JBuilder view that uses our existing `_recipe.json.jbuilder` partial: 169 | 170 | git://receta.git/app/views/recipes/show.json.jbuilder#recipe-show-passes 171 | 172 | To handle the case of a non-existent recipe, we'll let the 173 | `ActiveRecord::RecordNotFound` leak out of our controller, and use 174 | `rescue_from` in `ApplicationController` to handle that. This way, we never 175 | have to worry about translating this error into a 404 again. 176 | 177 | git://receta.git/app/controllers/application_controller.rb#..recipe-show-passes 178 | 179 | Now, everything passes! 180 | 181 | git://receta.git/#recipe-show-passes!rspec spec/controllers/recipes_controller_spec.rb 182 | 183 | Let's bring it all together with a browser-based test. 184 | 185 | ## Browser-based test 186 | 187 | Our browser-based test will simulate a user using our app, so we'll first do a 188 | search, and then navigate to a specific recipe. We'll then check that the 189 | resulting view shows the title *and* instructions. We'll also navigate back 190 | to our results, assuming the existence of a “back” button. 191 | 192 | git://receta.git/spec/features/view_spec.rb#view-spec 193 | 194 | Running the test fails: 195 | 196 | git://receta.git/#view-spec!rspec spec/features/view_spec.rb!nonzero 197 | 198 | If you recall, we just used an `href` of `#` around the recipe name in the 199 | search results. Clicking that essentially clears the search and starts over. 200 | 201 | We'll need to change that `a` to route us to `/recipes/:recipeId`, as well 202 | as actually build out the “show” view. 203 | 204 | First, we'll change the `a`: 205 | 206 | git://receta.git/app/assets/javascripts/templates/index.html#..view-integrated 207 | 208 | Notice that we removed the argument to `href`, which will keep the browser 209 | from changing its location and reloading our view. Instead, we used 210 | `ng-click` to trigger the `view()` method that we'll now add to 211 | `RecipesController`: 212 | 213 | git://receta.git/app/assets/javascripts/controllers/RecipesController.coffee#..view-integrated 214 | 215 | All that's left is to create the view in `show.html`: 216 | 217 | git://receta.git/app/assets/javascripts/templates/show.html#view-integrated 218 | 219 | We'll also add a method to make the “Back” button work: 220 | 221 | git://receta.git/app/assets/javascripts/controllers/RecipeController.coffee#..add-back-button 222 | 223 | Now, everything works: 224 | 225 | git://receta.git/#view-integrated!rspec spec/features/view_spec.rb 226 | 227 | We added the back button and function specifically to call out a gap between 228 | what we get with Angular and what we get with Rails, with respect to view and 229 | controller implementation. 230 | 231 | We've already seen that Angular's router requires more explicit configuration 232 | than Rails'. We can also now see that we don't get convenient methods like 233 | `recipes_path` or `recipe_path(recipe)` to generate routes for us. There 234 | doesn't seem to be a canonical way to do this at this time. 235 | 236 | There's one last thing to do, and that's integrate the flash message. 237 | 238 | ## Flash message 239 | 240 | Although we hope to not generate links to non-existent 241 | recipes, it's still possible it could happen and, unlike a web app where a 404 will send us to a special page, since our Angular 242 | app is using AJAX requests, we'll have to do _something_ if we get an error from the backend. We test-drove setting an error 243 | message in the flash, so now we just need to show it in our view. 244 | 245 | The `angular-flash` module we installed has two parts. The first, which we've already seen, is a place to store flash messages. 246 | The second is to allow your view to “subscribe” to them, which means that you can arrange for markup to be shown if there is a 247 | flash message. 248 | 249 | Because this is not something a user will ever be intended to see, and is also very simple, we're not going to write a test for it. If there were more 250 | complex logic around the flash, and its message, a test would be more useful, but for this case, it's not really worth it. 251 | 252 | First, we'll add the necessary markup to `show.html`: 253 | 254 | git://receta.git/app/assets/javascripts/templates/show.html#..integrate-flash 255 | 256 | Our `article` tag has three special attributes, provided by `angular-flash`: 257 | 258 | * `flash-alert` which binds this markup to the contents of the flash 259 | * `duration` which we can use to “fade out” the flash after a time 260 | * `active-class`, a class that will be added to this element when the flash has a value 261 | 262 | The second thing we need to do is to configure `angular-flash` so that it knows about the various alert classes that Bootstrap 263 | provides. We do this in the app config in `app.coffee`: 264 | 265 | git://receta.git/app/assets/javascripts/app.coffee#..integrate-flash 266 | 267 | What this configuration means is that if there is, for example, a value in `flash.error`, `angular-flash` will add the class 268 | `alert-danger` to the flash element. Since we also configured that element to add `alert` to *any* flash message, our markup 269 | will be styled with the class `alert alert-danger` which is what Bootstrap needs to show an alert. 270 | 271 | Now, let's navigate to a non-existent recipe and see it in action: 272 | 273 | ![Flash Message](images/FlashMessage.png) 274 | 275 | Perfect! 276 | 277 | Now that we've seen how we can use TDD for all aspects of our feature, let's 278 | add the create, update, and destroy features. We'll do this very quickly as a 279 | way to demonstrate what the code looks like. You probably wouldn't add all of 280 | these at the same time in your “real” application. 281 | --------------------------------------------------------------------------------