Skip to content

Instantly share code, notes, and snippets.

@jmbejar
Last active August 29, 2016 19:21
Show Gist options
  • Save jmbejar/620502af4471f993f45645da14dec0f4 to your computer and use it in GitHub Desktop.
Save jmbejar/620502af4471f993f45645da14dec0f4 to your computer and use it in GitHub Desktop.
Account Form - Rails
  • Accounts controller
  • Vistas:
    • new
    • _modal y _modal_content
    • _form
  • currencies_options helper
  • Vistas con respuesta js desde el servidor:
    • create.js.erb
    • update.js.erb
    • error.js.erb
  • Assets/Javascript
    • money_form js
    • account_form js
    • application.js (diff)
  • Tests
  • JS Tests

Cosas que no estoy poniendo (pero habría que comentar):

  • Botones de agregar y editar cuentas
  • Resources :accounts en las rutas
  • Template accounts/edit es exactamente igual al accounts/new
  • account-create.js.erb es exactamente igual al account-update.js.erb
  • navigation.js
class AccountsController < ApplicationController
before_action :set_accounts, only: [:index, :new, :edit]
before_action :set_account, only: [:edit, :update]
# GET /accounts
def index
end
# GET /accounts/1/new
def new
@account = Account.build
end
# GET /accounts/1/edit
def edit
end
# POST /accounts
def create
@account = Account.build(account_params)
respond_to do |format|
if @account.save
format.js {}
else
format.js { render :error }
end
end
end
# PATCH/PUT /accounts/1
def update
respond_to do |format|
if @account.update(account_params)
format.js {}
else
format.js { render :error }
end
end
end
private
def set_accounts
@accounts = Account.order(id: :desc)
end
def set_account
@account = Account.find(params[:id])
end
def account_params
params.require(:account).permit(:kind, :name, :balance, :currency, :credit_limit)
end
end
<%= render template: 'accounts/index' %>
<%= render 'modal' %>
<div class="modal fade" id="accountFormModal" role="dialog">
<%= render 'modal_content' %>
</div>
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title"><%= modal_title(@account) %></h4>
</div>
<%= render 'form' %>
</div>
</div>
<%= form_for(@account, remote: true) do |f| %>
<div class="modal-body">
<% @account.errors.full_messages.each do |message| %>
<div class="alert alert-danger"><%= message %></div>
<% end %>
<div class="form-group">
<%= f.label :kind, 'Account type' %>
<div class="input-group">
<div class="btn-group btn-group-justified" data-toggle="buttons">
<%= f.collection_radio_buttons :kind,
Account.kinds.keys,
:to_s,
:titleize,
{},
class: 'btn-primary' do |b| %>
<%= radio_buttons_options(b, @account, :kind) %>
<% end %>
</div>
</div>
</div>
<div class="form-group">
<%= f.label :name %>
<div class="input-group">
<div class="input-group-addon"><i class="fa fa-user fa--wide"></i></div>
<%= f.text_field :name, class: 'form-control', required: true %>
</div>
</div>
<div class="form-group">
<%= f.label :balance %>
<div class="input-group">
<div class="row">
<div class="col-md-6 col-xs-6">
<div class="input-group">
<div class="input-group-addon"><i class="fa fa-usd fa--wide"></i></div>
<%= f.number_field :balance, class: 'form-control', required: true, step: 0.01 %>
</div>
</div>
<div class="col-md-6 col-xs-6">
<%= f.label :currency, class: 'sr-only' %>
<%= f.select :currency, currencies_options, {},
class: 'form-control selectpicker', data: { style: 'dropdown-select__button' } %>
</div>
</div>
</div>
</div>
<div class="form-group">
<%= f.label :credit_limit %>
<div class="input-group">
<div class="input-group-addon"><i class="fa fa-credit-card fa--wide"></i></div>
<%= f.number_field :credit_limit, class: 'form-control', required: true, step: 0.01, min: 0.01 %>
</div>
</div>
</div>
<div class="modal-footer">
<%= f.button 'Cancel', type: :reset, class: 'btn btn-default', data: { dismiss: 'modal' } %>
<%= f.submit class: 'btn btn-primary' %>
</div>
<% end %>
module ApplicationHelper
def modal_title(model)
"#{model.persisted? ? 'Edit' : 'New'} #{ui_model_name(model)}"
end
def currencies_options
[
['Dollars', 'USD'],
['Uruguayan Pesos', 'UYU'],
['Argentine Pesos', 'ARS']
]
end
def radio_buttons_options(builder, object, attribute, options = {})
label_class = "btn btn-primary"
if object.send(attribute) == builder.value
label_class += " active"
end
id = "#{attribute}_#{builder.value}"
builder.label({ class: label_class, for: id }.merge!(options)) do
builder.radio_button(id: id) + builder.text
end
end
# ...
end
AccountForm.updateHTML('<%=j render 'modal_content' %>');
(function() {
'use strict';
window.AccountForm = function() {
MoneyForm.call(this, {
modalSelector: '#accountFormModal',
indexPath: '/accounts'
});
};
$.extend(AccountForm.prototype, MoneyForm.prototype, {
updateHTML: function(html) {
MoneyForm.prototype.updateHTML.call(this, html);
this.updateCreditCardField();
},
updateCreditCardField: function() {
var isCreditCardAccount = this.creditCardButton().is(':checked');
this.creditCardLimitFieldGroup().toggle(isCreditCardAccount);
this.creditCardLimitField().prop('disabled', !isCreditCardAccount);
},
creditCardButton: function() {
return this.modalElement().find('#kind_credit_card');
},
creditCardLimitFieldGroup: function() {
return this.creditCardLimitField().closest('.form-group');
},
creditCardLimitField: function() {
return this.modalElement().find('#account_credit_limit');
},
// Helper functions
_initializeModal: function() {
MoneyForm.prototype._initializeModal.call(this);
this.updateCreditCardField();
},
_bindEvents: function() {
MoneyForm.prototype._bindEvents.call(this);
$(document)
.on('change', '#accountFormModal input[type=radio][id^=kind_]', this.updateCreditCardField.bind(this));
}
});
}());
(function() {
'use strict';
window.MoneyForm = function(options) {
this.modalSelector = options.modalSelector;
this.indexPath = options.indexPath;
this._bindEvents();
this._initializeModal();
};
MoneyForm.prototype = {
updateHTML: function(html) {
this.modalElement().
find('.modal-dialog').
replaceWith(html);
$('.selectpicker').selectpicker();
},
show: function() {
this.modalElement().modal('show');
},
hide: function() {
this.modalElement().modal('hide');
},
modalElement: function() {
return $(this.modalSelector);
},
// Helper functions
_initializeModal: function() {
if (this.modalElement().length > 0) {
this.show();
}
},
_onModalHidden: function() {
this._navigateToIndex();
},
_navigateToIndex: function() {
Turbolinks.visit(this.indexPath);
},
_resetModalForCache: function() {
// Hide modal immediatelly, do not run neither animation nor callbacks
this.modalElement().removeClass('fade');
this.hide();
},
_bindEvents: function() {
$(document)
.on('hidden.bs.modal', this.modalSelector, this._onModalHidden.bind(this))
.on('turbolinks:load', this._initializeModal.bind(this))
.on('turbolinks:before-cache', this._resetModalForCache.bind(this));
}
};
}());
(function() {
'use strict';
window.Navigation = function() {};
Navigation.prototype = {
// Change the url that is shown in the browser URL bar, without reloading the page,
// in a way that is compatible with Turbolinks history
updateBrowserURL: function(path) {
Turbolinks
.controller
.pushHistoryWithLocationAndRestorationIdentifier(path, Turbolinks.uuid());
},
};
}());
(function() {
'use strict';
$(document).on('ready', function() {
window.AccountForm = new AccountForm();
// ...
});
$(document).on('turbolinks:load', function() {
$('.selectpicker').selectpicker('show');
});
$(document).on('turbolinks:before-cache', function() {
$('.selectpicker').selectpicker('hide');
});
}());
require "integration_test_helper"
class CreateAccountTest < ActionDispatch::IntegrationTest
test 'create a new account' do
visit accounts_path
click_link 'New Account'
click_toggle_button 'Cash', from: 'Account type'
fill_in 'Name', with: 'My account'
fill_in 'Balance', with: '300'
select_picker 'Uruguayan Pesos', from: 'Currency'
click_button 'Create Account'
assert_selector 'i.fa-money'
assert_content 'My account UYU 300.00'
end
test 'create a new credit card account' do
visit accounts_path
click_link 'New Account'
fill_in 'Name', with: 'My credit card account'
fill_in 'Balance', with: '300'
assert has_no_field?('Credit limit')
click_toggle_button 'Credit Card', from: 'Account type'
fill_in 'Credit limit', with: '100'
click_button 'Create Account'
assert_selector 'i.fa-credit-card'
assert_content 'My credit card account USD 300.00'
end
end
MagicLamp.define do
fixture(controller: AccountsController) do
@account = Accounts.first
render partial: 'modal'
end
end
module('AccountForm', {
beforeEach: function() {
MagicLamp.load('accounts/modal');
// Disable jQuery animations (e.g. fade())
$.fx.off = true;
},
afterEach: function() {
MagicLamp.clean();
}
});
test('Show credit card limit field if this kind is selected', function(assert) {
var modal = AccountForm.modalElement(),
creditCardButton = modal.find('#kind_credit_card'),
creditCardLimitField = modal.find('#account_credit_limit');
AccountForm.creditCardLimitFieldGroup().hide();
creditCardButton.trigger("click");
assert.ok(creditCardLimitField.is(':visible'));
});
test('Hide credit card limit field if bank kind is selected', function(assert) {
var modal = AccountForm.modalElement(),
bankButton = modal.find('#kind_bank'),
creditCardLimitField = modal.find('#account_credit_limit');
AccountForm.creditCardLimitFieldGroup().show();
bankButton.trigger('click');
assert.ok(creditCardLimitField.is(':hidden'));
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment