Building a persistent in-line editing experience with Crystal, MongoDB (datanoise / sam0x17)

As documentation is still sparse, I would like to add some.

This is what I am building currently:


The individual fields are going to be editable with an inline editor, which will automatically save to the backend – no need for reloading the whole page.


The point of the whole is a backend for my company, to be able to automate some processes, calculate stock prices, keep things in stock, etc. A warehouse application basically, tailored to me!

Technologies used

Important bits


MongoORM uses _id as the main index. Please note the underscore!

The _id is NOT a string. It is of the type ObjectId

Therefore it will need to be constructed. If you want to construct it from a previous id, use this code:

mongo_id = my_string_id

where my_string_id is a String, and mongo_id is an ObjectId.

The code


is responsible for setting up the communication to your Mongo DB instance. Please refer to Sam’s documentation., responsible for defining Stockitem, Subitem, and the apropriate Kemal handlers:

require “mongo_orm”
require “json”
require “./”
require “./”

module Stock
     include Macros

    class Itemgroup < Mongo::ORM::Document #used for grouping, e.g. Pimoroni products, Shipping, etc.
         field group : String
         has_many :stockitems #note that the stockitem can only belong to one ItemGroup at a time.

    #this includes other items as well (shipping, digital downloads, etc)
     class Stockitem < Mongo::ORM::Document
         field sku : String
         field description : String
         field is_alias : Bool
         field alias_of : String #if this field is set, do not use any other definitions from this item, get the aliased master item
         field is_set : Bool # if this item is a set of other items
         embeds_many :subitems # should only be set if is_set = true
         field is_physical_item : Bool
         field weee_applicable : Bool #can be either: paid for by someone else, or item is not electronics, …
         field weight : Int32 # in g

    class Subitem < Mongo::ORM::EmbeddedDocument
         field sku : String #a reference to the main stock item. NB: this could probably be reworked in a nicer fashion with belongs_to ? e.g. belongs_to :itemref, class_name: Stock::Stockitem
         field note : String
         field amount : Int32

    get “/stock/show” do |env|
         sis = Stockitem.all({“is_alias” => {“$eq” => false}})
         name = “Stock :: Show &amp; Edit”
         mrender “stock”

    post “/stock/edit/:what” do |env|
         pp “Entering edit, with target ” + env.params.url[“what”]
         pp env.params.body
         if env.params.body.has_key?(“pk”) && env.params.body.has_key?(“value”)
             pp “Inside pk & value”
             pk = env.params.body[“pk”]?.as(String)
             value = env.params.body[“value”]?.as(String)
             if si = Stockitem.find( pk)   
                 case env.params.url[“what”]
                 when “sku”
                     si.sku = value
                 when “description”
                     si.description = value
                 when “is_alias”
                     si.is_alias = value == “true” ? true : false
                 when “alias_of”
                     si.alias_of = value
                 when “is_set”
                     si.is_set = value == “true” ? true : false
                 when “is_physical_item”
                     si.is_physical_item = value == “true” ? true : false
                 when “weee_applicable”
                     pp “weee_applicable”
                     si.weee_applicable = value == “true” ? true : false
                     pp “inside”
                     env.response.status_code = 200
                     env.response.status_code = 400
                 env.response.status_code = 400   
             env.response.status_code = 400

    #generate from WEEE entries
     get “/stock/generate/:year” do |env|
         #walk through the entire database, collect the items, and display a list
         stock_collection = Hash(String,String).new
         invoices = WEEE::Invoice.all({“year”=>env.params.url[“year”].to_i32})       
         invoices.each do |invoice|
             #p “processing invoice #{invoice.number}”
             #note that we might not get absolutely all invoices here, as easybill has a different view of the dates (import)
             invoice.items.each do |item|
                 if !item.description.nil? && !item.number.nil?
                     key = item.number == “” ? item.description : item.number
                     if !key.nil?
                         if !stock_collection.has_key?(key)
                             description = “”
                             if !item.description.nil?
                                 description = item.description
                             #stock_collection[key] = item.description
                             if !description.nil?
                                 stock_collection[key] = description
                         end # end of !stock_collection.has_key?
                     end # end of if !key.nil?
             end #end of invoice.items.each do
         end #end of invoices.each
         #check that this does not exist already in our database, else create it.
         stock_collection.each do |sku,description|
             if !Stockitem.find_by(:sku, sku)
                 si =
                 si.sku = sku
                 si.is_alias = false
                 si.alias_of = “”
                 si.description = description
                 si.is_set = false
                 si.is_physical_item = true
                 si.weee_applicable = true
                 si.weight = 100
     end #end of get macro

end #end of module


  • Sam’s documentation explains parts of this code, I will only address the stumbling blocks:
  • sis = Stockitem.all({“is_alias” => {“$eq” => false}}) – this code searches for Stockitems (Mongo ORM will take care of the DB name for you, etc.)
  • si = Stockitem.find( pk)
    • note that here is used to reconstruct the id from the key pk which was passed
  • return code 200 on success and 400 on failure is for x-editable to know whether the edit succeeded

