An article about Ruby in the DomClick company blog ! How did it happen that a dead language was brought to a young company ? The secret is that you can quickly write and test a business idea in Ruby. And this is done not without the help of Rails and Active Admin - a library that allows you to quickly create an admin panel with minimal effort and time.

, Active Admin 15- . ( ), .
, Active Admin.
Active Admin , arbre, formtastic, inherited_resources ransack. . β , Active Admin.
Arbre:
Active Admin β : , action', , , . - Β« single responsibility?Β» . , .
Arbre β Ruby. , DSL Arbre:
html do
head do
title('Welcome page')
end
body do
para('Hello, world')
end
end
DSL . , Active Admin tabs, table_for, paginated_collection . Arbre .
Arbre: hello world
Arbre, Admin::Components::HelloWorld Arbre::Component:
# app/admin/components/hello_world.rb
module Admin
module Components
class HelloWorld < Arbre::Component
builder_method :hello_world
def build(attributes = {})
super(attributes)
text_node('Hello world!')
add_class('hello-world')
end
def tag_name
'h1'
end
end
end
end
: builder_method , DSL. , , #build.
Arbre β DOM- ( frontend-, 2012 ). div, , #tag_name. #add_class, , class DOM-.
. , app/admin/dashboard.rb
# app/admin/dashboard.rb
ActiveAdmin.register_page 'Dashboard' do
menu priority: 1, label: proc { I18n.t('active_admin.dashboard') }
content do
hello_world
end
end

.
Arbre: ()
, , Arbre , , . , (Article) (Comment) 1:M. 10 ( show).
# app/admin/articles.rb
ActiveAdmin.register Article do
permit_params :title, :body
show do
attributes_table(:body, :created_at)
panel I18n.t('active_admin.articles.new_comments') do
table_for resource.comments.order(created_at: :desc).first(10) do
column(:author)
column(:text)
column(:created_at)
end
end
end
end

. ActiveAdmin::Views::Panel. ( hello_world ) panel, panel div, .
app/admin/components/articles/new_comments.rb, . , Active Admin , app/admin/**/*:
# app/admin/components/articles/new_comments.rb
module Admin
module Components
module Articles
class NewComments < ActiveAdmin::Views::Panel
builder_method :articles_new_comments
def build(article)
super(I18n.t('active_admin.articles.new_comments'))
table_for last_comments(article) do
column(:author)
column(:text)
column(:created_at)
end
end
private
def last_comments(article)
article.comments
.order(created_at: :desc)
.first(10)
end
end
end
end
end
panel app/admin/articles.rb resource:
# app/admin/articles.rb
ActiveAdmin.register Article do
permit_params :title, :body
show do
attributes_table(:body, :created_at)
articles_new_comments(resource)
end
end
! , resource , . , resource, , .
, show ( ) partial:
# app/admin/articles.rb
ActiveAdmin.register Article do
show do
render('show', article: resource)
end
end
# app/views/admin/articles/_show.html.arb
panel(ActiveAdmin::Localizers.resource(active_admin_config).t(:details)) do
attributes_table_for(article, :body, :created_at)
end
articles_new_comments(article)
, .erb , , , .
Arbre:
, Active Admin .
arbre activeadmin, . , gem activeadmin_addons, .
, , , .
Formtastic:
Formtastic β DSL. :
semantic_form_for object do |f|
f.inputs
f.actions
end
Formtastic object input' -. input' README. Arbre, Formtastic -. , , hello world .
Formtastic: hello world
Arbre, app/admin/inputs:
# app/admin/inputs/hello_world_input.rb
class HelloWorldInput
include Formtastic::Inputs::Base
def to_html
"Input for ##{object.public_send(method)}"
end
end
input, :as, , :
# app/admin/articles.rb
ActiveAdmin.register Article do
form do |f|
f.inputs do
f.input(:id, as: :hello_world)
f.input(:title)
f.input(:body)
end
f.actions
end
end

