Saturday, September 24, 2016

Using bootstrap-datepicker in Rails Apps

Using a datepicker widget in Rails is simple. There are two ways of doing so:


  • Using ‘date_field’ (HTML5)
  • Gem ‘bootstrap-datepicker-rails’

Using 'date_field' to Generate Datepicker Widget

<%= form_for(order) do |f| %>
        :
    <%= f.date_field :shipment_require_date %>
        :
<% end %>       
The datepicker widget will show up while focusing on the field.
However, this HTML5 functionality only supported by Chrome.


It does not work while using Safari or Firefox.

Using Gem‘bootstrap-datepicker-rails’

Gem ‘bootstrap-datepicker-rails’ is a datepicker widget gem in the Bootstrap style for Rails. The document is here.
According to the document, just few steps to go and you are all done!
  • First, include ‘bootstrap-datepicker-rails’ in Gemfile.
gem 'bootstrap-datepicker-rails'
  • And add this line to app/assets/stylesheets/application.css
 *= require bootstrap-datepicker
  • Add this line to app/assets/javascripts/application.js
//= require bootstrap-datepicker
  • add code 
    There are two ways of coding: 
    • jQuery
    • Code In-line

jQuery: Assigning values to the options by using javascript.

<%= f.text_field :shipment_require_date, class: "datepicker" %>
        :
    <script type="text/javascript">
      $(document).ready(function(){
        $('.datepicker').datepicker({
          format: "yyyy/mm/dd",
          autoclose: true,
          todayBtn: "linked",
          todayHighlight': true
        });
      });
    </script>
The story ends.

Code In-line: Assign values to the options directly in-line.

According to the options document, option should be transformed to a certain format (data-date-) in order to be placed in-line to the code.
All options that take a “Date” can handle a Date object; a String formatted according to the given format; or a timedelta relative to today, eg “-1d”, “+6m +1y”, etc, where valid units are “d” (day), “w” (week), “m” (month), and “y” (year). Use “0” as today. There are also aliases for the relative timedelta’s: “yesterday” equals “-1d”, “today” is equal to “+0d” and “tomorrow” is equal to “+1d”.
Most options can be provided via data-attributes. An option can be converted to a data-attribute by taking its name, replacing each uppercase letter with its lowercase equivalent preceded by a dash, and prepending “data-date-” to the result. For example, startDate would be data-date-start-dateformat would bedata-date-format, and daysOfWeekDisabled would be data-date-days-of-week-disabled.
Therefore, option ‘todayBtn’ should be ‘data-date-today-btn‘.
The code of the datepicker part is:
app/views/orders/_form.html.erb
<%= f.text_field :shipment_require_date, 
    data: { provide: "datepicker", 
        'date-format': 'yyyy-mm-dd', 
        'date-autoclose': 'true', 
        'date-today-btn': 'linked',
        'date-today-highlight': 'true'} %>
It generates HTML code as follows:
<input data-provide="datepicker" data-date-format="yyyy-mm-dd" data-date-autoclose="true" data-date-today-btn="linked" data-date-today-highlight="true" type="text" name="order[shipment_require_date]" id="order_shipment_require_date" />
Is this way more concise than the jQuery one? It depends. Maybe the jQuery way is better since the code will be cleaner if there are more than two datepicker widgets.

Making a Helper for Code In-line.

As you can see, if using code in-line method and the datepicker input fields are more than one, it is necessary to refactor the code. A helper is created as follows:
app/helpers/application_helper.rb
def datepicker_field(form, field)
    form.text_field(field, data: { provide: "datepicker",
      'date-format': 'yyyy-mm-dd',
      'date-autoclose': 'true',
      'date-today-btn': 'linked',
      'date-today-highlight': 'true'}).html_safe
end
It will generate the HTML code exactly the same as above.
The code of the datepicker part changes to:
app/views/orders/_form.html.erb
<%= datepicker_field(f, :shipment_require_date) %>

It's nice and clean! Happy ending!

Saturday, September 10, 2016

An Improved Error Message Bar

