เริ่ม AngularJS ไมยาก Documentation ดีที่สุด tutorials เรียบง่าย
แต่น่าปวดหัวตอนรวม technologies
ถ้าใช้ CoffeeScript แทน JavaScript ต้องคิด preprocessing concerns และ syntax differences ปัญหาเล็กน้อยตัวเอง แต่วาง Ruby on Rails, Jasmine และ Karma ลงไป? tricky อย่างไม่คาดคิด
นี่คือ stack ที่ใช้ใน tutorial ไม่ใช่เพราะ gluttons for punishment แต่ เพราะ setup แบบนี้เจอใน real world
Tutorial สมมติ comfortable กับ Rails แต่ไม่จำเป็น AngularJS
Creating a Base Rails App
Tech layers เยอะ สร้าง simple application ที่ barely does anything CRUD สำหรับ restaurants - จริงๆ แค่ CR -UD เป็น exercise for reader ;-)
App ชื่อ Restauranteur
ใช้ PostgreSQL และ RSpec DBMS และ server-side testing framework ไม่ significant ใช้อะไรก็ได้
Initial Setup
สร้าง project:
$ rails new restauranteur --database=postgresql --skip-test-unit
ถ้าใช้ Pow add project to Pow:
$ ln -s /Users/jasonswett/projects/restauranteur ~/.pow/restauranteur
สร้าง PostgreSQL database user:
$ createuser -P -s -e restauranteur
เพิ่ม RSpec ใน Gemfile:
# Gemfile gem "rspec-rails", "~> 2.14.0"
Install RSpec:
$ bundle install $ rails g rspec:install
สร้าง database:
$ rake db:create
Creating the Restaurant Model
Project และ database พร้อม สร้าง resource แรก Restaurant resource มี attribute เดียว: name string
$ rails generate scaffold restaurant name:string
OCD ให้ restaurant names unique:
# db/migrate/[timestamp]_create_restaurants.rb class CreateRestaurants < ActiveRecord::Migration def change create_table :restaurants do |t| t.string :name t.timestamps end # Add the following line add_index :restaurants, :name, unique: true end end
Run migration:
$ rake db:migrate
เพิ่ม specs verify ไม่สร้าง invalid restaurants ได้ unique failure gives raw error
require 'spec_helper' describe Restaurant do before do @restaurant = Restaurant.new(name: "Momofuku") end subject { @restaurant } it { should respond_to(:name) } it { should be_valid } describe "when name is not present" do before { @restaurant.name = " " } it { should_not be_valid } end describe "when name is already taken" do before do restaurant_with_same_name = @restaurant.dup restaurant_with_same_name.name = @restaurant.name.upcase restaurant_with_same_name.save end it { should_not be_valid } end end
Validators ทำให้ specs pass:
class Restaurant < ActiveRecord::Base validates :name, presence: true, uniqueness: { case_sensitive: false } end
พร้อมไปต่อ
Bringing AngularJS into the Mix
แทน dump ทุกอย่าง มา demo simplest "Hello, world" AngularJS-Rails app แล้วค่อย build restaurant CRUD
Hello world page ไม่จำเป็นต้อง tied กับ Rails resource ใดๆ สร้าง StaticPagesController serve AngularJS home page
Create the Controller
$ rails generate controller static_pages index
Root route ตอนนี้ "Welcome to Rails" ตั้งเป็น index action ของ StaticPagesController:
# config/routes.rb Restauranteur::Application.routes.draw do # Add the following line root 'static_pages#index' end
Download Angular
- ต้องการ
angular-mocks.jsสำหรับ tests ไม่คิดว่ามีใน Angular docs แต่จำเป็น - AngularJS tutorial list latest bleeding-edge version แต่ผมเจอ compatibility issues ระหว่าง
angular.jsและangular-mocks.jslatest version เวอร์ชัน 1.1.5 ทำงานร่วมกันได้ แม้ไม่ใช่ latest stable ใช้ version นี้ เวลาผ่านไป compatibility จะดีขึ้น
Download angular.js และ angular-mocks.js จาก code.angularjs.org ย้ายไป app/assets/javascripts
$ wget https://code.angularjs.org/1.1.5/angular.js \ https://code.angularjs.org/1.1.5/angular-mocks.js $ mv angular* app/assets/javascripts
Add it to the Asset Pipeline
บอก application require AngularJS file และ load ก่อน files ที่ depend on it (RequireJS manage dependencies ได้ แต่ tutorial นี้ thin stack)
Note: Angular และ Turbolinks conflict ปิด Turbolinks ที่นี่
// app/assets/javascripts/application.js //= require jquery //= require jquery_ujs // Add the following two lines //= require angular //= require main //= require_tree .
Set up the Layout
เพิ่ม ng-app และ ng-view signal Angular app ใน page ลบ Turbolinks mentions
<%= yield %>
Creating an Angular Controller
สร้าง directory controllers:
$ mkdir -p app/assets/javascripts/angular/controllers
สร้าง controller file เรียก "home controller" convention Angular append filenames ด้วย Ctrl filename: app/assets/javascripts/angular/controllers/HomeCtrl.js.coffee
# app/assets/javascripts/angular/controllers/HomeCtrl.js.coffee @restauranteur.controller 'HomeCtrl', ['$scope', ($scope) -> # Notice how this controller body is empty ]
Add an Angular Route
เพิ่ม routing directive ทำให้ HomeCtrl เป็น "default page" กำหนด routing ใน app/assets/javascripts/main.js.coffee
# app/assets/javascripts/main.js.coffee # This line is related to our Angular app, not to our # HomeCtrl specifically. This is basically how we tell # Angular about the existence of our application. @restauranteur = angular.module('restauranteur', []) # This routing directive tells Angular about the default # route for our application. The term "otherwise" here # might seem somewhat awkward, but it will make more # sense as we add more routes to our application. @restauranteur.config(['$routeProvider', ($routeProvider) -> $routeProvider. otherwise({ templateUrl: '../templates/home.html', controller: 'HomeCtrl' }) ])
Add an Angular Template
เก็บ Angular templates ใน public/templates
mkdir public/templates
สร้าง public/templates/home.html ใส่ content ใดๆ ควรเห็นใน browser
This is the home page.
ไป https://restauranteur.dev/ (หรือ https://localhost:3000/ ไม่ใช้ Pow) ควรเห็น contents ของ home.html
An Example of Data Binding
Kind of interesting แต่ไม่ impressive Edit app/assets/angular/controllers/HomeCtrl.js.coffee:
# app/assets/angular/controllers/HomeCtrl.js.coffee @restauranteur.controller 'HomeCtrl', ['$scope', ($scope) -> $scope.foo = 'bar' ]
Analogous to @foo = "bar" ใน Rails controller Plug foo ใน template double-brace syntax:
Value of "foo": {{foo}}
Doing it for Real This Time
Hello world app เสร็จแล้ว Full CRUD application ไม่อยากยากกว่านี้
Seed the Database
Restaurant CRUD meaningful ถ้าเริ่มด้วย restaurants ใน database seed file:
# db/seeds.rb Restaurant.create([ { name: "The French Laundry" }, { name: "Chez Panisse" }, { name: "Bouchon" }, { name: "Noma" }, { name: "Taco Bell" }, ]) rake db:seed
Creating a Restaurant Index Page
สร้าง template folder restaurants:
mkdir public/templates/restaurants
Template แรก index page:
[index](/#) * {{ restaurant.name }}
อธิบายในที สร้าง controller:
# app/assets/javascripts/angular/controllers/RestaurantIndexCtrl.js.coffee @restauranteur.controller 'RestaurantIndexCtrl', ['$scope', '$location', '$http', ($scope, $location, $http) -> $scope.restaurants = [] $http.get('./restaurants.json').success((data) -> $scope.restaurants = data ) ]
ปรับ routing config:
# app/assets/javascripts/main.js.coffee @restauranteur = angular.module('restauranteur', []) @restauranteur.config(['$routeProvider', ($routeProvider) -> $routeProvider. when('/restaurants', { templateUrl: '../templates/restaurants/index.html', controller: 'RestaurantIndexCtrl' }). otherwise({ templateUrl: '../templates/home.html', controller: 'HomeCtrl' }) ])
ไป /#/restaurants ควรเห็น list of restaurants เพิ่ม test ก่อน
Adding Our First Test
Add JS test folder:
mkdir spec/javascripts
Write test:
# spec/javascripts/controllers_spec.js.coffee describe "Restauranteur controllers", -> beforeEach module("restauranteur") describe "RestaurantIndexCtrl", -> it "should set restaurants to an empty array", inject(($controller) -> scope = {} ctrl = $controller("RestaurantIndexCtrl", $scope: scope ) expect(scope.restaurants.length).toBe 0 )
Add config:
// spec/javascripts/restauranteur.conf.js module.exports = function(config) { config.set({ basePath: '../..', frameworks: ['jasmine'], autoWatch: true, preprocessors: { '**/*.coffee': 'coffee' }, files: [ 'app/assets/javascripts/angular.js', 'app/assets/javascripts/angular-mocks.js', 'app/assets/javascripts/main.js.coffee', 'app/assets/javascripts/angular/controllers/RestaurantIndexCtrl.js.coffee', 'app/assets/javascripts/angular/*', 'spec/javascripts/*_spec.js.coffee' ] }); };
Install Karma และ start server:
sudo npm install -g karma sudo npm install -g karma-ng-scenario karma start spec/javascripts/restauranteur.conf.js
ไป https://localhost:9876/ test รัน pass ถ้าอยากดู fail เปลี่ยน expect(scope.restaurants.length).toBe 0 เป็น expect(scope.restaurants.length).toBe 1 รันอีกครั้ง
Meaningfulness ของ test นี้ questionable แต่ intention คือ save work จะได้ Angular code เข้า test harness มีสิ่งบางอย่าง เช่น CoffeeScript preprocessor และ angular-mocks.js inclusion ที่ไม่ obvious ใช้เวลาหลายชั่วโมง head-scratching
Building Out the Restaurants Page
Temporary adjustment restaurant index template:
* {{restaurant.name}} ({{restaurant.id}})
Revisit /#/restaurants รestaurants ไม่มี IDs ทำไม blank?
Generate scaffolding Rails 4 ให้ .jbuilder files:
$ ls -1 app/views/restaurants/*.jbuilder app/views/restaurants/index.json.jbuilder app/views/restaurants/show.json.jbuilder
เปิด app/views/restaurants/index.json.jbuilder:
# app/views/restaurants/index.json.jbuilder json.array!(@restaurants) do |restaurant| json.extract! restaurant, :name json.url restaurant_url(restaurant, format: :json) end
Include :name แต่ไม่ :id เพิ่มเข้าไป:
# app/views/restaurants/index.json.jbuilder json.array!(@restaurants) do |restaurant| json.extract! restaurant, :id, :name json.url restaurant_url(restaurant, format: :json) end
Save refresh /#/restaurants IDs ควรปรากฏ
กลับ template เหมือนเดิม:
[index](/#) * {{ restaurant.name }}
อาจสังเกต point at viewRestaurant() แต่ไม่เคย define มาสร้างตอนนี้:
# app/assets/javascripts/angular/controllers/RestaurantIndexCtrl.js.coffee @restauranteur.controller 'RestaurantIndexCtrl', ['$scope', '$location', '$http', ($scope, $location, $http) -> $scope.restaurants = [] $http.get('./restaurants.json').success((data) -> $scope.restaurants = data ) # Add the following lines $scope.viewRestaurant = (id) -> $location.url "/restaurants/#{id}" ]
Convention Rails resource_name/:id map to "show" page ทำกัน
สร้าง show template, route และ controller:
# {{restaurant.name}} # app/assets/javascripts/main.js.coffee @restauranteur = angular.module('restauranteur', []) @restauranteur.config(['$routeProvider', ($routeProvider) -> $routeProvider. when('/restaurants', { templateUrl: '../templates/restaurants/index.html', controller: 'RestaurantIndexCtrl' }). when('/restaurants/:id', { templateUrl: '../templates/restaurants/show.html', controller: 'RestaurantShowCtrl' }). otherwise({ templateUrl: '../templates/home.html', controller: 'HomeCtrl' }) ]) # app/assets/javascripts/angular/controllers/RestaurantShowCtrl.js.coffee @restauranteur.controller 'RestaurantShowCtrl', ['$scope', '$http', '$routeParams', ($scope, $http, $routeParams) -> $http.get("./restaurants/#{$routeParams.id}.json").success((data) -> $scope.restaurant = data ) ]
Refresh /#/restaurants click restaurant ไป show page Yay!
That's All for Now
ผลลัพธ์ไม่อาจ impressive แตหวังว่า save time plugging AngularJS into Rails 4 ต่อไปแนะนำ ngResource ทำ CRUD DRY
Interested in Learning More?
ดู post ของ Adam Anderson Bootstrapping an AngularJS app in Rails 4.0 series ช่วยเริ่ม AngularJS และ Rails อาจชอบ tutorial ของเขา แต่ tutorial นี้ต่างตรงที่ _really_ spoon-feed details minimize chances stuck in weeds