Oxymoron

Oxymoron is a new approach to build the architecture of SPA-applications. This gem makes a strong connection between server and client side. It is a fully complete framework based on AngularJS and Ruby on Rails.

You can greatly accelerate the development of your applications with it. But not only that! The speed of your application will accelerate at times.

This gem contains the Angular Resources and UI Router States which are automatically generated from the Rails routes (from routes.rb). It also includes all the necessary configs and the most useful services and directives.

Setting up

Add it to your Gemfile:

gem 'oxymoron'

Add dependencies to your application.js

/*
= require oxymoron/underscore
= require oxymoron/angular
= require oxymoron/angular-resource
= require oxymoron/angular-ui-router
= require oxymoron/ng-notify
= require oxymoron
*/
and application.css
/*
*= require oxymoron/ng-notify
*/

As you see, it is using Angular packages, underscoreUI Router and  ngNotify from  Oxymoron repo. But you can use them from other repository.

Dependency injection

Inject required modules oxymoron and ui.router into you angular application.

angular.module('app', ['ui.router', 'oxymoron']);

Routing and states

Create file config/routes.js , which will contain the application routing. Method $stateProvider.rails()  transform routes.rb to Angular UI Router states.

angular.module('app')
.config(['$stateProvider', function ($stateProvider) {
  $stateProvider.rails()
}])
Now, if you start your application, rake or just change the routes.rb, new routing will automatically regenerated. You can find it in oxymoron.js.

Use ui-sref  insted of link_to helper.

For example, if you have:

resources :posts
you should use:
a ui-sref="posts_path" All posts
a ui-sref="new_post_path" New post
a ui-sref="edit_post_path({id: id})" Edit post
a ui-sref="post_path({id: id})" Show

Layouts

It's very important to disable layouts for all ajax query. Otherwise it can call recursive views rendering.So, inside your ApplicationController, redefine layout like this:

class ApplicationController < ActionController::Base
  layout proc {
    if request.xhr?
      false
    else
      'application' # or other layout from views/layouts/
    end
  }
end

Edit your layout with ui-view notation.Change

= yield
to
ui-view
or for SEO-friendly
ui-view
  div[ng-non-bindable]
    = yield

FormBuilder

Default FormBuilder was rewrited with new paradigm. Now, form_for  helper is rendering with ng-submit. And text_field, email_field, etc with ng-model. Add FormBuilder definition to initializers/oxymoron.rb

ActionView::Base.default_form_builder = OxymoronFormBuilder

Also, you can inherit your FormBuilder from OxymoronFormBuilder and add some custom helpers using ng-model.

Rendering control

All controllers methods, which expecting to have html and json response, must be wrapped with respond_to, like this:

class TestController < ActiveRecord::Base
  def index
    respond_to do |format|
      format.html
      format.json {
        render json: {}
      }
    end
  end
end

Rendering flash notice

If you want to show a notice, you can use msg field with http-statuses.

For example:

render json: {msg: "Successfully authorized"} # green notice
render json: {msg: "Not autorized"}, status: 401 # red notice

Info: If you want to rewrite this behaviour, you should redefine Notice.callback(type, result)  as you want, where type is string: "success" or "error".

Rendering form errors

Validation is automatically invoked, when you render errors field. Just render if you have validation errors with create or update and it's automatically add errors on your form.

def create
  @post = Post.new post_params
  if @post.save
    render json: {msg: "Post was successfully created"}
  else
    render json: {errors: @post.errors}, status: 422
  end
end

Redirect to state

If after request you want to redirect user to other state(local path), you can user redirect_to  option.

render json: {redirect_to: "root_path"}
And you will be redirected to the root_path without page reloading.

You can pass an options, like:

render json: {redirect_to: "user_path", redirect_options: {id: 1}}
And you'll be redirected to the "/users/1" url

Redirect to URL

Also, you can use redirect_to_url. It's using $location.url method to change your local state without page reloading.

render json: {redirect_to_url: "/users/1?foo=bar"}
# OR
render json: {redirect_to_url: user_path(id: 1, foo: "bar")}

But if you want to get query params, you should use $location.search() instead of $state.params.

Reload page

Sometimes, you may need to reload the user's page. Use reload  option:

render json: {reload: true}

Services

Action

Action – is the most useful service that resolves directly to the controller based on the current state and resources GET-methods(routes.rb). Be default it is index, show, new, edit. You can separate your code as you want with this service and it will be invoked in the case of compliance action.

For example:
resources :posts do
  get "custom_method"
end
angular.module('app')
  .controller('PostsCtrl', ['action', function (action) {
    // Called only on '/posts'
    action('index', function(){});

    // Called only for '/posts/:id'
    action('show', function (params){});

    // Called only for '/posts/:id/edit' or '/posts/new'
    action(['edit', 'new'], function(){})

    // Called only for your custom resource method
    action('custom_method', function(){})
  }])

Resources

