📜 ⬆️ ⬇️

AngularJS and Ruby on Rails Single Page Application Building Architecture

Having become interested in the methodology for building SPA applications on Ruby on Rails, I came up with some ideas that are now being implemented in each of my applications and were even later singled out into a separate Oxymoron gem. Currently, Oxymoron has more than 20 fairly large commercial rail applications written. I want to bring heme to public court. Therefore, I will continue my further narration on its basis.

An example of a finished application.

What tasks does Oxymoron solve?


For me, this gem reduces the amount of routine code by an order of magnitude and, as a result, significantly increases the speed of development. It makes it very easy to build AngularJS + RoR interaction.

  1. Automatic construction of AngularJS routing based on routes.rb
  2. Autogenerating AngularJS resources from routes.rb
  3. Set architectural strictness for AngularJS controllers
  4. Registering constantly used configs
  5. Validation of forms
  6. FormBuilder automatically ng-model
  7. Notification
  8. Frequently used directives (ajax fileupload, click-outside, content-for, check-list)
  9. Implementation of a compact analog JsRoutes

How it works?


First of all, it is necessary to connect gems in the Gemfile:
')
gem 'oxymoron' 

Now, each time you change routes.rb , or when you restart the application, the file oxymoron.js will be generated in app / assets / javascripts , containing all the necessary code for building the application.

The next step is to configure the assets. In the simplest case, it looks like this:

For application.js:

 /* = require oxymoron/underscore = require oxymoron/angular = require oxymoron/angular-resource = require oxymoron/angular-cookies = require oxymoron/angular-ui-router = require oxymoron/ng-notify = require oxymoron = require_self = require_tree ./controllers */ 

For application.css:

 /* *= require oxymoron/ng-notify *= require_self */ 

We use the UI Router, which means you need to define the ui-view tag in our layout. Since the application will use HTML5 routing, you must specify a base tag. In our case, this is application.html.slim. I use SLIM as a preprocessor and strongly recommend it to everyone.

 html ng-app="app" head title  base href="/" = stylesheet_link_tag 'application' body ui-view = javascript_include_tag 'application' 

For all AJAX requests, you must disable layout. To do this, in ApplicationController we write the necessary logic:

 layout proc { if request.xhr? false else "application" end } 

For correct processing of forms and setting up of ng-model, it is necessary to create an initializer that overrides the default FormBuilder on OxymoronFormBuilder.

 ActionView::Base.default_form_builder = OxymoronFormBuilder 

The last thing you need to do is inject the oxymoron module into your application and inform the UI Router that the automatically generated routing will be used:

 var app = angular.module("app", ['ui.router', 'oxymoron']); app.config(['$stateProvider', function ($stateProvider) { $stateProvider.rails() }]) 

Everything is ready to create a full-fledged SPA application!

Let's write the simplest SPA blog


So. First we will prepare a Post model and a RESTful controller for managing this model. To do this, run the commands in the console:

 rails g model post title:string description:text rake db:migrate rails g controller posts index show 

In routes.rb, create a posts resource:

 Rails.application.routes.draw do root to: "posts#index" resources :posts end 

Now we will describe the methods of our controller. Often, the same method can return both JSON structures and HTML markup in the response, such methods need to be wrapped in respond_to .

An example of a typical Rails controller
 class PostsController < ActiveRecord::Base before_action :set_post, only: [:show, :edit, :update, :destroy] def index respond_to do |format| format.html format.json { @posts = Post.all render json: @posts } end end def show respond_to do |format| format.html format.json { render json: @post } end end def new respond_to do |format| format.html format.json { render json: Post.new } end end def edit respond_to do |format| format.html format.json { render json: @post } end end def create @post = Post.new post_params if @post.save render json: {post: @post, msg: "Post successfully created", redirect_to: "posts_path"} else render json: {errors: @post.errors, msg: @post.errors.full_messages.join(', ')}, status: 422 end end def update if @post.update(post_params) render json: {post: @post, msg: "Post successfully updated", redirect_to: "posts_path"} else render json: {errors: @post.errors, msg: @post.errors.full_messages.join(', ')}, status: 422 end end def destroy @post.destroy render json: {msg: "Post successfully deleted"} end private def set_post @post = Post.find(params[:id]) end def post_params params.require(:post).permit(:title, :description) end end 


Each Rails controller has an AngularJS controller. The matching rule is very simple:

 PostsController => PostsCtrl Admin::PostsController => AdminPostsCtrl #    namespace Admin 

Create the appropriate controller in app / javascripts / controllers / post_ctrl.js :

