AJAX_ify StockArticle manipulation; Introduce publish/subscribe pattern for DOM updates

This commit is contained in:
Julius Rapp 2013-12-07 11:20:03 +01:00
parent e99752e483
commit dd08e277c7
22 changed files with 313 additions and 107 deletions

View file

@ -5,29 +5,61 @@ class StockitController < ApplicationController
order('suppliers.name, article_categories.name, articles.name') order('suppliers.name, article_categories.name, articles.name')
end end
def index_on_stock_article_create # See publish/subscribe design pattern in /doc.
@stock_article = StockArticle.find(params[:id])
render :layout => false
end
def index_on_stock_article_update # See publish/subscribe design pattern in /doc.
@stock_article = StockArticle.find(params[:id])
render :layout => false
end
# three possibilites to fill a new_stock_article form
# (1) start from blank or use params
def new def new
@stock_article = StockArticle.new @stock_article = StockArticle.new(params[:stock_article])
render :layout => false
end
# (2) StockArticle as template
def copy
@stock_article = StockArticle.find(params[:stock_article_id]).dup
render :layout => false
end
# (3) non-stock Article as template
def derive
@stock_article = Article.find(params[:old_article_id]).becomes(StockArticle).dup
render :layout => false
end end
def create def create
@stock_article = StockArticle.new(params[:stock_article]) @stock_article = StockArticle.new(params[:stock_article])
if @stock_article.save if @stock_article.valid? and @stock_article.save
redirect_to stock_articles_path, :notice => I18n.t('stockit.stock_create.notice') render :layout => false
else else
render :action => 'new' render :action => 'new', :layout => false
end end
end end
def edit def edit
@stock_article = StockArticle.find(params[:id]) @stock_article = StockArticle.find(params[:id])
render :layout => false
end end
def update def update
@stock_article = StockArticle.find(params[:id]) @stock_article = StockArticle.find(params[:id])
if @stock_article.update_attributes(params[:stock_article]) if @stock_article.update_attributes(params[:stock_article])
redirect_to stock_articles_path, :notice => I18n.t('stockit.stock_update.notice') render :layout => false
else else
render :action => 'edit' render :action => 'edit', :layout => false
end end
end end
@ -36,9 +68,15 @@ class StockitController < ApplicationController
@stock_changes = @stock_article.stock_changes.order('stock_changes.created_at DESC') @stock_changes = @stock_article.stock_changes.order('stock_changes.created_at DESC')
end end
def show_on_stock_article_update # See publish/subscribe design pattern in /doc.
@stock_article = StockArticle.find(params[:id])
render :layout => false
end
def destroy def destroy
@article = StockArticle.find(params[:id]) @stock_article = StockArticle.find(params[:id])
@article.mark_as_deleted @stock_article.mark_as_deleted
render :layout => false render :layout => false
rescue => error rescue => error
render :partial => "destroy_fail", :layout => false, render :partial => "destroy_fail", :layout => false,

View file

@ -14,4 +14,13 @@ module StockitHelper
link_to t('.stock_taking'), stock_taking_path(stock_change.stock_taking) link_to t('.stock_taking'), stock_taking_path(stock_change.stock_taking)
end end
end end
def stock_article_price_hint(stock_article)
t('simple_form.hints.stock_article.edit_stock_article.price',
:stock_article_copy_link => link_to(t('.copy_stock_article'),
stock_article_copy_path(stock_article),
:remote => true
)
)
end
end end

View file

@ -1,9 +1,12 @@
= simple_form_for stock_article, :validate => true do |f| = simple_form_for stock_article, remote: true, :validate => true do |f|
.modal-header
= link_to t('ui.marks.close').html_safe, '#', class: 'close', data: {dismiss: 'modal'}
%h3= title
.modal-body
= f.association :supplier = f.association :supplier
= f.input :name = f.input :name
= f.input :unit = f.input :unit
= f.input :note = f.input :note
- if stock_article.new_record? - if stock_article.new_record?
= f.input :price = f.input :price
= f.input :tax, :wrapper => :append do = f.input :tax, :wrapper => :append do
@ -11,8 +14,8 @@
%span.add-on % %span.add-on %
= f.input :deposit = f.input :deposit
- else - else
= f.input :price, :input_html => {:disabled => 'disabled'}, :hint => t('.price_hint') = f.input :price, :input_html => {:disabled => 'disabled'}, :hint => stock_article_price_hint(stock_article).html_safe
= f.association :article_category = f.association :article_category
.form-actions .modal-footer
= f.submit class: 'btn' = link_to t('ui.close'), '#', class: 'btn', data: {dismiss: 'modal'}
= link_to t('ui.or_cancel'), stock_articles_path = f.submit :class => 'btn btn-primary', 'data-disable-with' => t('ui.please_wait')