Behaviour of

  •! will raise an error if saving is not possible
  • will return true if saving was possible, and false if it was not possible

stock.ecr, the HTML template

<% content_for “name” do %>
     <%= name %>
<% end %>

<% content_for “main” do%>
<div class=”col-md-12″>
     <div class=”box box-primary”>
         <div class=box-header with-border”>
             <h3 class=”box-title”><%= name %></h3>
         <div class=”box-body”>
             Please note: alias’ed items are not loaded from the database.
             <TABLE class=”table table-bordered table-striped dataTable” role=”grid” aria-describedby=”Table with stock items” id=”stock_items”>
                     <tr role=”row”>
                         <th class=”sorting_asc” tabindex=”0″>sku</th>
                         <th>weight (g)</th>
                 <% sis.each do |si| %>
                 <TR role=”row”>
                     <TD><%= si.sku.to_s %></TD>
                     <TD><%= si.description.to_s %></TD>
                     <TD><%= si.is_alias ? “true” : “false” %></TD>
                     <TD><%= si.alias_of.to_s %></TD>
                     <TD><%= si.is_set ? “true” : “false” %></TD>
                     <TD><%= si.is_physical_item ? “true” : “false” %></TD>
                     <TD><A HREF=”#”
                             id=”<%= si._id.to_s.rchop %>.weee_applicable”
                             class=”weee_applicable editable”
                             data-pk=”<%= si._id.to_s.rchop %>”
                             data-source=”[{value: ‘true’, text: ‘true’}, {value: ‘false’, text: ‘false’}]”
                             data-title=”WEEE: <%= si.sku.to_s %>”
                         ><%= si.weee_applicable ? “true” : “false” %></A></TD>
                     <TD><%= si.weight.to_s %> g</TD>
                 <% end %>

     //demo.js x-editable
     window.onload = function() {
         $(document).ready(function (){
             $.fn.editable.defaults.mode = ‘popup’;
             $(“.editable”).on(‘hidden’, function(e, reason){
                 if(reason === ‘save’ || reason === ‘nochange’){
                     var $next = $(this).closest(‘tr’).next().find(‘.editable’);
                     }, 300);
<% end %>


  • since we add our JS scripts after this (I should rework this to add a custom script section at the end), we have to wrap them in a window.onload function – otherwise $ is not defined (jQuery shortcut)
  • note how we use a function here to jump to the next editable on save success
  • si._id.to_s.rchop – the BSON id is translated to a string, and then the last weird character (null terminator?) is chopped off. This is an important stumbling block!

layout.ecr (excerpts)

<!DOCTYPE html>
   <meta charset=”utf-8″>
   <meta http-equiv=”X-UA-Compatible” content=”IE=edge”>
   <title>TaxGod | <%= yield_content “name” %></title>
   <meta content=”width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no” name=”viewport”>
   <link rel=”stylesheet” href=”/bower_components/bootstrap/dist/css/bootstrap.min.css”>
   <link rel=”stylesheet” href=”/bower_components/font-awesome/css/font-awesome.min.css”>
   <link rel=”stylesheet” href=”/bower_components/Ionicons/css/ionicons.min.css”>
   <link rel=”stylesheet” href=”/dist/css/AdminLTE.min.css”>
   <link rel=”stylesheet” href=”/dist/css/skins/skin-blue.min.css”>
   <link rel=”stylesheet” href=”/danielm_uploader/css/”>
   <link rel=”stylesheet” href=”/bower_components/”>
   <link rel=”stylesheet” href=”/tg_css/bootstrap-editable.css”>
   <link rel=”stylesheet” href=”/tg_css/picockpit.css”>
   <link rel=”stylesheet” href=”/tg_css/dropzone.css”> 
   <!–[if lt IE 9]>
   <script src=””></script>
   <script src=””></script>

  <link rel=”stylesheet”
<body class=”hold-transition skin-blue sidebar-mini”>
<div class=”wrapper”>


  <!– Content Wrapper. Contains page content –>
   <div class=”content-wrapper”>
     <!– Content Header (Page header) –>
     <section class=”content-header”>
         TaxGod :: <%= yield_content “name” %>
         <small>Being God is fun.</small>

    <!– Main content –>
     <section class=”content container-fluid”>

     <%= yield_content “main” %>

     <!– /.content –>
   <!– /.content-wrapper –>

<!– ./wrapper –>


<!– jQuery 3 –>
<script src=”/bower_components/jquery/dist/jquery.min.js”></script>
<!– Bootstrap 3.3.7 –>
<script src=”/bower_components/bootstrap/dist/js/bootstrap.min.js”></script>
<!– AdminLTE App –>
<script src=”/danielm_uploader/js/”></script>
<script src=”/bower_components/”></script>
<script src=”/bower_components/”></script>
<script src=”/dist/js/adminlte.min.js”></script>
<script src=”/tg_js/bootstrap-editable.min.js”></script>
<script src=”/tg_js/dropzone.min.js”></script>

Further references