Antonio Stradivari (Italian pronunciation: [anˈtɔːnjo stradiˈvaːri]; 1644 – 18 December 1737) was an Italian luthier and a crafter of string instruments such as violins, cellos, guitars, violas, and harps. Stradivari is generally considered the most significant and greatest artisan in this field.
Like Antonio Stradivari built great musical instruments, this Ruby Gem is a collection of great tools you can play in your Ruby Web App, and produce the best symphonies ever.
Started off as a replacement of the Active Admin table and filter generators DSLs, during the development of an enterprise product that used to rely on Active Admin. It then stood up, started walking, and grew up on its own. 😄
This Gem combines HAML and Bootstrap 3 to provide you easy generators for:
- HTML Tables
- CSV Tables
- XLS Tables
- Tabbed layouts
- Definition Lists
- Filter forms
The filter form generator includes also all controller code boilerplate to parse submitted form parameters, and model code to define callable search scopes and safely process the search parameters, thanks to the useful help from Ransack and PgSearch.
Whoa, what a lot of Gems this glues together! 😄
Add to your Gemfile
gem 'stradivari', github: 'ifad/stradivari'
Add to your app/assets/javascripts/application.js
//= require stradivari
Add to your app/assets/stylesheets/application.css
*= require stradivari
Add to your app/controllers/application_controller.rb
include Stradivari::Controller
Add to your config/initializers/stradivari.rb
module Stradivari
end
Given a typical app/controllers/foo_controller.rb
:
class FooController < ApplicationController
def index
@foos = Foo.order_by_awesomeness
end
end
You'd have in your app/views/foo/index.html.haml
:
= table_for @foos [header_visible: (true|false), body_visible: (true|false), footer_visible: (true|false), downloadable: (:xlsx|:csv)] do
- row do |attributes,foo|
attributes[:class] << " foo_#{foo.foo_type}"
attributes['data-foo-id'] = foo.id
- column :id
- column :awesomeness, presence: true
- column :something_special do |foo|
- if foo.really_special?
%strong This is a special Foo!
- else
Nothing to foo here.
- column :lazy_to_translate, title: "Here we go"
This will generate the table head, body and foot markup, cycling over
the @foos
AR collection. Column headings take attribute names using
t()
, fitting nicely in Rails' I18n for Active Record.
The I18n'ed title can be overriden passing the :title
option to the
column
generator, as shown above.
If you wish to set custom attributes on the table row itself, the row
block will pass you a hash of the row attributes and the current object
(@foos[n]
). You may add or alter the hash contents and these will be
set on the row element. A good use case for this would be if you wanted
to bind a click handler to the row and needed to record the foo.id
for
an action specific to that object.
If there is no data, the table will be replaced with a div saying
There is no data.
You can change this in one of two ways - specify a
different message as the no_data option:
= table_for @foos, no_data: 'There is nothing to see here'
or, provide a no_data
block which will be rendered in place:
= table_for @foos do
- no_data do
There are no entries.
%a.btn.btn-default{ href:'#' } Create
= csv_for @foos do
- column :id
- column :boolean_field
- column :name, presence: true, html: {:class => "custom th class"}
- column :text_field
- column :string_field, title: "Here we go"
- column :created_at
= xlsx_for @foos do
- column :id
- column :boolean_field
- column :name, presence: true, html: {:class => "custom th class"}
- column :text_field
- column :string_field, title: "Here we go"
- column :created_at
You can alter the XLS heading and body styles in the header:
= xlsx_for @foos, heading_style: { font_name: 'Verdana', sz: 15, bg_color: 'c6c6c6' } do
- column :id
- column text_field
You can also alter the style of each column:
= xlsx_for @foos do
- column :id, style: { size: 8, alignment: { horizontal: :right } }
Each style is a hash of Axlsx styling parameters. Here is a subset of the Axslx style parameters that are probably most useful to you:
Parameter | Example value | Description |
---|---|---|
:sz | 10 | Font size (point size) |
:font_name | 'Verdana' | Font to use |
:bg_color | 'c6c6c6c6' | Background colour of cell (RGB 6-hex code) |
:fg_color | '00000000' | Foreground colour of cell (RGB 6-hex code) |
:alignment | { horizontal: :center } | Centred cell alignment |
:alignment | { vertical: :center } | Vertically centred cell alignment |
:alignment | { wrap_text: true } | Wrap overflowing text |
:b | true | Bold text |
:i | true | Italic text |
:u | :double | Underline |
:strike | true | Strike-through text |
:border | Axlsx::STYLE_THIN_BORDER | Add border to cells |
= tabs_for @foos, flavor: (:tabs|:pills|:stacked), counters: (true|false) do |foos|
- blank do
.warning Nothing found
- tab "Tab label", "tab_div_id", foos.scope, present: (false|true|:force) do
= render(partial: 'foos', locals: { foos: foos.scope })
When a tab scope is empty, Stradivari will by default not show the tab. You can force the tab to show
with the present: true
option. You can also force the rendering of the tab's content with the option present: :force
.
The blank block is rendered in two cases. The first, if the tabs_for
scope is empty and there are no
tabs with option present: true
. The second, if a tab has this option, but its personal scope is
empty.
Tabs can be conditionally rendered using the if:
option and passing a lambda
to it. If it evaluates
to a falsish value, the tab is hidden. The block is executed in the view context.
Tab content can be optionally loaded from a remote URL by passing the url:
option. If you need to use
Rails' route helpers, you have to reach to them through the view
object, e.g.:
tabs_for @foos do
- tab :bar, url: view.bars_path do
.loading Loading...
When retrieving data from a remote URL, Stradivari will emit events at key lifecycle points:
Event Name | Extra Parameters | When |
---|---|---|
stradivari:tab:loading | none | Before the AJAX send |
stradivari:tab:loaded | none | After a successful response is received |
stradivari:tab:failed | none | After a failed response is received |
These events will be emitted on the tab link that was clicked to initiate the remote load. This is useful if there are actions that must be performed before or after the remote data is loaded. For example, here is a simple example enabling pagination of a table loaded in the remote request:
The HAML:
= tabs_for @foos do |foos|
- tab "Tab label", "tab_div_id", foos.scope, present: true, url: '/foos' do
.loading Loading...
The partial /foos
:
= table_for @foos do |foos|
- column :bar
- column :baz
= paginate @foos, remote: true
And finally, some javascript to bind the paginators correctly when the partial loads:
function bindPaginators(tab_pane) {
$('nav.pagination a',tab_pane).on('ajax:success', function(e,data) {
$(tab_pane).html(data);
bindPaginators(tab_pane);
});
$('[data-toggle=tab']).on('stradivari:tab:loaded', function(evt) {
var tab_id = $(this).attr('href'); // href of the <a> element is "#tab_div_id"
bindPaginators( $(tab_id) ); // $('#tab_div_id')
});
The tabbed examples above place some constraints on how you lay out the tabs. It
groups both the nav and the content under the same container. In some cases, you
may wish to separatethe nav from the content. The tab_navs_for
and
tab_content_for
helper methods allow this:
.row
.col-xs-3
= tab_navs_for @people, flavor: :stacked do |people|
- tab 'Option 1', 'option_1_id', people.foos, active: true
- tab 'Option 2', 'option_2_id', people.bars
.col-xs-3
= tab_content_for @people do |people|
- tab_content 'option_1_id', people.foos, active: true do |scope|
This is content for option 1
- tab_content 'option_2', people.bars do |scope|
This is content for option 2
tab_navs_for
takes the same options as tabs_for
, as does its tab
method,
which does not take a block, however. Instead, the block is present in the tab
option of tab_content_for
. It is important both that the ID fields match
between the two tab
methods and that the scopes do as well, otherwise there
may be display issues. You also have to set active: true
on the content
tab as well as the nav tab.
You can specify which tabs to automatically open at the page loading adding their id to the parameter stradi_tabs[]=
to the page URI. Multiple tabs can be specified.
es: http://mysite.com?stradi_tabs[]=bar&stradi_tabs[]=baz
Stradivari uses Ransack to perform search queries. Ransack provides a mapper
that transforms form parameters into an ActiveRecord Relation query, using
predicates such as id_eq
or name_cont
. For details about Ransack's
predicates syntax, please have a look here.
To compose the search query for a model, Stradivari extends ActiveRecord with
a stradivari_filter
method that is available in your models.
The user-provided search query is available in your controller through the
stradivari_filter_options
method.
Stradivari provides a set of filter builders that you can (should) use to create your filter forms.
TODO: Stradivari is based on a (as of 04/2015) legacy version of Ransack, extending it to add functions that are now available upstream - so this should be fixed.
Given this data model:
# db/schema.rb
create_table :posts do |t|
t.integer :author_id
t.string :title
t.text :body
t.timestamps
end
By default, with no changes to the model, you can already search on all model fields using Ransack features. Supposing you also have a specific scope you want to search on, you can define it in your model:
# app/models/post.rb
class Post < ActiveRecord::Base
belongs_to :author
stradivari_scope :by_author_name do |user_provided_input|
joins(:author).where("authors.name LIKE ?", user_provided_input)
end
end
Post.by_author_name
behaves like every other ActiveRecord scope, and it is
also marked accessible through user-provided search queries. Always be
careful with user provided input. Stradivari or ActiveRecord can't protect
you from shooting yourself in the foot if you want to. Follow the Robustness
principle: Be conservative in what you do, be liberal in what you accept.
Let's then define a posts list table with some filters.
/ app/views/posts/index.html.haml
= table_for @posts do
- column :author do |post|
= post.author.name
- column :title
- column :body do |post|
= truncate(post.body, length: 140)
- column :created_at
- column :updated_at
= filter_for Post do
- selection :author_id, collection: Author.all
- search :title
- search :body
- date_range :created_at
- search :by_author_name
Behind the scenes, Stradivari builds an appropriate Ransack predicate for each builder, and builds the search form. Each builder has its own options and tweaks, see the source for now for more information.
In this case, the title
and body
attribute will generate a title_cont
Ransack predicate that will be translated to a SQL LIKE
operator, while the
by_author_name
field will pass along the user provided input to the
corresponding stradivari_scope
method defined on the Post
model.
Start by adding Stradivari::Controller
to your ApplicationController
# app/controllers/application_controller.rb
module ApplicationController
include Stradivari::Controller
end
Then, in your PostsController
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def index
@posts = stradivari_filter(Post)
end
end
@posts
is an ActiveRecord::Relation
that can be further manipulated,
paginated, etc.
stradivari_filter
is both the name of a controller helper, that accepts
an ActiveRecord object (either a model or a Relation) and that invokes the
search machinery, using as parameters the hash representation of the q
query string parameter.
Stradivari defines also a .stradivari_filter
method on your models, that
behaves like a standard Active Record scope. It accepts and hash, that are
the search parameters. You can pass your own hash, or you can get it from
the query string using stradivari_filter_options
.
The following two calls are identical:
stradivari_filter(Post)
Post.stradivari_filter(stradivari_filter_options)
The latter is useful if you want to massage the filter options yourself, or if you want to have a pure scopes chain - matter of taste.
The return value of .stradivari_filter_options
is an Hash and can be
manipulated as you see fit.
To enable sorting support, currently Stradivari has a constraint on one model per controller. If you manage multiple models with the same controller (and you shouldn't ;-) then you can use sorting functions only on one of them.
The model can be defined in your controller by overriding the
sorting_object_class
method. By default, Stradivari will try to infer the
model name from the controller name, e.g. a PostsController will try to find a
Post model, but if this fail then it's up to you to define it. Example:
# app/controllers/posts_controller.rb
class PostsController
# [ ... ]
protected
def sorting_object_class
Post
end
end
Default sort column and direction are, respectively, id
and ASC
. To
override them, you can override the default_sort_column
and
default_sort_direction
methods.
If we would want to display most recent posts first, for example, we could use:
# app/controllers/posts_controller.rb
class PostsController
def default_sort_column
'created_at'
end
def default_sort_direction
'DESC'
end
end
Alternatively, you can define sort parameters using the stradivari_filter
DSL method:
stradivari_filter Post,
default_sorting: 'created_at',
default_direction: 'DESC'
Ransack provides basic sorting options for column names. The title
column in
the above example can be made sortable in the rendered table by adding the
sortable: true
option:
= table_for @posts do
- column :title, sortable: true
For more complex sorting schemes, e.g. if we want to sort on the associated author name, we have to define the sorting semantics in the model first.
Stradivari looks up a scope
in the model named after the column with a
sort_by
prefix and asc
or desc
as suffix. In this example, this would
be:
class Post < ActiveRecord::Base
scope :sort_by_author_asc, -> { joins(:author).order('authors.name ASC') }
scope :sort_by_author_desc, -> { joins(:author).order('authors.name DESC') }
end
= table_for @posts do
- column :author, sortable: true do |post|
= post.author.name
You can pass to the sortable
option the scope name you want to use, e.g. if
you called the above scope sort_by_person_asc
, you would have needed to
specify sortable: person
. This is useful when reusing these scopes for
multiple purposes.
TODO
TODO
- prepend(class: "options") do
%p This is a prepended text
= details_for @foos[1] do
- field :id
- field :boolean_field, title: "custom title"
- field :name, presence: true, html: {:class => "custom dd class"}
- field :text_field
- field :string_field, title: "Here we go"
- field :created_at
If you want to use the Stradivari Autocomplete, please add twitter-typeahead-rails to your project
The stradivari Autocompleter uses both the detached form and the form filters
It will use the keys in the filters to generate a list of autocomplete terms in the detached form
When you select a term from the autocomplete list, the corresponding filter is automagically selected in the filter list
To enable it, put the autocomplete:true option in the detached form search field
- search :matching, title: 'Search', class: "focus", autocomplete: true
The class "focus" is used to give default focus to the search field
Then, on the checkbox fields in the filter, use the autocomplete: true option to tell to the Stradivari to grab that list and use it in the Autocompleter
- checkbox :foo, collection: Foo.foos, priority: :low, title: "Foo", autocomplete: true
MIT
© IFAD 2014-2015
This software is Made in Italy 🇮🇹 😄.
Organizations that allow you to write Open Source code are just awesome. It's actually a shame that it's not mandatory for public service engineers to share their code as Open Source - as this should be natural and embodied in any public service. So, for now, we try to lead this path, delighted to see remarkable examples such as NASA and ESA who deliver code and data for free.
Information and knowledge wants to be free, and through it we can make a huge impact in making the world a better place.
-- vjt Mon Jun 9 20:21:42 CEST 2014