MagicResource
MagicResource is a Rails plugin which introduces the resource-based ideology for WEB applications development.
Getting Started
Add to your Gemfile:
gem 'magic-resource', git: '[email protected]:anadea/magic-resource.git', tag: 'v0.0.3'
Run the bundle command to install it.
Run the generator:
rails generate magic_resource:install
Include to controller, typically to ApplicationController:
class ApplicationController < ActionController::Base
include MagicResource::Controller
Resource abstraction
It is a quite naturally when we affecting to something, we always affecting to specific object or to the scope of alike objects. Just like the calling the instance method or class method. Even when we want to do something like turn off all electrical devices in the house
, it's better and more natural to trigger the turn_off_the_devices
method on House.belongs_to(me)
object, rather than to implement inplace logic which will find all the devices, and do something with them.
Lets think like that when building the RoR application. Any time when the browser makes the request to the server, it affects the specific object, or to the scope of alike object. In RoR terms, each call to the server affects to specific Model instance, or to the scope of such Model.
Each Model has resource_name
, typically that is the model's class name, underscored and pluralized. For Property model that will be :properties
.
Then, lets add the resource_context
when we affecting to the Model. It is just a Symbol identity. It should have just a logical sense, but not tied to any terms from the application like user roles, application areas, menu items etc.
For example, lets say that admin
users should have an access to all crud actions for the Property model, and guest
users should have acccess to show and filterable index actions only. It's better to say that Property model can be affected under :crud
and :search
contexts, rather than :admin
and :guest
ones. Then, if admin
role will be renamed to superadmin
, if crud actions will be available for manager
as well etc - :crud
context name will not loose the sense, rather than :admin
context.
The affecting to the Model is always has some context, it can't be blank. Lets say that default context for any Model is :crud
.
Each resource context means the separated controller and the special folder for templates. To finish the picture, lets add the single localization file, then say:
Resource is the container-like abstraction, which includes the Model, the contexts to be applied when affecting the Model, the scope of one-per-context Controllers, the scope of templates to be rendered with the Model, and single localization file.
Sources structure
Controllers
Controllers should be placed to app/controllers/#{resource_name}/#{resource_context}_controller.rb
and named accordingly. For Property model and :crud context it should be placed to app/controllers/properties/crud_controller.rb
and contains something like:
module Properties
class CrudController < ApplicationController
end
end
Models
Nothing specific, place and name them as always.
Views
There are the general folder for resource templates, named app/views/#{resource_name}/
. In the root of this folter should be placed the templates which you normally want to name shared
. In folters like app/views/#{resource_name}/#{resource_context}/
should be placed the context-specific templates.
The typical scope of templates for Property model under :crud context should looks like:
- app/views/properties/crud/edit.html.haml
- app/views/properties/crud/index.html.haml
- app/views/properties/crud/new.html.haml
- app/views/properties/crud/show.html.haml
- app/views/properties/_form_attrs.html.haml
- app/views/properties/_list_item.html.haml
- app/views/properties/_form_search_attrs.html.haml
- app/views/properties/_view.html.haml
Locales
The single locale file should be placed to config/locales/en/#{resource_name}.yml
. For Property model it should be placed to config/locales/en/properties.yml
and contains something like:
en:
properties:
button_edit: 'Edit Property'
Magic
When we speaking about the magic
, we mean some smart and powerful instruments. Just like:
redirect_to user
But, uncontrolled usage of such things is not good:
redirect_to my_favorite_variable
Where we go? Lets guess that to users#show
action, and we going to rename the action or controller - how we can find all places in the sources which redirects to users#show
in this way?
Lets define, that the magic
is good when it:
- invariantly predictable
- searchable in the sources
- can be banned partially or totally in whole project
General rules
Lets separate the controller's action and templates to two group - collection and member, dependent when we working with the single Model instance of with Model's scope.
Set the resource object
In collection
controller actions call resources=
method:
def index
self.resources = Property.all
end
In member
controller actions call resource=
method:
def show
self.resource = Property.find(params[:id])
end
Controller will set the resource context automatically by controller's class name.
Get the resource object
In 'collection' controller actions and templates call 'resources' method:
resources.size
In 'member' controller actions and templates call 'resource' method:
resource.full_name
Magic patterns
The full pattern looks like #{resource_name}::#{resource_context}#{separator}#{identifier}
. Dependent on the magic method, it changes the separator and identifier sense:
# the way to controller's action
r.path_to 'users::search#show'
# the template path
r.render 'users::search/view'
# the translation key
r.t 'users::search.button_search'
When the context name is skipped, it means the inheritance of current context for current object, or apllying the default context to other object:
r.path_to 'users#show' # inherit the current context
r(resource).path_to 'users#show' # the same as
r(resource).path_to 'users::crud#show'
MagicResource will check is the object's type matching to specified resource name:
r(Property).path_to 'users#show' # raise exception
For current object is possible to skip the resource name:
r.path_to '#index'
For current object is possible to specify just identifier:
r.path_to :show
Access to the magic
Typically you want to inherit the current resource object and resource_context for the magic. To do this, in controller or template call r
method. You are able to use short patterns in magic calls, as well as combined and full patterns:
r.path_to :show
r.path_to 'properties#show'
r.path_to '::search#show'
r.path_to 'properties::search#show'
Sometimes, you want to make a magic with some other object
. To do this, in controller or template call r
method with object as parameter. Once the other object
is specified, you are forced to use only full patterns in magic calls:
r(resource.user).path_to 'users#show'
r(resource.user).path_to 'users::search#show'
When you pass the current resource as parameter, it anyway means that the other object
is specified - you loose the current context and must use the full patterns in magic calls:
r(resource).path_to 'properties#show'
r(resource).path_to 'properties::search#show'
When you don't have the specific object, pass the class as parameter. It again means the other object
rules:
r(User).path_to 'users#index'
You can pass the form object as parameter, in this case the object inside it will be a the other object
:
= r.form_for :update do |f|
= f.simple_fields_for :user do |uf|
= r(ff).render 'users/form_attrs', f: ff
Magic methods
r.context?
Check is the resource context is in the args list:
r.context?(:crud, :search) # true
r.t
The call looks like:
r.t :button_edit
r(user).t 'users::crud.button_edit', {interpolation_option: 'foo'}
It will search the translation in next order:
- users.crud.button_edit
- users.button_edit
- resources.crud.button_edit
- resources.button_edit
The method will not accept dotted keys like .my.dotted.key
. But why do you want it actually? '.my_dotted_key' has the same count of chars, and it's better to separate the locales section by white space rather than to produce the one more subtree for nothing.
r.render
The call looks like:
r.render :form_attrs, f: f
r(user).render 'users::crud/form_attrs', f: f
It will search the template in next order:
- users/crud/_form_attrs
- users/_form_attrs
- resources/crud/_form_attrs
- resources/_form_attrs
The method will apply the specified object and context as current
inside the target template.
The method will not accept the subfolder /form/attrs
templates just like r.t method.
The accepts the form object as second param, and passes it as f
local parameter:
= r.form_for :update do |f|
= r.render :form_attrs, f
It's good to render the other resource template, rather than mass calling to it's attributes or magic methods:
that is bad:
= r(resource.user).link_to 'users#show', resource.user.name
= resource.user.role
= resource.user.last_logged_in
that is good:
= r(resource.user).render 'users#view'
and then in 'app/views/users/_view'
= r.link_to :show, resource.name
= resource.role
= resource.last_logged_in
In general, when you see in your partial the massive definition of full magic pattern - it's time to move this part to other resource's partial.
r.render_collection
The call looks like:
r.render_collection :list_item
r(users).render_collection 'users::crud/list_item'
Just calls r.render for collection items, with current context inheritance in first sample.
r.render_collection_build
The call looks like:
r.render_collection_build :new_form
r(users).render_collection_build 'users::crud/new_form'
Calls the r.render for 'object.soft_build' object (look at stokarenko/association-soft-build
gem), with current context inheritance in first sample.
r.path_to
The call looks like:
r.path_to :show
r(user).path_to 'users::crud#show'
Returns the URL to specific controller/action.
To pass the URL parameters call it in hashable way:
r.path_to show: {foo: :bar}
r(user).path_to 'users::crud#show' => {foo: :bar}
r.redirect_to
Can be used only in controller.
The call looks like:
r.redirect_to :show
r(user).redirect_to 'users::crud#show'
Redirects to specific controller/action.
To pass the URL parameters call it in hashable way:
r.redirect_to show: {foo: :bar}
r(user).redirect_to 'users::crud#show' => {foo: :bar}
r.link_to
The call looks like:
r.link_to :edit
r(user).link_to 'users::crud#edit', 'Special edit button'
The method will build the link html tag to specific controller/action.
It will accept the URL parameters in hashable way, just like r.path_to
do.
It will automatically apply the HTTP verb for the link.
It will use :button_#{action}
as the label param by default, then...
It will apply r.t
method if the label param is a Symbol.
It will use string label param for label as it is.
It will automatically set :confirm
param to true
for :destroy
actions, then
It will set :confirm
param to :confirm_destroy
if it is true, then
It will apply r.t method for confirmation message if :confirm
param if is a Symbol.
It will apply :confirm
string param as confirmation message as it is.
It will accept the block to be used as label parameter.
r.link_to_if
The call looks like:
r.link_to_if true, :edit
r(user).link_to_if false, 'users::crud#edit'
Works just like regular link_to_if
but uses r.link_to on success.
It will accept the URL parameters in hashable way, just like r.path_to
do.
It will accept the block to be used as label parameter.
r.link_to_unless
The call looks like:
r.link_to_unless false, :edit
r(user).link_to_unless true, 'users::crud#edit'
Works just like regular link_to_unless
but uses r.link_to on success.
It will accept the URL parameters in hashable way, just like r.path_to
do.
It will accept the block to be used as label parameter.
r.form_for
The call looks like:
r.form_for :update do |f|
end
r(user).form_for 'users::crud#update' do |f|
end
Build the form to specific controller/action.
It will accept the URL parameters in hashable way, just like r.path_to
do.
It automatically set the HTTP verb.
It uses the default :simple
form helper prefix, so the form will be built with simple_form_for
helper.
It takes the second param as the form helper prefix to be used.
It supports Ransack, just pass Ransack::Search object as second param.
It takes the logical #save
action, which will be transformed to #create
or #update
actions dependent on the object's persistency status.
r.content_for
This method is too much magical, and disabled by default. Activate it in configuration if you really need it.
The usage looks like:
- r(resource.user).content_for 'users:header_class', 'strong'
- r(resource.user).content_for 'users:header_prefix' do
some super prefix
= r(resource.user).render 'users::crud/view'
Then, in app/views/users/crud/_view
template:
.header{class: r.content(:header_class) || :default}
= r.content(:header_prefix)
the body
The same can be done by locals passing to render, but sometimes we need to forward such locals deeply and deeply to other render calls. Looks like r.content_for
method is a better solution for that...
Configuration
MagicResource generator will prepare the config/initializers/magic-resource.rb
config file.
Please take a look on it:
MagicResource.setup do |config|
## Uncoment the line to change the context to be used as default.
## Default value is `:crud`.
# config.default_context = :manage
## Uncoment the line to change default form helper prefix.
## Default value is `:simple`.
# config.default_form_type = :default
## Change the method to adjust the level or resource magic.
## possible methods are `no_magic`, `try_magic?` and `full_magic!`
## Thechnically such methods just setting all following parameters to
## `:by_exception`, `:by_warning` and `false` respectively.
config.no_magic
############################
#### For all next parameters
#### the correct values are: `[false, :by_exception, :by_warning]`.
## Will require to specify the resource name in helper's pattern when resource is "other".
## Can be useful for fast development or experiments. But typically it's better
## to switch it off later and define the resource names everywhere,
## because the magic becames to be not predictable and searchable.
# config.force_resource_name_definition = :by_exception
## Will require the correct resource assignation in controller
## Means the situation when inside Users::CrudController you trying to do
## self.resource = Property.first
# config.assert_resource_name_in_controller = :by_exception
## Will disable content_for helper.
## It is too much magical...
## But in some cases it is the better than other solutions.
## Try to avoid it.
## Activate it if you really need it.
# config.disable_content_for_helper = :by_exception
end
Changes
v0.0.3
- Added the possibility to pass URL params to routable helpers.
- Not ActiveModel-like classes can be a resource.
r
method will extract theobject
and use it as resource when form object received.r.render
accepts the form object as second param, and passes it asf
local parameter.- Added
r.link_to_unless
helper. r.link_to
-like helpers accept the block as label.- Added
r.redirect_to
controller helper.
TODO
- Implement resourcable Coccon, and make it less painful.
- Leave r object in controller as it was set there (?).
- Apply the passing of translation interpolation params as Hash (?).
- Separate the helpers for controller and view.
- Resource controllers.
- Automatic preload in controllers, bases on statistic of association calls in templates.
- inverse_of as default in ActiveModel (?).
- Resource tests (?).
- ActiveRecord translations.
- cancancan integration.
- Single-file navigation for whole application (?).
- Routes definition helper.
- Implement resources default templates, and configuration to turn them off.