Whenever when you update routes.rb, resources are automatically udated. You should inject  it is as a service. They have base methods and extending from your custom methods in routes.rb

Standart methods for post resource:

Post.query() // => GET /posts.json
Post.get({id: id}) // => GET /posts/:id.json
Post.new() // => GET /posts/new.json
Post.edit({id: id}) // => GET /posts/:id/edit.json
Post.create({post: post}) // => POST /posts.json
Post.update({id: id, post: post}) // => PUT /posts/:id.json
Post.destroy({id: id}) // => DELETE /posts/:id.json

Earlier we defined method custom_method. When you define a custom method in routes.rb, you also can use it.

Post.custom_method() // => GET /posts/custom_method

Important: Resource must be declared with show-method to generate corresponding service.

Name of the Resources consists of a camelized route name. For example:

resources :posts # => Post

namespace :admin do
  resources :posts # => AdminPost
end

Reource decorator

You can decorate resource with shared methods. Just define your own service with "resourceDecorator"  name and it will be applied for all resources.

angular.module('app').factory('resourceDecorator', [function () {
  return function(resource) {
    // do it what you want with generated resources
    // you can add some method through __prototype__
    // or anything else
    return resource;
  };
}])

Http Interceptor

You can catch all response which was sent from resource/$http like that:

$rootScope.$on('loading:finish', function (h, res) {
  // success response
})

$rootScope.$on('loading:error', function (h, res, p) {
  // response with errors
})

PS: It's using for Rendering Controls. So you can extend it as you please.

Sign (in, up, out)

It's very useful and simple service for the ajax authorization with Devise. You can read more about Devise integration in this section.

Sign.in(user_params, callback) # => POST /users/sign_in
Sign.up(user_params, callback) # => POST /users
Sign.out(callback) # => DELETE /users/sign_out

Controllers

Controllers naming

Name of the Angular Controller consists of a Rails Controller name with replacement "Controller" to "Ctrl" and disposal :: if exist.

Using ctrl.save

OxymoronFormBuilder adds ng-submit for form_for. All you need is to define a method ctrl.save. In most cases this will be as follows:

= form_for Post.new do |f|
  = f.text_field :title
  = f.text_area :desctiption

  = f.submit "Save"
angular.module('app')
  .controller('PostsCtrl', ['Post', 'action', function (Post, action) {
    var ctrl = this;

    action('new', function(){
      ctrl.save = Post.create;
    });

    action('edit', function (params){
      ctrl.save = Post.update;
    })

  }])

Directives

ngYield and contentFor

It's realy like yield and content_for  helpers but only for clientside. It can be very useful for dinamic page-title.

title ng-yield="title"
body
  div content-for="title" foobar

"foobar" will be inserted into the title and not displayed in the .content block

ngYield Option Description
onlyText Can be "true" or "false". Remove all tags and identation
prefix Add some prefix text, like "Google – "
postfix Add some postfix text, like " | Google.com"

Fileupload

This directive can help you to upload some files with AJAX-request. You can read more about Carrierwave integration in this section.

input type="file" fileupload="'/uploads/image'" ng-model="uploadedFile"
Fileupload Option Description
fileupload String url to file uploader
ngModel Result from server. Must be an array.
percentCompleted The percentage of loaded
multiple Mark it for multiple upload
hash True or false. Mark it if you return from server structure like that:{images: [], videos: [], audios: [], etc: []}

ClickOutside

Very useful directive for catching a click outside the block. But be careful with it. It will be called every time when you clicked out of the block.

div click-outside="isShow=false" ng-show="isShow"

Routes

Routes.js

In window you can find Routes variable, which contains all routes of your app (such as  JsRoutes).

Routes.posts_path() //=> '/posts'
Routes.post_path({id: 1}) //=> '/posts/1'
Routes.post_path({id: 1, format: "json"}) //=> '/posts/1.json'
Routes.posts_url() //=> 'http://localhost:3000/posts'

Render an array

If your expect that your controller method return an array, you must mark these routes with is_array property. If you do not, it throw an exception from angular.

resources :posts do
  get 'return_array', is_array: true
end

Optional UI Router params

That query parameters are mapped to UI Router's $stateParams object, you need to declare ui_params in you routes.rb:

get 'method_with_ui_params', ui_params: [:filter, :search, :page]

Page caching

By default all pages(except show) cached inside AngularJS templateCache. If you want to disable this behaviour, mark these routes with cache property.
resources :posts do
  get 'my_method', cache: false
end

Anvanced

Oxymoron::Config.setup do |c|
  # change path for generated oxymoron.js
  c.oxymoron_js_path = Rails.root.join('app', 'assets', 'javascripts', 'public') 

  # Change form builder. By default used OxymoronFormBuilder
  c.form_builder = MyFormBuilder 

  # Disabled rewrite form_for method in ActionView::FormHelper. In this case use helpers oxymoron_form_for and oxymoron_field_for
  c.rewrite_form_for = false 