When showing error message such as validation errors or any kind of error messages stored in the object instantiated from the class ActiveModel::Errors, the default way to show error messages is ugly and inefficient. Let’s take a look at the validation error handling code generated by rails g scaffold (take model Color for example):
/app/views/colors/_form.html.erb
<%= form_for(color) do |f| %>
  <% if color.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(color.errors.count, "error") %> prohibited this color from being saved:</h2>
      <ul>
        <% color.errors.full_messages.each do |message| %>
          <li><%= message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :name %>
    <%= f.text_field :name %>
  </div>

  <div class="action">
    <%= f.submit %>
    <%= link_to 'Back' %>
  </div>
<% end %>
The error message handling code is as follows:
<% if color.errors.any? %>
  <div id="error_explanation">
  <h2><%= pluralize(color.errors.count, "error") %> prohibited this color from being saved:</h2>
    <ul>
      <% color.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
    </ul>
  </div><% end %>
The new.html.erb and edit.html.erb forms call partial form _form.html.erb. However, there are many models using the same code as above except the model name. It is obvious that code refactoring jobs must be done to the error message code.
Besides, the original error message code generates a not very nice-looking web page. It is just few lines of messages shown on top of the page. Why don’t we use the same idea of notice message bar to make a better looking error message bar?

Sure we can, so here is the code and how to implement it:

1. Create a helper method error_message_for(object_habdled)in application_helper.rb.

/app/helpers/applicaiton_helper.rb
def error_message_for(object_handled)
    if object_handled.errors.any?
      close_button_options = { class: "close", "data-dismiss" => "alert", "aria-hidden" => true }
      close_button = content_tag(:button, "×", close_button_options)

      error_content = close_button + error_content
      object_handled.errors.full_messages.each do |message|
        error_content = error_content + message + simple_format("\n")
      end
      content_tag(:div, error_content, id: "error_explanation", class: "alert alert-danger alert-dismissable")
    end
  end

2. Call the helper method in every _form.html.erband assign the model name accordingly.

Insert <%= error_message_for(color) %>on top of the partial form.
For example:
/app/views/color/_form.html.erb
<%= error_message_for(color) %>
<%= form_for(color) do |f| %>
  <div class="field">
    <%= f.label :name %>
    <%= f.text_field :name %>
  </div>
  <div class="action">
    <%= f.submit %>
    <%= link_to 'Back' %>
  </div>
<% end %>
Now, it shows a very nice looking error message bar on top of the page with Bootstrap style.

Thursday, September 8, 2016

Make a Standard Flash Message Bar with Bootstrap Style in Rails Application

Normally, we have a very simple flash message such as “The data has been saved!” in the top of the browser to tell you the operation is done. It is just a simple text, sometimes we even don’t see it since it is not shining!

Fortunately, we can make a helper method accompany with Bootstrap classes to make it look better. It’s simple, useful and can be used as a standard code base for every project.

Here is how:

1. Add a line of ocde: <%= notice_message %> in

/app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  :
  :
  <body>
    <div class="container" >
      <%= notice_message %>
      <%= yield %>
    </div>
  </body>
</html>

2. Add a helper method “notice_message” in

/app/helpers/application_helper.rb
module ApplicationHelper
  def notice_message
    alert_types = { notice: :success, alert: :danger }

    close_button_options = { class: "close", "data-dismiss" => "alert", "aria-hidden" => true }
    close_button = content_tag(:button, "×", close_button_options)

    alerts = flash.map do |type, message|
      alert_content = close_button + message

      alert_type = alert_types[type.to_sym] || type
      alert_class = "alert alert-#{alert_type} alert-dismissable"

      content_tag(:div, alert_content, class: alert_class)
    end

    alerts.join("\n").html_safe
  end
end
Once the helper is called, like:
redirect_to @size, notice: 'Size was successfully created.'
The flash notice shows:

Monday, September 5, 2016

Build an Order Form Using Google Spreadsheet Service and Google Apps Script

Google SpreadSheet is a free, convient and powerful tool to use for everybody. Not only the super easy usability but also programable capability allowed users to customize Google SpreadSheet to have more advanced functionalities. By using on-line editing and debugging tool, writting javascript and calling Google Spreadsheet Service seems to be an easy job to do.

In the Spreadsheet window, click the "Tool" and select "code editor".




There are four important classes needed to be noticed:
  • SpreadsheetApp
  • SpreadSheet
  • Range
  • Cell

Basic Operation

A user has to call these classes one by one in order to get the object to be controlled. For example:
// get active spreadsheet.
var ss = SpreadsheetApp.getActiveSpreadsheet(); 

