[LRUG] [JOBS] Help with AngularJS + Rails
Jasim A Basheer
jasim.ab at gmail.com
Thu Jan 16 10:41:15 PST 2014
Hi Diego,
We're working on an Angular-Rails project and have made some mistakes and learned a few things along the way. I cannot offer to do a paid and/or onsite gig, but I'd be happy to help with any specific questions you might have (thru LRUG, if it isn't terribly off-topic, or through email).
To start with, here are a bunch of pointers in no particular order. Apologies if you're already familiar with them. I'm also curious to hear everybody's thoughts on this and hopefully learn something in the process.
General
Use Coffeescript.
Start with Egghead.io. Read AngularJS by Brad Green, Shyam Seshadri (http://shop.oreilly.com/product/0636920028055.do). Spend a few hours and skim it cover-to-cover at least once. Angular documentation on the web is quite fragmented; a book is the best way to begin learning Angular.
If you aren't familiar with Underscore.js already, go through it. Add it to the project as soon as you begin. You'll need it.
Drop gem 'ng-rails-csrf' into your Gemfile to make Angular AJAX requests play well with Rails CSRF protection.
Directives
Some material on the web might lead you to think that a deep understanding of Directives is crucial to ‘get’ Angular. But for a beginner, the important thing is to just have fun doing things the Angular way (it is loads of fun, let me assure you). The basic directives Angular provides (eg: ng-repeat and ng-show) can go a surprisingly long way. Worry about transclusion, $compile, $digest and other arcane incantations only after you get comfortable with the basics.
One reason for you to write a directive is when you need to use existing widgets. A lot of popular widgets already have Angular wrappers. For example, we use the select2 wrapper https://github.com/angular-ui/ui-select2 for multi-select.
There is also AngularStrap (http://mgcrea.github.io/angular-strap/) with a bunch of nice components that wraps over Twitter Bootstrap. The v1 (http://mgcrea.github.io/angular-strap/1.0/) version has Datepicker and Timepicker which isn't available in the new version yet.
You would hit a point when off-the-shelf directives won’t be enough and you'd want to do something that requires DOM manipulation. That would be a good time to start digging into Directives. You can also start exploring Directives when you start seeing patterns in your controllers that can be extracted out into reusable components.
Serialization
We use ActiveModel::Serializer. It is a safe choice.
Pay close attention to what you include in each serializer. Here is the obvious thing: you can put a bunch of has_many in your User model, and if you keep doing that for other objects, arender json: User.all will end up sending your entire db graph to the client.
We implement a base serializer for most entities with some basic fields and no associations. When we need more data/associations, we sub-class them for that specific use case. Discourse follows a similar style - https://github.com/discourse/discourse/tree/master/app/serializers
Try not to do any decoration on the server side; it is tempting with all the view helper goodness Rails provides, but then you are splitting logic between client and server and it causes all sorts of headaches. You can use Angular filters as client-side helpers on some occasions. eg: {{ start_time | date }} prints out nicely formatted dates.
Forms, validations and POJO form objects
Angular gives you validation attributes like ng-required, ng-minlength, ng-maxlength etc. on all form INPUTs. For simple use cases, this is the best way to go. But for a complex form, we ended up building an ActiveModel-like POJO Form Object to hold validation information, and for doing some slight transformations on the data before submission. We bound this object to the scope and removed a lot of clutter from our controllers that had begun to get bloated. We have to see how this pans out in the long run; I can give code snippets if you wish.
When I introduced Angular to our Rails project that was already under development, I tried to minimize the amount of change required by trying to keep form POSTs as they were.
<input type='text' ng-model='post.title' name='post[title]'>
<input type='submit' />
It worked for the most part, but we ran into some edge cases that wasted a lot of time. That was when I learned to let go and fully embrace the Angular way. We switched over to something like this:
<input type='text' ng-model='post.title' name='post[title]'>
<button ng-click='submitForm()'>
And the controller:
angular.module('myApp').controller 'postCtrl', ['$log', '$scope', '$http', '$state', '$stateParams', '$window', 'flash', ($log, $scope, $http, $state, $stateParams, $window, flash) ->
$scope.submitForm = ->
$http.post(
Routes.posts_path,
$scope.post # this is the ng-model hash we bound the input boxes to.
).success((data, status) ->
$state.go("posts.show", {post_id: data.post.id})
).error((data, status) ->
if data['message']
flash.error = data['message']
else
flash.error = "Could not create Post. #{data}"
)
# `flash` is from https://github.com/wmluke/angular-flash.
# $state.go is ui-router. https://github.com/angular-ui/ui-router. More on that later.
# we use JSRoutes to access Rails routes from JS. Quite useful. https://github.com/railsware/js-routes/
The above code isn't ideal. You will be better off putting the $http.post inside an Angular service. This looks like a good post describing that: http://sravi-kiran.blogspot.in/2013/03/MovingAjaxCallsToACustomServiceInAngularJS.html.
Rails+Angular
Go all the way in. Don’t try to keep a mix of Angular and non-Angular pages. Except for Devise, all our pages are rendered client-side. We don't have to worry about SEO since this is an internal app.
Use ui-router https://github.com/angular-ui/ui-router, not the one that comes with Angular. The ui-router README could be confusing, but like most of Angular documentaiton, it becomes obvious in hindsight if you persevere.
We keep everything together in the same Rails project. Here is a rough structure:
routes.rb:
root ‘base#angular’
resources :posts, only: [:index]
app/views/base/angular.html.erb:
<div ng-app='myApp'>
<div ui-view></div>
</div>
posts_controller.rb:
class PostsController < ApplicationController
respond_to :json
def index
render json: Post.all, root: false
end
end
app/assets/javascripts/routes.coffee.erb:
angular.module('myApp').run ($rootScope, $state, $stateParams) ->
$rootScope.$state = $state
$rootScope.$stateParams = $stateParams
angular.module('myApp').config ($stateProvider, $urlRouterProvider) ->
# The default route
$urlRouterProvider.when("", "/posts/list")
$stateProvider
.state("posts",
url: "/posts"
controller: "postsRootCtrl"
template: '<div ui-view></div>'
abstract: true
).state("posts.index",
url: "/list"
controller: "postsIndexCtrl"
templateUrl: "<%= asset_path('posts/index.html.slim') %>"
resolve:
postsPromise: ($http) ->
$http.get(Routes.posts_path())
)
app/assets/javascripts/posts.js.coffee:
angular.module('myApp').controller 'postsCtrl', ['postsPromise', '$log', '$scope', '$http', '$state', '$stateParams', '$window', (postsPromise, $log, $scope, $http, $state, $stateParams, $window) ->
$scope.posts = postsPromise.data
app/assets/templates/index.html.slim
div ng-repeat='post in posts'
h1
| {{post.title}}
p
| {{post.body}}
Quirks and bugs
It is worth keeping in mind that in JS {} == {} is false, since comparisons are by object identity, not by object value. You can do explict comparison using Angular.equals when you need it. But in some cases Angular does an implicit comparison, for which it doesn't use Angular.equals. For example, if you try loading a page with a bunch of pre-selected radio-buttons, if their values are JS Objects (Hash), Angular won't pre-select them since the comparison will fail. This is not an issue if your values are strings/numbers.
JS treats the keys of all objects as strings.a={1: "Hello"}; _.each(a, (value, key) -> console.log(typeof key)). If your server sends Hashes whose keys are numeric ids, they'll end up in JS with string ids.
A quick and dirty way to inspect an Angular model is to drop this in your Angular view template:
pre
| {{ post | json }}
There is also Batarang, a Chrome plugin which lets you inspect your scope interactively. But I've found it to causes my pages to misbehave in certain cases. I these days use angular-chrome-debug, (https://gist.github.com/mzgol/7893061) which is quite light-weight. I load this script in my app in development env so that I don't have to go thru Chrome Snippets everytime.
Angular has Angular.copy for deep copy when you need it - https://github.com/angular/angular.js/blob/master/src/Angular.js#L725.
Well, that's mostly it. Please keep in mind that the structure I described is working well for our app, which is not meant for public consumption. You might want to look at keeping your Angular and Rails project separately and utilize JS build tools like Bower, Yeoman and Grunt if your app needs that.
I'm generally liking building rich UIs with Angular and wouldn't go back to building for the web without using data-binding. Awesome sauce.
--
Jasim A Basheer — Nilenso Software — http://nilenso.com
http://twitter.com/jasim_ab
On 16-Jan-2014, at 6:20 am, Matthew Rudy Jacobs <matthewrudyjacobs at gmail.com> wrote:
> On 15 January 2014 14:38, Matt Spendlove <matt at cenatus.org> wrote:
>
> I’ve never used the service but these guys seem to be trying to solve your kind of request:
>
> https://www.airpair.com
>
> Also check out CodeMentor, who do something similar.
> https://www.codementor.io/directory/angularjs
> _______________________________________________
> Chat mailing list
> Chat at lists.lrug.org
> http://lists.lrug.org/listinfo.cgi/chat-lrug.org
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.lrug.org/pipermail/chat-lrug.org/attachments/20140117/646fe471/attachment-0003.html>
More information about the Chat
mailing list