( object method) #initialize, Formtastic::Inputs::Base. input' #to_html.
, read-only . , Formtastic, hello world read-only input. :
# app/admin/inputs/hello_world_input.rb
class HelloWorldInput
include Formtastic::Inputs::Base
def to_html
input_wrapping do
label_html <<
object.public_send(method).to_s
end
end
end

, β . input_wrapping Formtastic::Inputs::Base::Wrapping input'. , . label_html Formtastic::Inputs::Base::Labelling input'. hello world input ( ).
, , JS-.
Formtastic: ()
, , HTML, CSS JS. input'.
, : . , JavaScript' , : Countable.js. input , .
, input':
- input
div; - CSS-
div; - Countable.js
div.
Formtastic::Inputs::TextInput. class="countable-input" textarea, div class="countable-content":
# app/admin/inputs/countable_input.rb
class CountableInput < Formtastic::Inputs::TextInput
def to_html
input_wrapping do
label_html <<
builder.text_area(method, input_html_options.merge(class: 'countable-input')) <<
template.content_tag(:div, '', class: 'countable-content')
end
end
end
, . input_html_optionsβ . builder β ActiveAdmin::FormBuilder, ActionView::Helpers::FormBuilder. template β , ( view-helper'). , , builder. - link_to, template.
Countable.js : app/assets/javascripts/inputs/countable_input .js , Countable.js div.countable-content ( JS-):
// app/assets/javascripts/inputs/countable_input.js
//= require ./countable_input/countable.min.js
const countable_initializer = function () {
$('.countable-input').each(function (i, e) {
Countable.on(e, function (counter) {
$(e).parent().find('.countable-content').html('words: ' + counter['words']);
});
});
}
$(countable_initializer);
$(document).on('turbolinks:load', countable_initializer);
app/assets/javascripts/active_admin.js:
// app/assets/javascripts/active_admin.js
// ...
//= require inputs/countable_input
β CSS- app/assets/stylesheets/active_admin.scss:
// app/assets/stylesheets/inputs/countable_input.scss
.countable-content {
float: right;
font-weight: bold;
}
// app/assets/stylesheets/active_admin.scss
// ...
@import "inputs/countable_input";
, input . :
# app/admin/articles.rb
ActiveAdmin.register Article do
form do |f|
f.inputs do
f.input(:id, as: :hello_world)
f.input(:title)
f.input(:body, as: :countable)
end
f.actions
end
end

. , input' . , .
Formtastic:
Arbre, partial', :
# app/admin/articles.rb
ActiveAdmin.register Article do
form(partial: 'form')
end
# app/views/admin/articles/_form.html.arb
active_admin_form_for resource do
inputs(:title, :body)
actions
end
, - views. , , , .
Formtastic:
Formtastic β , README, . activeadmin_addons. gem' input', .
, Formtastic Arbre , , Arbre-.
Inherited Resources β
. resource, . , gem'.
Inherited Resources β , CRUD-.
, , , . :
class ArticlesController < InheritedResources::Base
respond_to :html
respond_to :json, only: :index
actions :index, :new, :create
def update
resource.updated_by = current_user
update! { articles_path }
end
end
, .respond_to . .respond_to «», . , .clear_respond_to.
.actions CRUD- (index, show, new, edit, create, update destroy).
resource β , :
resource #=> @article
collection #=> @articles
resource_class #=> Article
, #update! β alias #update, super.
.has_scope. , Article scope :published:
class Article < ApplicationRecord
scope :published, -> { where(published: true) }
end
.has_scope:
class ArticlesController < InheritedResources::Base
has_scope :published, type: :boolean
end
.has_scope query-. scope :published, URL /articles?published=true.
README. , , , , Active Admin.
Inherited Resources:
Active Admin InheritedResources::Base, , , .
, action' :
# app/admin/articles.rb
ActiveAdmin.register Article do
actions :all, :except => [:destroy]
end
, action . , : Active Admin . .
Active Admin HTML, JSON XML ( index CSV). XML- :
# app/admin/articles.rb
ActiveAdmin.register Article do
clear_respond_to
respond_to :html, :json
respond_to :csv, only: :index
end

, undefined method 'clear_respond_to' for #<ActiveAdmin::ResourceDSL>.
, -, ActiveAdmin::ResourceDSL, . , ActiveAdmin::ResourceDSL #actions.
, , - #controller:
# app/admin/articles.rb
ActiveAdmin.register Article do
controller do
clear_respond_to
respond_to :html, :json
respond_to :csv, only: :index
end
end
, localhost:3000/admin/articles.xml . action'?
Inherited Resources:
, Article#created_by_admin. #create:
# app/admin/articles.rb
ActiveAdmin.register Article do
controller do
def create
build_resource
@article.created_by_admin = true
create!
end
end
end
, build_resource β , @article. created_by_admin create! ( super), @article.
: . Inherited Resources instance- . , , ( ).
, , XML- . , XML ? ?
! , ActiveAdmin::ResourceController:
# lib/active_admin/remove_xml_rendering_extension.rb
module ActiveAdmin
module RemoveXmlRenderingExtension
def self.included(base)
base.send(:clear_respond_to)
base.send(:respond_to, :html, :json)
base.send(:respond_to, :csv, only: :index)
end
end
end
.included , . Active Admin ActiveAdmin::ResourceController:
# config/initializers/active_admin.rb
require 'lib/active_admin/remove_xml_rendering_extension'
ActiveAdmin::ResourceController.send(
:include,
ActiveAdmin::RemoveXmlRenderingExtension
)
# ...
#include #included, ! .xml.
, , #prepend, #include #extend β , , . , .
Inherited Resources:
README. , Active Admin, , .
Ransack:
Active Admin index- , , - . Ransack.
Ransack β , SQL-, . , , . .
, (Article) (title). Ransack :
Article.ransack(title_cont: '').result
_cont β , Ransack. , SQL- . wiki.
: , , (body). Ransack :
Article.ransack(title_or_body_cont: 'active admin').result
, Ransack , . , (Comment#text):
Article.ransack(comments_text_cont: 'I hate type annotations!').result
, . . Ransack #ransack_alias. alias: comments, :
# app/models/article.rb
class Article < ActiveRecord::Base
has_many :comments
ransack_alias :comments, :comments_text_or_comments_author
end
Article.ransack(comments_cont: 'Matz').result
, Ransack , , , , Active Admin.
Ransack:
Active Admin:
# app/admin/articles.rb
ActiveAdmin.register Article do
preserve_default_filters!
filter :title_or_body_cont,
as: :string,
label: I18n.t('active_admin.filters.title_or_body_cont')
filter :comments,
as: :string
end

, . #preserve_default_filters!, .
Ransack: scope-
Ransack . , ransackable_attributes, ransackable_associations ransackable_scopes. ( , Active Admin ), ransackable_scopes.
, ransackable_scopes scope'. , scope ( ), .ransackable_scopes.
, scope:
# app/models/article.rb
class Article < ActiveRecord::Base
has_many :comments
scope :comments_count_gt, (lambda do |comments_count|
joins(:comments)
.group('articles.id')
.having('count(comments.id) > ?', comments_count)
end)
def self.ransackable_scopes(auth_object = nil)
[:comments_count_gt]
end
end
auth_object: , . , current_user, Active Admin .
scope .ransackable_scopes, Active Admin:
# app/admin/articles.rb
ActiveAdmin.register Article do
filter :comments_count_gt,
as: :number,
label: I18n.t('active_admin.filters.comments_count_gt')

: β , , :

«» , Ransack. , sanitize_custom_scope_booleans:
# /config/initializers/ransack.rb
Ransack.configure do |config|
config.sanitize_custom_scope_booleans = false
end
, , 1 , scope'.
Ransack:
, Active Admin . README wiki, , , view- .
, , Ransackers β , Arel ( ActiveRecord, SQL-).
, Active Admin , , - . Active Admin frontend- .
Active Admin, , , .
activeadmin_addons, , , Active Admin. , , ( ).