// get spreadsheet named "Order Form". 
var sheet = ss.getSheetByName("Order Form");

// get range from first row to the max row, first column to the max column.
var seriesRange = sheet.getRange(startingRow, 1, sheet.getMaxRows(), 1);

// get the first cell.
var aCell = seriesRange.getCell(1, 1);

Create Customized Menu












Google SpreadSheet allows us to cusmize menu. The demo code is:
function onOpen() {
  var ui = SpreadsheetApp.getUi();
  // Or DocumentApp or FormApp.
  ui.createMenu('Order Form')
      .addItem('Clear From', 'clearForm')
      .addSeparator() 
      .addItem('Submit Order', 'cloneSheet')
      // .addSubMenu(ui.createMenu('Sub-menu')
      //     .addItem('Second item', 'menuItem2'))
      .addToUi();
  
  // Set the starting cursor location.
  setStartingPoint_();
}

How to refresh (clear) the spreadsheet?

function clearForm() {
  var ui = SpreadsheetApp.getUi()
  var response = ui.alert('Clear Form', 'Do you want to clear all data?', ui.ButtonSet.OK_CANCEL);
  if (response == ui.Button.CANCEL) {
    return;
  }
  
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  
  var sheet = ss.getSheetByName('Empty Order Form');
  sheet.copyTo(ss).setName('Clear Order Form');
  newSheet = ss.getSheetByName('Clear Order Form');
  ss.setActiveSheet(newSheet);
  ss.moveActiveSheet(0);

  var orderFormSheet = ss.getSheetByName('Order Form');
  ss.deleteSheet(orderFormSheet);

  newSheet.setName('Order Form');  
  
  setStartingPoint_();
}

function setStartingPoint_() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName("Order Form");
  activeRange = sheet.getRange(5,2);
  sheet.setActiveRange(activeRange);
}

How to insert a row and do other logics?

function insertRow() {
  var startingRow = 4
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName("Order Form");
  var seriesRange = sheet.getRange(startingRow, 1, sheet.getMaxRows(), 1);
  var seriesValues = seriesRange.getValues();
  var seriesSelected = sheet.getRange((startingRow-1),1).getValue();   // .getRange("A3")
  
  // if series not selected, show warning dialog box 
  if (seriesSelected == "") {
    SpreadsheetApp.getUi().alert("Please select the series. Thank you.");
    return;
  };
  
  // find the row number of the title of series.
  // It also represents the sequence of the series, 
  // which can be used to determin the sequence of sheet.
  i = 0;
  while (seriesValues[i][0] != seriesSelected) {
    i++;
  };
  
  // Compensate the difference of starting point.
  seriesSelectedRow = i + startingRow;      
  
  // find the last row of the model.
  // Compensate the difference of starting point.
  var modelRow = seriesSelectedRow; 
  
  // "modelRow + 1" means start testing from the next row of the sub title. 
  while (!sheet.getRange(modelRow+1, 2).isBlank()) {
    modelRow += 1;
  };
  
  // if first model row is blank, then modelRaw = seriesSelectedRaw + 1
  if (modelRow == seriesSelectedRow) {
    modelRow += 1;
  };
  
  Logger.log("seriesSelectedRow: "+seriesSelectedRow)
  Logger.log("modelRow: "+modelRow)
    
  // This inserts one row after the last row of the model and focus on it.
  sheet.insertRowsAfter(modelRow,1);
  lastRowRange = sheet.getRange(modelRow+1,2);
  sheet.setActiveRange(lastRowRange);
  
  // Set the "TOTAL" formula
  sheet.getRange(modelRow+1, 7).setFormulaR1C1("=R[0]C[-2]*R[0]C[-1]")
}

Searching the right data