View file

@ -0,0 +1,14 @@
%tr{:class => stock_article_classes(stock_article), :id => "stockArticle-#{stock_article.id}"}
%td= link_to stock_article.name, stock_article
%td= stock_article.quantity
%td= stock_article.quantity - stock_article.quantity_available
%th= stock_article.quantity_available
%td= stock_article.unit
%td= stock_article.price
%td= number_to_percentage stock_article.tax
%td= link_to stock_article.supplier.name, stock_article.supplier
%td= stock_article.article_category.name
%td
= link_to t('ui.edit'), edit_stock_article_path(stock_article), remote: true, class: 'btn btn-mini'
= link_to t('ui.delete'), stock_article, :method => :delete, :confirm => t('.confirm_delete', :name => stock_article.name),
class: 'btn btn-mini btn-danger', :remote => true

View file

@ -0,0 +1,26 @@
#stockArticleDetails
%dl.dl-horizontal
%dt= heading_helper(StockArticle, :supplier)
%dd= link_to stock_article.supplier.name, stock_article.supplier
%dt= heading_helper(StockArticle, :name)
%dd= stock_article.name
%dt= heading_helper(StockArticle, :unit)
%dd= stock_article.unit
%dt= heading_helper(StockArticle, :price)
%dd= number_to_currency stock_article.price
%dt= heading_helper(StockArticle, :tax)
%dd= number_to_percentage stock_article.tax
%dt= heading_helper(StockArticle, :deposit)
%dd= number_to_currency stock_article.deposit
%dt= heading_helper(StockArticle, :fc_price)
%dd= number_to_currency stock_article.fc_price
%dt= heading_helper(StockArticle, :article_category)
%dd= stock_article.article_category.name
%dt= heading_helper(StockArticle, :note)
%dd= stock_article.note
%dt= heading_helper(StockArticle, :quantity)
%dd= stock_article.quantity
%dt= heading_helper(StockArticle, :quantity_available)
%dd= stock_article.quantity_available
.form-actions
= link_to t('ui.edit'), edit_stock_article_path(stock_article), remote: true, class: 'btn'

View file

@ -0,0 +1,9 @@
$('#modalContainer').html('<%= j(render(
:partial => "form",
:locals => {
:title => t('.title'),
:stock_article => @stock_article
}
)) %>');
$('#modalContainer').modal();

View file

@ -0,0 +1,15 @@
$('div.container-fluid').prepend('<%= j(render(
:partial => 'shared/alert_success',
:locals => {
:alert_message => t('.notice', :name => @stock_article.name)
}
)) %>');
// Publish database changes.
// See publish/subscribe design pattern in /doc.
$(document).trigger({
type: 'StockArticle#create',
stock_article_id: <%= @stock_article.id %>
});
$('#modalContainer').modal('hide');

View file

@ -0,0 +1,13 @@
$('div.container-fluid').prepend('<%= j(render(
:partial => 'shared/alert_success',
:locals => {
:alert_message => t('.notice', :name => @stock_article.name)
}
)) %>');
// Publish database changes.
// See publish/subscribe design pattern in /doc.
$(document).trigger({
type: 'StockArticle#destroy',
stock_article_id: <%= @stock_article.id %>
});

View file

@ -1,8 +0,0 @@
-# please polish the following line if you know how, same in partial _destroy_fail
var successDiv = $('<div class="alert fade in alert-success"><a class="close" data-dismiss="alert" href="#">#{escape_javascript(t('ui.marks.close').html_safe)}</a></div>');
successDiv.append(document.createTextNode('#{escape_javascript(t('.notice', name: @article.name))}'));
$('div.container-fluid').prepend(successDiv);
$('#stockArticle-#{@article.id}').remove();
-# WARNING: Do not use a simple .fadeOut() above, because it conflicts with the show/hide function of unavailable articles.