end

Lifehacks

Devise integration

SessionsController:
class Auth::SessionsController < Devise::SessionsController
  layout proc {
    if request.xhr?
      false
    else
      "application"
    end
  }

  after_action :set_csrf_headers, only: [:create, :destroy]

  def create
    self.resource = warden.authenticate(auth_options)

    if self.resource
      sign_in(resource_name, self.resource)
      render json: {msg: "Successfully Signed In"}
    else
      render json: {msg: I18n.t('devise.failure.not_found_in_database', authentication_keys: 'email')}, status: 401
    end
  end

  def destroy
    Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name)
    render json: {msg: "Successfully Signed out"}
  end

  protected
  def set_csrf_headers
    cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?  
  end

endRegistrationsController:
class Auth::RegistrationsController < Devise::RegistrationsController
  layout proc {
    if request.xhr?
      false
    else
      "application"
    end
  }

  def create
    build_resource(sign_up_params)

    if resource.save
      if resource.active_for_authentication?
        sign_up(resource_name, resource)

        render json: {
          msg: "Welcome! You have signed up successfully.",
        }
      else
        expire_data_after_sign_in!
        respond_with resource, location: after_inactive_sign_up_path_for(resource)
      end
    else
      render json: {
        msg: resource.errors.full_messages.first,
        errors: resource.errors,
        form_name: "sign_up_form"
      }, status: 403
    end
  end

  def destroy
    Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name)
    resource.soft_delete!
    render json: {msg: "Successfully deleted"}
  end

  protected
  def after_update_path_for(resource)
    edit_user_registration_path
  end

end

Carrierwave integration

Integration takes place in several steps.

  1. First we need to create a model Image(for example) with body
    rails g model image body:string
  2. Second, create uploader:
    rails g uploader image
    and mount it on our model
    mount_uploader :body, ImageUploader
  3. Third, create UploadsController:
                   
    class UploadsController < ApplicationController
      def image
        @image = Image.create(body: params[:attachments].first)
        render json: [@image]
      end
    end
  4. Fourthly, create route inside routes.rb:
    resources :uploads, only: [:show] do
      collection do
        post "image"
      end
    end
  5. and use fileupload directive:
    input type="file" fileupload="'/uploads/image'" ng-model="uploadedFile"
    img ng-src="{{uploadedFile.body.url}}"

ActiveModel Serializers

You can use ActiveModel Serializers with Oj if you want maximum performance. Also, you can add to your ApplicationController some useful methods like:

def serialize res, options = {}
  serializer = options[:serializer] || "#{res.model_name}Serializer".constantize
  if res.respond_to?('each')
    ActiveModel::ArraySerializer.new(res, each_serializer: serializer, scope: self, root: false).as_json
  else
    serializer.new(res, scope: self, root: false).as_json
  end
end

def dump res, options = {}
  Oj.dump serialize(res, options).as_json
end
and you can use it in very simple notation and not specified serializer name:
serialize(@posts)
#or
dump(@posts)
Serializer name will be taken on the basis of the model name.

Admin and public part

Often there is a need for the separation of parts of the application in the admin panel and the public part. You can do this very nicely:

  1. Generate Public::ApplicationController and Admin::ApplicationController
    class Public::ApplicationController < ApplicationController
      layout proc {
        if request.xhr?
          false
        else
          "public"
        end
      }
    end
    class Admin::ApplicationController < ApplicationController
      layout proc {
        if request.xhr?
          false
        else
          "admin"
        end
      }
    end
  2. Create layouts views/layouts/admin.html.slim and views/layouts/public.html.slim like that:
    
    html ng-app="app"
      head
        meta charset="UTF-8"
        title Public
        = csrf_meta_tags
        = stylesheet_link_tag 'public'
        base href="/"
      body
        ui-view
        = javascript_include_tag 'public'
    
    html ng-app="app"
      head
        meta charset="UTF-8"
        title Admin
        = csrf_meta_tags
        = stylesheet_link_tag 'admin'
        base href="/"
      body
        ui-view
        = javascript_include_tag 'admin'
  3. Create assets:
    • stylesheets/admin/
    • stylesheets/public/
    • stylesheets/public.scss
      //= require_tree public
    • stylesheets/admin.scss
      //= require_tree admin
    • javascripts/public.js
      //= require_tree public
    • javascripts/admin.js
      //= require_tree admin
    and add it to the config/initializers/assets.rb
    Rails.application.config.assets.precompile += %w( 
      public.js admin.js public.css admin.css
    )

Examples

  1. Oxymoron Demo App
  2. Basic forum based on Oxymoron (working demo)

Conclusion

If you are russian, you can read my articles of the gem on the Habrahabr:  https://habrahabr.ru/post/300954/  and  https://habrahabr.ru/post/283214/.

I will be glad your feedback. Thank you for your time and Happy coding :)