An example of a typical AngularJS controller
 app.controller('PostsCtrl', ['Post', 'action', function (Post, action) { var ctrl = this; //     '/posts' action('index', function(){ ctrl.posts = Post.query(); }); //    '/posts/:id' action('show', function (params){ ctrl.post = Post.get({id: params.id}); }); //   '/posts/new' action('new', function(){ ctrl.post = Post.new(); //   ,       . . . ctrl.save = Post.create; }); //   '/posts/:id/edit' action('edit', function (params){ ctrl.post = Post.edit({id: params.id}); //      ctrl.save = Post.update; }) //  .     edit  new. action(['edit', 'new'], function(){ // }) action(['index', 'edit', 'show'], function () { ctrl.destroy = function (post) { Post.destroy({id: post.id}, function () { ctrl.posts = _.select(ctrl.posts, function (_post) { return _post.id != post.id }) }) } }) //     routes.rb     .  : '/posts/some_method' action('some_method', function(){ // }) // etc }]) 


Pay attention to the action factory. With it, it is very convenient to share code between application pages. The factory is resolved through the generated state in oxymoron.js and, as a result, knows the current rail method of the controller.

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

Next you should pay attention to the Post factory. This factory is automatically generated from the resource defined in routes.rb. For proper generation, the resource must have a show method defined. The following methods of working with the resource are available from the box:

 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 

Custom resource methods (member and collection) work the same way. For example:

 resources :posts do member do get "comments", is_array: true end end 

Will create the appropriate method for the AngularJS resource:

  Post.comments({id: id}) //=> posts#comments 

Set the is_array option : true if it is expected that an array is expected in response. Otherwise, AngularJS will throw an exception.

It remains to create the missing view.

posts / index.html.slim
 h1 Posts input.form-control type="text" ng-model="search" placeholder="" br table.table.table-bordered thead tr th Date th Title th tbody tr ng-repeat="post in ctrl.posts | filter:search" td ng-bind="post.created_at | date:'dd.MM.yyyy'" td a ui-sref="post_path(post)" ng-bind="post.title" td.w1 a.btn.btn-danger ng-click="ctrl.destroy(post)"  a.btn.btn-primary ui-sref="edit_post_path(post)"  


posts / show.html.slim
 .small ng-bind="ctrl.post.created_at | date:'dd.MM.yyyy'" a.btn.btn-primary ui-sref="edit_post_path(ctrl.post)"  a.btn.btn-danger ng-click="ctrl.destroy(ctrl.post)"  h1 ng-bind="ctrl.post.title" p ng-bind="ctrl.post.description" 


posts / new.html.slim
 h1 New post = render 'form' 


posts / edit.html.slim
 h1 Edit post = render 'form' 


posts / _form.html.slim
 = form_for Post.new do |f| div = f.label :title = f.text_field :title div = f.label :description = f.text_area :description = f.submit "Save" 


Particular attention should be paid to the result of generating the form_for helper.

 <form ng-submit="formQuery = ctrl.save({form_name: 'post', id: ctrl.post.id, post: ctrl.post}); $event.preventDefault();"></form> 

It is enough to define the ctrl.save method inside the controller and it will be executed each time the form is submitted and transfer the parameters that you see. But since these parameters are ideal as arguments for the update and create resource methods, we can only write ctrl.save = Post.create in our controller. In the PostsCtrl listing, this moment is marked with a corresponding comment.

For the text_field and text_area tags, the ng-model attribute was automatically added. The rule of ng-model is the following:

 ng-model="ctrl._._" 

Json render functionality: {}


In the PostsController rail listing you probably noticed the msg, redirect_to, and so on fields in the render method. For these fields, a special interceptor works, which takes the necessary action before transmitting the result to the controller.

msg - the content will be shown in a green pop-up at the top of the screen. If you pass an error status to render, the color will change to red.

errors - takes the errors object, serves to display the errors of the form fields themselves.

redirect_to - redirect to the necessary UI Router state

redirect_to_options - if the state requires an option, for example, the show page requires an id, then you must specify them in this field

redirect_to_url - go to the specified url

reload - completely reload the page to the user

All these actions occur without reloading the user’s page. Uses HTML5 routing based on UI Router.

Now without link_to


We used to use the link_to helper when we wanted to define a link depending on the name of the route. Now this feature is implemented by ui-sref in our usual manner of describing the route.

  a ui-sref="posts_path"   a ui-sref="post_path({id: 2})"2 a ui-sref="edit_post_path({id: 2})"2 a ui-sref="new_post_path"    

Lightweight analog js-routes. CONFLICT


In the global scope, you can find the variable Routes. It works almost exactly the same way as js-routes. The only difference is that this implementation accepts only the object and does not have sugar in the form of a number argument. A conflict is possible, so I recommend disabling js-routes.

 Routes.posts_path() // => "/posts" Routes.new_post_path() // => "/post/new" Routes.edit_posts_path({id: 1}) // => "/post/1/edit" //    Routes.defaultParams = {id: 1} Routes.post_path({format: 'json'}) // => "/posts/1.json" 

Total


We wrote a primitive SPA application. In this case, the code looks absolutely rail, the logic described at least, and the one that is already, is the most common. I understand that there is no limit to perfection and that Oxymoron is far from ideal, however, I hope that I could interest someone with my approach. I will be glad to any criticism and any positive participation in the life of the heme.

An example of a finished application.

Source: https://habr.com/ru/post/283214/


All Articles