View file

@ -1,3 +0,0 @@
- title t('.title')
= render :partial => 'form', :locals => {:stock_article => @stock_article}

View file

@ -0,0 +1,9 @@
$('#modalContainer').html('<%= j(render(
:partial => "form",
:locals => {
:title => t('.title'),
:stock_article => @stock_article
}
)) %>');
$('#modalContainer').modal();

View file

@ -3,6 +3,30 @@
:javascript :javascript
$(function() { $(function() {
$('tr.unavailable').hide(); $('tr.unavailable').hide();
// Subscribe to database changes.
// See publish/subscribe design pattern in /doc.
$(document).on('StockArticle#create', function(e) {
$.ajax({
url: '#{index_on_stock_article_create_stock_articles_path}',
type: 'get',
data: {id: e.stock_article_id},
contentType: 'application/json; charset=UTF-8'
});
});
$(document).on('StockArticle#destroy', function(e) {
$('#stockArticle-' + e.stock_article_id).remove();
});
$(document).on('StockArticle#update', function(e) {
$.ajax({
url: '#{index_on_stock_article_update_stock_articles_path}',
type: 'get',
data: {id: e.stock_article_id},
contentType: 'application/json; charset=UTF-8'
});
});
}) })
.well.well-small .well.well-small
@ -17,7 +41,7 @@
.btn-group .btn-group
= link_to_if @current_user.role_orders?, t('.order_online'), new_order_path(supplier_id: 0), = link_to_if @current_user.role_orders?, t('.order_online'), new_order_path(supplier_id: 0),
class: 'btn', class: 'btn btn-primary' class: 'btn', class: 'btn btn-primary'
= link_to t('.new_stock_article'), new_stock_article_path, class: 'btn' = link_to t('.new_stock_article'), new_stock_article_path, remote: true, class: 'btn'
= link_to t('.new_stock_taking'), new_stock_taking_path, class: 'btn' = link_to t('.new_stock_taking'), new_stock_taking_path, class: 'btn'
= link_to t('.show_stock_takings'), stock_takings_path, class: 'btn' = link_to t('.show_stock_takings'), stock_takings_path, class: 'btn'
@ -42,22 +66,9 @@
%th= t '.article.supplier' %th= t '.article.supplier'
%th= t '.article.category' %th= t '.article.category'
%th %th
%tbody %tbody#articles-tbody
- for article in @stock_articles - for stock_article in @stock_articles
%tr{:class => stock_article_classes(article), :id => "stockArticle-#{article.id}"} = render :partial => 'stock_article', :locals => {:stock_article => stock_article}
%td= link_to article.name, article
%td= article.quantity
%td= article.quantity - article.quantity_available
%th= article.quantity_available
%td= article.unit
%td= article.price
%td= number_to_percentage article.tax
%td= link_to article.supplier.name, article.supplier
%td= article.article_category.name
%td
= link_to t('ui.edit'), edit_stock_article_path(article), class: 'btn btn-mini'
= link_to t('ui.delete'), article, :method => :delete, :confirm => t('.confirm_delete'),
class: 'btn btn-mini btn-danger', :remote => true
%p %p
= t '.stock_worth' = t '.stock_worth'
= number_to_currency StockArticle.stock_value = number_to_currency StockArticle.stock_value

View file

@ -0,0 +1,12 @@
// Handle more advanced DOM update after AJAX database manipulation.
// See publish/subscribe design pattern in /doc.
(function() {
var stock_article_row = $('<%= j(render(
:partial => 'stock_article',
:locals => {
:stock_article => @stock_article
}
)) %>');
$('#articles-tbody').prepend(stock_article_row);
})();

View file

@ -0,0 +1,12 @@
// Handle more advanced DOM update after AJAX database manipulation.
// See publish/subscribe design pattern in /doc.
(function() {
var stock_article_row = $('<%= j(render(
:partial => 'stock_article',
:locals => {
:stock_article => @stock_article
}
)) %>');
$('#stockArticle-<%= @stock_article.id %>').replaceWith(stock_article_row);
})();

