├── README.md └── template.rb /README.md: -------------------------------------------------------------------------------- 1 | # Rails API + Angular Template 2 | 3 | Creates a **rails-api** backend with an **angular** frontend. The frontend code will be located in `/frontend/`. 4 | 5 | ## Usage 6 | 7 | ``` 8 | rails-api new \ 9 | --skip-sprockets \ 10 | --skip-test-unit \ 11 | --template=https://github.com/joshnuss/angular-rails-api/raw/master/template.rb 12 | ``` 13 | 14 | ## Features 15 | 16 | - [rails-api](https://github.com/rails-api/rails-api) 17 | - [spring](https://github.com/rails/spring) 18 | - [grunt](http://gruntjs.com/) 19 | - [jade](http://jade-lang.com/) 20 | - [sass](http://sass-lang.com/) 21 | - [coffeescript](http://coffeescript.org/) 22 | - [bootstrap](http://getbootstrap.com/) 23 | - [live-reload](http://livereload.com/) 24 | - [rspec](https://relishapp.com/rspec) 25 | - [jbuilder](https://github.com/rails/jbuilder) 26 | - [yeoman](http://yeoman.io/) 27 | 28 | ## Requirements 29 | 30 | - [rails-api](https://github.com/rails-api/rails-api) 31 | - [nodejs](http://nodejs.org/) 32 | - [grunt-cli](http://gruntjs.com/) 33 | - [yeoman](http://yeoman.io/) 34 | 35 | ## Common Tasks 36 | 37 | - To build the frontend: `cd frontend && grunt build` 38 | - To run the frontend server: `cd frontend && grunt serve` 39 | - To run the rails server: `bin/rails server` 40 | - To run the rails specs: `bin/rake spec` 41 | - To run the angular specs: `cd frontend && grunt test` 42 | 43 | ## Development Mode 44 | 45 | In dev mode, you need to run both the frontend node server `grunt serve` and the rails server `rails server` 46 | All requests are served from the frontend server, requests to `/api` are proxied to rails. 47 | 48 | ## Deployment 49 | 50 | Run `grunt build` which copies all files to `/public`. Only rails is needed in production. `Rack::Static` is configured to serve static files. 51 | 52 | ## Links 53 | 54 | 1. [Working with Angular.js and Rails](http://rockyj.in/2013/10/24/angular_rails.html) 55 | 56 | ### Time for a coffee break? 57 | 58 | @joshnuss is a freelance software consultant. joshnuss@gmail.com 59 | -------------------------------------------------------------------------------- /template.rb: -------------------------------------------------------------------------------- 1 | gem 'jbuilder' 2 | 3 | gem_group :development, :test do 4 | gem 'rspec-rails' 5 | gem 'jazz_hands' 6 | gem 'spring' 7 | end 8 | 9 | generate 'rspec:install' 10 | 11 | route <<-CODE 12 | namespace :api, path: 'api/v1' do 13 | resources :things, only: :index 14 | end 15 | CODE 16 | 17 | file 'app/controllers/api/things_controller.rb', <<-CODE 18 | class Api::ThingsController < ApplicationController 19 | def index 20 | @things = %w(stuff other foo) 21 | 22 | render 23 | end 24 | end 25 | CODE 26 | 27 | file 'app/views/api/things/index.jbuilder', <<-CODE 28 | json.array! @things 29 | CODE 30 | 31 | remove_file 'config.ru' 32 | file "config.ru", <<-CODE 33 | # This file is used by Rack-based servers to start the application. 34 | 35 | require ::File.expand_path('../config/environment', __FILE__) 36 | 37 | use Rack::Static, 38 | urls: ["/views", "/images", "/scripts", "/styles", "/bower_components"], 39 | index: 'index.html', 40 | root: "public" 41 | 42 | run Rails.application 43 | CODE 44 | 45 | run "mkdir frontend" 46 | run "rm public/*" 47 | 48 | inside("frontend") do 49 | run "yo angular --coffee" 50 | run "rm app/views/main.html" 51 | run "rm app/styles/main.scss" 52 | run "rm app/scripts/controllers/main.coffee" 53 | run "rm Gruntfile.js" 54 | 55 | file 'app/views/main.jade', <<-CODE 56 | h1 My Application 57 | 58 | ul 59 | li(ng-repeat='thing in awesomeThings') 60 | | {{thing}} 61 | CODE 62 | 63 | file 'app/styles/main.sass', <<-CODE 64 | $icon-font-path: "/bower_components/sass-bootstrap/fonts/" 65 | 66 | @import sass-bootstrap/lib/bootstrap 67 | 68 | body 69 | padding: 10px 70 | CODE 71 | 72 | file "app/scripts/controllers/main.coffee", <<-CODE 73 | 'use strict' 74 | 75 | angular.module('frontendApp') 76 | .controller 'MainCtrl', ($scope, $http) -> 77 | $scope.awesomeThings = [] 78 | 79 | $http.get('/api/v1/things').success (data) -> 80 | $scope.awesomeThings = data 81 | CODE 82 | 83 | file "Gruntfile.js", <<-CODE 84 | // Generated using ng-rails-api-template 85 | 'use strict'; 86 | 87 | // # Globbing 88 | // for performance reasons we're only matching one level down: 89 | // 'test/spec/{,*/}*.js' 90 | // use this if you want to recursively match all subfolders: 91 | // 'test/spec/**/*.js' 92 | 93 | var proxySnippet = require('grunt-connect-proxy/lib/utils').proxyRequest; 94 | var mountFolder = function (connect, dir) { 95 | return connect.static(require('path').resolve(dir)); 96 | }; 97 | 98 | module.exports = function (grunt) { 99 | 100 | // Load grunt tasks automatically 101 | require('load-grunt-tasks')(grunt); 102 | 103 | // Time how long tasks take. Can help when optimizing build times 104 | require('time-grunt')(grunt); 105 | 106 | var jadeFiles = grunt.file.expand('app/views/{,*/}*.jade'); 107 | var htmlFiles = {}; 108 | 109 | jadeFiles.forEach(function(file) { 110 | htmlFiles[file.replace(/.jade$/, ".html").replace(/^app\\/views/, ".tmp/views")] = [file]; 111 | }); 112 | 113 | // Define the configuration for all the tasks 114 | grunt.initConfig({ 115 | 116 | // Project settings 117 | yeoman: { 118 | // configurable paths 119 | app: require('./bower.json').appPath || 'app', 120 | dist: '../public' 121 | }, 122 | 123 | mkdir: { 124 | all: { 125 | options: { 126 | create: ['.tmp'] 127 | }, 128 | }, 129 | }, 130 | 131 | // Watches files for changes and runs tasks based on the changed files 132 | watch: { 133 | jade: { 134 | files: ['<%= yeoman.app %>/views/{,*/}*.jade'], 135 | tasks: ['jade'] 136 | }, 137 | coffee: { 138 | files: ['<%= yeoman.app %>/scripts/{,*/}*.{coffee,litcoffee,coffee.md}'], 139 | tasks: ['newer:coffee:dist'] 140 | }, 141 | coffeeTest: { 142 | files: ['test/spec/{,*/}*.{coffee,litcoffee,coffee.md}'], 143 | tasks: ['newer:coffee:test', 'karma'] 144 | }, 145 | compass: { 146 | files: ['<%= yeoman.app %>/styles/{,*/}*.{scss,sass}'], 147 | tasks: ['compass:server', 'autoprefixer'] 148 | }, 149 | gruntfile: { 150 | files: ['Gruntfile.js'] 151 | }, 152 | livereload: { 153 | options: { 154 | livereload: '<%= connect.options.livereload %>' 155 | }, 156 | files: [ 157 | '<%= yeoman.app %>/{,*/}*.html', 158 | '.tmp/views/{,*/}*.html', 159 | '.tmp/styles/{,*/}*.css', 160 | '.tmp/scripts/{,*/}*.js', 161 | '<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}' 162 | ] 163 | } 164 | }, 165 | 166 | // The actual grunt server settings 167 | connect: { 168 | options: { 169 | port: 9000, 170 | // Change this to '0.0.0.0' to access the server from outside. 171 | hostname: 'localhost', 172 | livereload: 35729 173 | }, 174 | proxies: [{ 175 | context: '/api', 176 | host: 'localhost', 177 | port: 3000 178 | }], 179 | livereload: { 180 | options: { 181 | open: true, 182 | base: [ 183 | '.tmp', 184 | '<%= yeoman.app %>' 185 | ], 186 | middleware: function (connect) { 187 | return [proxySnippet, 188 | mountFolder(connect, '.tmp'), 189 | mountFolder(connect, 'app')]; 190 | } 191 | }, 192 | }, 193 | test: { 194 | options: { 195 | port: 9001, 196 | base: [ 197 | '.tmp', 198 | 'test', 199 | '<%= yeoman.app %>' 200 | ] 201 | } 202 | }, 203 | dist: { 204 | options: { 205 | base: '<%= yeoman.dist %>' 206 | } 207 | } 208 | }, 209 | 210 | // Make sure code styles are up to par and there are no obvious mistakes 211 | jshint: { 212 | options: { 213 | jshintrc: '.jshintrc', 214 | reporter: require('jshint-stylish') 215 | }, 216 | all: [ 217 | 'Gruntfile.js' 218 | ] 219 | }, 220 | 221 | // Empties folders to start fresh 222 | clean: { 223 | dist: { 224 | files: [{ 225 | dot: true, 226 | src: [ 227 | '.tmp', 228 | '<%= yeoman.dist %>/*', 229 | '!<%= yeoman.dist %>/.git*' 230 | ] 231 | }] 232 | }, 233 | server: '.tmp' 234 | }, 235 | 236 | // Add vendor prefixed styles 237 | autoprefixer: { 238 | options: { 239 | browsers: ['last 1 version'] 240 | }, 241 | dist: { 242 | files: [{ 243 | expand: true, 244 | cwd: '.tmp/styles/', 245 | src: '{,*/}*.css', 246 | dest: '.tmp/styles/' 247 | }] 248 | } 249 | }, 250 | 251 | // Automatically inject Bower components into the app 252 | 'bower-install': { 253 | app: { 254 | html: '<%= yeoman.app %>/index.html', 255 | ignorePath: '<%= yeoman.app %>/' 256 | } 257 | }, 258 | 259 | 260 | // Compiles CoffeeScript to JavaScript 261 | coffee: { 262 | options: { 263 | sourceMap: true, 264 | sourceRoot: '' 265 | }, 266 | dist: { 267 | files: [{ 268 | expand: true, 269 | cwd: '<%= yeoman.app %>/scripts', 270 | src: '{,*/}*.coffee', 271 | dest: '.tmp/scripts', 272 | ext: '.js' 273 | }] 274 | }, 275 | test: { 276 | files: [{ 277 | expand: true, 278 | cwd: 'test/spec', 279 | src: '{,*/}*.coffee', 280 | dest: '.tmp/spec', 281 | ext: '.js' 282 | }] 283 | } 284 | }, 285 | 286 | // Compiles Jade to HTML 287 | jade: { 288 | compile: { 289 | files: htmlFiles 290 | } 291 | }, 292 | 293 | // Compiles Sass to CSS and generates necessary files if requested 294 | compass: { 295 | options: { 296 | sassDir: '<%= yeoman.app %>/styles', 297 | cssDir: '.tmp/styles', 298 | generatedImagesDir: '.tmp/images/generated', 299 | imagesDir: '<%= yeoman.app %>/images', 300 | javascriptsDir: '<%= yeoman.app %>/scripts', 301 | fontsDir: '<%= yeoman.app %>/styles/fonts', 302 | importPath: '<%= yeoman.app %>/bower_components', 303 | httpImagesPath: '/images', 304 | httpGeneratedImagesPath: '/images/generated', 305 | httpFontsPath: '/styles/fonts', 306 | relativeAssets: false, 307 | assetCacheBuster: false, 308 | raw: 'Sass::Script::Number.precision = 10\\n' 309 | }, 310 | dist: { 311 | options: { 312 | generatedImagesDir: '<%= yeoman.dist %>/images/generated' 313 | } 314 | }, 315 | server: { 316 | options: { 317 | debugInfo: true 318 | } 319 | } 320 | }, 321 | 322 | // Renames files for browser caching purposes 323 | rev: { 324 | dist: { 325 | files: { 326 | src: [ 327 | '<%= yeoman.dist %>/scripts/{,*/}*.js', 328 | '<%= yeoman.dist %>/styles/{,*/}*.css', 329 | '<%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}', 330 | '<%= yeoman.dist %>/styles/fonts/*' 331 | ] 332 | } 333 | } 334 | }, 335 | 336 | // Reads HTML for usemin blocks to enable smart builds that automatically 337 | // concat, minify and revision files. Creates configurations in memory so 338 | // additional tasks can operate on them 339 | useminPrepare: { 340 | html: '<%= yeoman.app %>/index.html', 341 | options: { 342 | dest: '<%= yeoman.dist %>' 343 | } 344 | }, 345 | 346 | // Performs rewrites based on rev and the useminPrepare configuration 347 | usemin: { 348 | html: ['<%= yeoman.dist %>/{,*/}*.html', '<%= yeoman.dist %>/views/{,*/}*.html'], 349 | css: ['<%= yeoman.dist %>/styles/{,*/}*.css'], 350 | options: { 351 | assetsDirs: ['<%= yeoman.dist %>'] 352 | } 353 | }, 354 | 355 | // The following *-min tasks produce minified files in the dist folder 356 | imagemin: { 357 | dist: { 358 | files: [{ 359 | expand: true, 360 | cwd: '<%= yeoman.app %>/images', 361 | src: '{,*/}*.{png,jpg,jpeg,gif}', 362 | dest: '<%= yeoman.dist %>/images' 363 | }] 364 | } 365 | }, 366 | svgmin: { 367 | dist: { 368 | files: [{ 369 | expand: true, 370 | cwd: '<%= yeoman.app %>/images', 371 | src: '{,*/}*.svg', 372 | dest: '<%= yeoman.dist %>/images' 373 | }] 374 | } 375 | }, 376 | htmlmin: { 377 | dist: { 378 | options: { 379 | collapseWhitespace: true, 380 | collapseBooleanAttributes: true, 381 | removeCommentsFromCDATA: true, 382 | removeOptionalTags: true 383 | }, 384 | files: [{ 385 | expand: true, 386 | cwd: '<%= yeoman.dist %>', 387 | src: ['*.html', 'views/{,*/}*.html'], 388 | dest: '<%= yeoman.dist %>' 389 | }] 390 | } 391 | }, 392 | 393 | // Allow the use of non-minsafe AngularJS files. Automatically makes it 394 | // minsafe compatible so Uglify does not destroy the ng references 395 | ngmin: { 396 | dist: { 397 | files: [{ 398 | expand: true, 399 | cwd: '.tmp/concat/scripts', 400 | src: '*.js', 401 | dest: '.tmp/concat/scripts' 402 | }] 403 | } 404 | }, 405 | 406 | // Replace Google CDN references 407 | cdnify: { 408 | dist: { 409 | html: ['<%= yeoman.dist %>/*.html'] 410 | } 411 | }, 412 | 413 | // Copies remaining files to places other tasks can use 414 | copy: { 415 | dist: { 416 | files: [{ 417 | expand: true, 418 | dot: true, 419 | cwd: '<%= yeoman.app %>', 420 | dest: '<%= yeoman.dist %>', 421 | src: [ 422 | '*.{ico,png,txt}', 423 | '.htaccess', 424 | '*.html', 425 | 'views/{,*/}*.html', 426 | 'bower_components/**/*', 427 | 'images/{,*/}*.{webp}', 428 | 'fonts/*' 429 | ] 430 | }, { 431 | expand: true, 432 | cwd: '.tmp/images', 433 | dest: '<%= yeoman.dist %>/images', 434 | src: ['generated/*'] 435 | }, { 436 | expand: true, 437 | cwd: '.tmp', 438 | dest: '<%= yeoman.dist %>/', 439 | src: ['views/**/*.html', 'scripts/**/*.js{.map}'] 440 | }] 441 | }, 442 | styles: { 443 | expand: true, 444 | cwd: '<%= yeoman.app %>/styles', 445 | dest: '.tmp/styles/', 446 | src: '{,*/}*.css' 447 | } 448 | }, 449 | 450 | // Run some tasks in parallel to speed up the build process 451 | concurrent: { 452 | server: [ 453 | 'coffee:dist', 454 | 'jade', 455 | 'compass:server' 456 | ], 457 | test: [ 458 | 'coffee', 459 | 'compass' 460 | ], 461 | dist: [ 462 | 'coffee', 463 | 'jade', 464 | 'compass:dist', 465 | 'imagemin', 466 | 'svgmin' 467 | ] 468 | }, 469 | 470 | // By default, your `index.html`'s will take care of 471 | // minification. These next options are pre-configured if you do not wish 472 | // to use the Usemin blocks. 473 | // cssmin: { 474 | // dist: { 475 | // files: { 476 | // '<%= yeoman.dist %>/styles/main.css': [ 477 | // '.tmp/styles/{,*/}*.css', 478 | // '<%= yeoman.app %>/styles/{,*/}*.css' 479 | // ] 480 | // } 481 | // } 482 | // }, 483 | // uglify: { 484 | // dist: { 485 | // files: { 486 | // '<%= yeoman.dist %>/scripts/scripts.js': [ 487 | // '<%= yeoman.dist %>/scripts/scripts.js' 488 | // ] 489 | // } 490 | // } 491 | // }, 492 | // concat: { 493 | // dist: {} 494 | // }, 495 | 496 | // Test settings 497 | karma: { 498 | unit: { 499 | configFile: 'karma.conf.js', 500 | singleRun: true 501 | } 502 | } 503 | }); 504 | 505 | grunt.loadNpmTasks('grunt-contrib-jade'); 506 | grunt.loadNpmTasks('grunt-connect-proxy'); 507 | grunt.loadNpmTasks('grunt-mkdir'); 508 | 509 | grunt.registerTask('serve', function (target) { 510 | if (target === 'dist') { 511 | return grunt.task.run(['build', 'connect:dist:keepalive']); 512 | } 513 | 514 | grunt.task.run([ 515 | 'clean:server', 516 | 'bower-install', 517 | 'concurrent:server', 518 | 'autoprefixer', 519 | 'configureProxies:server', 520 | 'connect:livereload', 521 | 'watch' 522 | ]); 523 | }); 524 | 525 | grunt.registerTask('server', function () { 526 | grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.'); 527 | grunt.task.run(['serve']); 528 | }); 529 | 530 | grunt.registerTask('test', [ 531 | 'clean:server', 532 | 'concurrent:test', 533 | 'autoprefixer', 534 | 'connect:test', 535 | 'karma' 536 | ]); 537 | 538 | grunt.registerTask('build', [ 539 | 'clean:dist', 540 | 'bower-install', 541 | 'useminPrepare', 542 | 'concurrent:dist', 543 | 'autoprefixer', 544 | 'concat', 545 | 'ngmin', 546 | 'copy:dist', 547 | 'cdnify', 548 | 'cssmin', 549 | 'uglify', 550 | 'rev', 551 | 'usemin', 552 | 'htmlmin' 553 | ]); 554 | 555 | grunt.registerTask('default', [ 556 | 'newer:jshint', 557 | 'test', 558 | 'build' 559 | ]); 560 | }; 561 | CODE 562 | 563 | run "npm install grunt-contrib-jade grunt-connect-proxy grunt-mkdir --save-dev" 564 | end 565 | 566 | git :init 567 | git add: '.' 568 | git commit: %Q(-m "Inital commit") 569 | --------------------------------------------------------------------------------