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, underscore, UI 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
toui-view
or for SEO-friendlyui-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 end
RegistrationsController: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.
-
First we need to create a model Image(for example) with body
rails g model image body:string
-
Second, create uploader:
and mount it on our modelrails g uploader image
mount_uploader :body, ImageUploader
-
Third, create UploadsController:
class UploadsController < ApplicationController def image @image = Image.create(body: params[:attachments].first) render json: [@image] end end
-
Fourthly, create route inside routes.rb:
resources :uploads, only: [:show] do collection do post "image" end end
-
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:
-
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
-
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'
-
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
Rails.application.config.assets.precompile += %w( public.js admin.js public.css admin.css )
Examples
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 :)