View file

@ -1,22 +0,0 @@
- title t('.title')
- content_for :head do
:javascript
$(function() {
$('#article_search').autocomplete({
source: '#{articles_search_stock_articles_path}',
select: function(e, ui) {
alert(ui.item.value);
//location.href = '#{nil}' + ui.item.value;
}
});
})
/
TODO: Fix this
%p
= t '.search_text'
= text_field_tag 'article_search'
#stock_article_form
= render :partial => 'form', :locals => {:stock_article => @stock_article}

View file

@ -0,0 +1,9 @@
$('#modalContainer').html('<%= j(render(
:partial => "form",
:locals => {
:title => t('.title'),
:stock_article => @stock_article
}
)) %>');
$('#modalContainer').modal();

View file

@ -1,32 +1,22 @@
- title @stock_article.name - title @stock_article.name
- content_for :javascript do
:javascript
$(function() {
// Subscribe to database changes.
// See publish/subscribe design pattern in /doc.
$(document).on('StockArticle#update', function(e) {
$.ajax({
url: '#{show_on_stock_article_update_stock_articles_path}',
type: 'get',
data: {id: e.stock_article_id},
contentType: 'application/json; charset=UTF-8'
});
});
});
.row-fluid .row-fluid
.span6 .span6
%dl.dl-horizontal = render :partial => 'stock_article_details', :locals => {:stock_article => @stock_article}
%dt= StockArticle.human_attribute_name 'supplier'
%dd= link_to @stock_article.supplier.name, @stock_article.supplier
%dt= StockArticle.human_attribute_name 'name'
%dd= @stock_article.name
%dt= StockArticle.human_attribute_name 'unit'
%dd= @stock_article.unit
%dt= StockArticle.human_attribute_name 'price'
%dd= number_to_currency @stock_article.price
%dt= StockArticle.human_attribute_name 'tax'
%dd= number_to_percentage @stock_article.tax
%dt= StockArticle.human_attribute_name 'deposit'
%dd= number_to_currency @stock_article.deposit
%dt= StockArticle.human_attribute_name 'fc_price'
%dd= number_to_currency @stock_article.fc_price
%dt= StockArticle.human_attribute_name 'article_category'
%dd= @stock_article.article_category.name
%dt= StockArticle.human_attribute_name 'note'
%dd= @stock_article.note
%dt= StockArticle.human_attribute_name 'quantity'
%dd= @stock_article.quantity
%dt= StockArticle.human_attribute_name 'quantity_available'
%dd= @stock_article.quantity_available
.form-actions
= link_to t('ui.edit'), edit_stock_article_path(@stock_article), class: 'btn'
.span6 .span6
%h2= t('.stock_changes') %h2= t('.stock_changes')

View file

@ -0,0 +1,13 @@
// Handle more advanced DOM update after AJAX database manipulation.
// See publish/subscribe design pattern in /doc.
(function() {
var stock_article_details = $('<%= j(render(
:partial => 'stock_article_details',
:locals => {
:stock_article => @stock_article
}
)) %>');
$('#stockArticleDetails').replaceWith(stock_article_details);
$('h1').first().text('<%= j(@stock_article.name) %>');
})();

View file

@ -0,0 +1,15 @@
$('div.container-fluid').prepend('<%= j(render(
:partial => 'shared/alert_success',
:locals => {
:alert_message => t('.notice', :name => @stock_article.name)
}
)) %>');
// Publish database changes.
// See publish/subscribe design pattern in /doc.
$(document).trigger({
type: 'StockArticle#update',
stock_article_id: <%= @stock_article.id %>
});
$('#modalContainer').modal('hide');

View file