function onEdit() {
  var startingRow = 4
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  
  // Quit if not at the "Order From" sheet.
  if (ss.getSheetName() != "Order Form") {
    return
  };
  
  var sheet = ss.getSheetByName("Order Form");  //getActiveSheet();
  var seriesRange = sheet.getRange(startingRow, 1, sheet.getMaxRows(), 1);
  var ar = sheet.getActiveRange();
  var specSelected = ar.getValue();
  
  var row = ar.getRow();  // getA1Notation();
  Logger.log(row);
  
  // Quit if row at the heading part.
  if (row < startingRow) {
    return
  } 
  
  // find the series name where the avtive range belongs.
  
  var i = (row - 1);
  Logger.log(i);
 
  while (sheet.getRange(i,1).isBlank()) {
    i--;
  };
  
  // find the sequence of the series
  var seriesName = sheet.getRange(i,1).getValue()
  Logger.log(seriesName);
  
  var seriesSequence = 0;
  var j = 0;
  while (seriesRange.getValues()[j][0] != seriesName) {
    if (!seriesRange.getValues()[j][0] == '' ) {
      seriesSequence++;
    }; 
    j++;    
  };
  seriesSequence++;     // Cpmpensate the sequence.
  Logger.log(seriesSequence);
  
  // Get the worksheet containing the series data.
  var seriesSheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[seriesSequence];
  var seriesSheetName = seriesSheet.getName();
  Logger.log(seriesSheet.getName());
  
  specRange = seriesSheet.getRange(1, 1, seriesSheet.getLastRow(), 4);
  specValues = specRange.getValues();
  Logger.log("seriesSheet:"+seriesSheet.getName());
  Logger.log(specValues);
 
  // find the price associated with the spec
  k = 0;
  while (specValues[k][0] != specSelected) {
    k++;
  };
  
  // Set price to order form
  priceSelected = specValues[k][3];
  sheet.getRange(row, 5).setValue(priceSelected);
  Logger.log(priceSelected);
}

How to find out the current cell where you are in edit?


function currentCell() {
  return SpreadsheetApp.getActiveRange().getA1Notation();
}

How to duplicate to a new spreadsheet?

// copy data from current SpreadSheet to a new SpreadSheet  
function cloneSheet() {
 
  // Show dialog box to ask if continue
  var ui = SpreadsheetApp.getUi();
  var response = ui.alert('Submit Order', 'Do you want to submit order?', ui.ButtonSet.OK_CANCEL);

  if (response == ui.Button.CANCEL) {
    return;
  }
  
  // source doc
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName("Order Form");
  
  // Get full range of data
  var SRange = sheet.getDataRange();
  // var SRange = sheet.getRange("A:G");  
 
  // get A1 notation identifying the range
  var A1Range = SRange.getA1Notation();
 
  // get the data values in range
  var SData = SRange.getValues();
  var SFormat = SRange.getNumberFormats();
  
  // target spreadsheet
  // var tss = SpreadsheetApp.openById(ssB);
  var ssNew = SpreadsheetApp.create('Mekk_2017_Order_'+ getDateTime_());
  
  // target sheet
  // var ts = tss.getSheetByName('Target Spreadsheet'); 
  var sheetNew = ssNew.getActiveSheet();
  
  // Clear the Google Sheet before copy
  // ts.clear({contentsOnly: true});
 
  // set the target range to the values of the source data
  sheetNew.getRange(A1Range).setValues(SData);
  sheetNew.getRange(A1Range).setNumberFormats(SFormat);
  
  // Sets the first column to a width which fits the text
  sheetNew.autoResizeColumn(1);
  sheetNew.autoResizeColumn(2);
  sheetNew.getRange('A3').clear();
  
  // Get the current folder
  var ssId = ss.getId();
  var parentFolders = DriveApp.getFileById(ssId).getParents();
  
  while (parentFolders.hasNext()) {
   var folder = parentFolders.next();  // parentFolders.next() is the current folder. 
  }; 
  
  // Get the id of new spreadsheet
  var ssNewFile = DriveApp.getFileById(ssNew.getId());
  var ssNewName = ssNewFile.getName();
  
  // Put the Spreadsheet to the current folder
  folder.addFile(ssNewFile);
  
  // Remove the Spreadsheet from the root folder
  DriveApp.getRootFolder().removeFile(ssNewFile);
  
  // user can also close the dialog by clicking the close button in its title bar.
  SpreadsheetApp.getUi().alert("The Order has been placed. Thank you.");  
}

How to get a proper datetime format?


function getDateTime_() {
 // This formats the date as Greenwich Mean Time in the format
 // year-month-dateThour-minute-second.
 
 var formattedDate = Utilities.formatDate(new Date(), "GMT", "yyyy-MM-dd'T'HH:mm:ss'Z'"); 
 
 return formattedDate;
}