@ -1348,7 +1348,7 @@ en:
units_to_order: If you change the total amount of delivered units, you also have to change individual group amounts by clicking on the article name. They will not be automatically recalculated and so ordergroups may be accounted for articles that were not delivered! units_to_order: If you change the total amount of delivered units, you also have to change individual group amounts by clicking on the article name. They will not be automatically recalculated and so ordergroups may be accounted for articles that were not delivered!
update_current_price: Also update the price of the current order update_current_price: Also update the price of the current order
stock_article: stock_article:
copy_stock_article: copy:
name: Please modify name: Please modify
edit_stock_article: edit_stock_article:
price: <ul><li>Price changes are forbidden.</li><li>If necessary, %{stock_article_copy_link}.</li></ul> price: <ul><li>Price changes are forbidden.</li><li>If necessary, %{stock_article_copy_link}.</li></ul>
@ -1421,8 +1421,10 @@ en:
stockit: stockit:
check: check:
not_empty: ! '%{name} could not be deleted, the inventory is not zero.' not_empty: ! '%{name} could not be deleted, the inventory is not zero.'
create:
notice: New stock article »%{name}« was created.
destroy: destroy:
notice: Article %{name} was deleted. notice: Article »%{name}« was deleted.
edit: edit:
title: Edit stock articles title: Edit stock articles
form: form:
@ -1461,10 +1463,10 @@ en:
reason: Reason reason: Reason
stock_changes: Stock quantity changes stock_changes: Stock quantity changes
stock_taking: Inventory stock_taking: Inventory
stock_create: stock_article:
notice: Stock article was created. confirm_delete: Are you sure you want to delete the stock article »%{name}«?
stock_update: update:
notice: Stock article was saved. notice: Stock article »%{name}« was saved.
suppliers: suppliers:
create: create:
notice: Supplier was created notice: Supplier was created

View file

@ -86,9 +86,15 @@ Foodsoft::Application.routes.draw do
end end
resources :stock_articles, :to => 'stockit' do resources :stock_articles, :to => 'stockit' do
get :copy
collection do collection do
get :articles_search get :articles_search
get :fill_new_stock_article_form get :fill_new_stock_article_form
get :index_on_stock_article_create
get :index_on_stock_article_update
get :show_on_stock_article_update
end end
end end

View file

@ -0,0 +1,33 @@
# Publish/subscribe pattern
## Handling DOM updates after AJAX database manipulation
As an example, let us consider the manipulation (create, update...) of `StockArticles`. This can be done in different views, e.g., `stock_articles/index`, `stock_articles/show` and `deliveries/_form` through modals using AJAX requests. As an advantage of the AJAX technique, the user does not need to reload the entire page. However, (after the update of the `StockArticle` in the database) it is generally required to update the DOM in the current view such that the page properly reacts to the asynchronous actions.
The process can be divided in two steps: **1.** AJAX database manipulation and **2.** DOM updates for the particular view. The crucial point is the coupling of the two steps since the controller for the first step offers the same functionality to all views and does not need to know anything about the current view.
1. AJAX database manipulation
1. Example: current view `deliveries/_form` offers a link for the AJAX action `StockArticle#new`. This opens a modal filled with `stock_articles/_form`.
2. AJAX form post addresses the `StockArticle#create` action which handles the database manipulation.
3. The database manipulation is finished by the rendering of, e.g., `stock_articles/create.js.erb`. The key task there is to **publish** the database changes by calling `trigger`, i.e.,
$(document).trigger({
type: 'StockArticle#create',
stock_article_id: <%= @stock_article.id %>
});
2. DOM updates for the particular view
1. Each view has the opportunity to **subscribe** to particular events of the previous step. A very simple example is the update of the `stock_articles/index` view after `StockArticle#destroy`:
$(document).on('StockArticle#destroy', function(e) {
$('#stockArticle-' + e.stock_article_id).remove();
});
However, in most of the situations you will like to use the full power of the MVC framework in order to read new data from the database and render some partial. Let us consider this slightly more advanced case in the following.
The view `stock_articles/index` could listen (amongst others) to `StockArticle#create` like this:
$(document).on('StockArticle#create', function(e) {
$.ajax({
url: '#{index_on_stock_article_create_stock_articles_path}',
type: 'get',
data: {id: e.stock_article_id},
contentType: 'application/json; charset=UTF-8'
});
});
2. The action `StockArticles#index_on_stock_article_create` is a special helper action to handle DOM updates of the `stock_articles/index` view after the creation of a new `StockArticle` with the given `id`.