2

The question has a javascript and coffescript jsfiddle at the bottom of the question.

Both fiddle include explanatory comments that needs to be read in a specific order, it print out values to the console when you click on product or submit div, in addition I give you this basic explanation of my issue.

  • I have 3 Javascript classes Purchase, Product and Item
  • One Purchase has many Products, One Product has many Items
  • The Purchase object sets a click event handler on the $('submit') and onClick() will post the items data to my backend api
  • This is the data format accepted from my backend api

    {
      'purchase' => {
        'items_attributes' => {
          '0' => {
            'purchase_id' => '1'
          },
          '1' => {
            'purchase_id' => '2'
          }
        }
      }
    }
    

My coffeescript jsfiddle is at the following link

Click below to open the javascript fiddle.

(function() {
  var Item, Product, Purchase,
    bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };

  Purchase = (function() {
    function Purchase() {
      /* on $(document).ready a new Purchase is created */ 
      this.submit = $('#submit');
      /* for each div.product a new Product instance is created */
      this.products = $.map($('.product'), function(product, i) {
        return new Product(product);
      });
      / @onSubmit() */
      
      /* Comment 3) 
      My issue here is how to I access the this.items from the Purchase class and call serialize()?
      onSubmit: function () {
        @submit.click(function(){console.log(Product.serialize())};
      }     */
    }

    return Purchase;

  })();

  Product = (function() {
    Product.items = [];

    function Product(product) {
      this.product = $(product);
      this.id = this.product.data("id");
      this.submit = $('#submit');
      this.setEvent();
      this.onSubmit();
    }

    Product.prototype.setEvent = function() {
      return this.product.click((function(_this) {
        return function() {
          /* Comment 1)
             Product.items is a class variable of Product, because I need to access it from the Purchase class and send post request. When the user clicks on the $('submit') button*/
          Product.items.push(new Item(_this.id));
          return console.log(Product.items);
        };
      })(this));
    };

    Product.prototype.onSubmit = function() {
      return this.submit.click(function() {
      /* Comment 2) 
      This works as you can see, but we have 4 products and this operation will 
      be performed 4 times. I want to achieve this in the Purchase object so it is perfomed only once, by creating a sumit event handler in Purchase */      
        return console.log(Product.serialize());
      });
    };

    Product.serialize = function() {
      var item;
      return {
        items_attributes: (function() {
          var j, len, ref, results;
          ref = Product.items;
          results = [];
          for (j = 0, len = ref.length; j < len; j++) {
            item = ref[j];
            results.push(item.serialize());
          }
          return results;
        })()
      };
    };

    return Product;

  })();

  Item = (function() {
    function Item(product_id) {
      this.product_id = product_id;
      this.serialize = bind(this.serialize, this);
    }

    Item.prototype.serialize = function() {
      return {
        product_id: this.product_id.toString()
      };
    };

    return Item;

  })();

  $(document).ready(function() {
    return new Purchase();
  });

}).call(this);
.console {
  background-color: grey;
  color: white;
  height: 500px;
}      # I print to the console Product.items 

h4 {
  color: red;
  width: 100%;
  text-align: center;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<ul>
  <li class="product" data-id="1">Product 1</li>
  <li class="product" data-id="2">Product 2</li>
  <li class="product" data-id="3">Product 2</li>
  <li class="product" data-id="4">Product 3</li>
  <li class="product" data-id="5">Product 4</li>
  <div id="submit">Create Purchase</div>
</ul>

<h4>check logs by opening the console</h4>

as I write opensource, you can review my commit history, the specific commit and fork the project

Fabrizio Bertoglio
  • 5,890
  • 4
  • 16
  • 57
  • are you sure you want an object with string keys `"0"` and `"1"` instead of an array?. And where is the `@products.click` handler in your code? Did you mean `@submit.click` ? – caffeinated.tech Apr 19 '18 at 12:10
  • @Caffeinated.tech thanks a lot! I was thinking of doing something like `@products.click => new Item` inside the `Purchase` class and then save those instances inside a `class Purchase; @items = ...` so that when I do `@submit.click` I can perform the `AJAX` by passing all the `@items`. The format required by `rails params` is `items_attributes => {"0"..` etc.. but do you think I can use an array? Because It needs to be stringified and how is going to work with the format? thanks a lot – Fabrizio Bertoglio Apr 19 '18 at 16:07

3 Answers3

2

I'm a fan of the Active Model Serializer gem which is now a part of Rails. I would try expanding this pattern into your coffeescript by adding a serialize method to all of your classes, and call these when you pass data to your server.

I'm not sure on your plans for the Item class, so here is a simple mockup with the proposed serialize method:

class Item
  constructor: (@purchase, @product, @quantity) ->

  serialize: =>
    purchase_id: @purchase.id.toString()
    product_id: @product.id.toString()
    quantity: parseInt(@quantity)

Given that your purchase class will have an array of @items, then the Purchase's serialize method would look something like this:

serialize: =>
  items_attributes: (item.serialize() for item in @items)

And your ajax post would then use the serialize method:

$.ajax
   url: "/items"
   method: "POST"
   dataType: "json"
   data: 
     purchase: @serialize()
   error: (jqXHR, textStatus, errorThrown) ->
   success: (data, textStatus, jqXHR) ->

Then you should get a JSON post body of

'purchase' => {
  'items_attributes' => [
    {
      'purchase_id' => '1'
    },
    {
      'purchase_id' => '2'
    }
  ]
}

which you can use within your rails controller via strong params:

params.require(:purchase).permit(item_attributes: [:purchase_id])
caffeinated.tech
  • 6,428
  • 1
  • 21
  • 40
  • thanks for you answer. It helped me better understanding this problem, given my need to learn Javascript/Coffescript and to better understand this programming languages. I decided to post a bounty of 200 for this question (which I reviewed both for javascript and coffescript). I though of notifying you. Thanks a lot – Fabrizio Bertoglio Apr 22 '18 at 11:01
  • @FabrizioBertoglio You should post a new question regarding your data structure, rather than editing and replacing the original question. Now my answer won't be useful to anyone else coming along in future with a similar question and seems off – caffeinated.tech Apr 23 '18 at 12:46
  • I can still give you the bounty of 200, the coffescript version of the question is still available on JSFiddle https://jsfiddle.net/b1tqkh00/1179/ and I already implemented a solution to this problem as suggested from @Munim Munna. You can see my commit history which clearly explains what I implemented https://github.com/fabriziobertoglio1987/sprachspiel/commits/development the functionality works fine, but a better Coffescript solution can posted and rewarded. If you wish once the bounty is over I will remove the Javascript fiddle and replace it with the coffescript code – Fabrizio Bertoglio Apr 23 '18 at 16:18
  • this is the specific commit that Implemented the solution for this problem https://github.com/fabriziobertoglio1987/sprachspiel/commit/93a8819992d9a0397affbc623d895bf41ef4f95a From the dry point of view I like your question, I did commit the above solution before @Munim Munna gave me this solution, but he was kind to explain me that I could use `Product.items` inside the `Purchase` class, improving my code. I am in debt with both of you. – Fabrizio Bertoglio Apr 23 '18 at 16:21
2

You can simply bind the event inside your Purchase object when it is initialized.

this.submit.click(function() {
    return console.log(Product.serialize());
});

Working Snippet: I have commented out onSubmit of Product.

(function() {
  var Item, Product, Purchase,
    bind = function(fn, me) {
      return function() {
        return fn.apply(me, arguments);
      };
    };

  Purchase = (function() {
    function Purchase() {
      /* on $(document).ready a new Purchase is created */
      this.submit = $('#submit');
      /* for each div.product a new Product instance is created */
      this.products = $.map($('.product'), function(product, i) {
        return new Product(product);
      });
      / @onSubmit() */

      /* Comment 3) 
      My issue here is how to I access the this.items from the Purchase class and call serialize()?
      onSubmit: function () {
        @submit.click(function(){console.log(Product.serialize())};
      }     */
      this.submit.click(function() {
        return console.log(Product.serialize());
      });
    }

    return Purchase;

  })();

  Product = (function() {
    Product.items = [];

    function Product(product) {
      this.product = $(product);
      this.id = this.product.data("id");
      this.submit = $('#submit');
      this.setEvent();
      // this.onSubmit();
    }

    Product.prototype.setEvent = function() {
      return this.product.click((function(_this) {
        return function() {
          /* Comment 1)
             Product.items is a class variable of Product, because I need to access it from the Purchase class and send post request. When the user clicks on the $('submit') button*/
          Product.items.push(new Item(_this.id));
          return console.log(Product.items);
        };
      })(this));
    };

    // Product.prototype.onSubmit = function() {
    //   return this.submit.click(function() {
    //     /* Comment 2) 
    //     This works as you can see, but we have 4 products and this operation will 
    //     be performed 4 times. I want to achieve this in the Purchase object so it is perfomed only once, by creating a sumit event handler in Purchase */
    //     return console.log(Product.serialize());
    //   });
    // };

    Product.serialize = function() {
      var item;
      return {
        items_attributes: (function() {
          var j, len, ref, results;
          ref = Product.items;
          results = [];
          for (j = 0, len = ref.length; j < len; j++) {
            item = ref[j];
            results.push(item.serialize());
          }
          return results;
        })()
      };
    };

    return Product;

  })();

  Item = (function() {
    function Item(product_id) {
      this.product_id = product_id;
      this.serialize = bind(this.serialize, this);
    }

    Item.prototype.serialize = function() {
      return {
        product_id: this.product_id.toString()
      };
    };

    return Item;

  })();

  $(document).ready(function() {
    return new Purchase();
  });

}).call(this);
.console {
  background-color: grey;
  color: white;
  height: 500px;
}

h4 {
  color: red;
  width: 100%;
  text-align: center;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<ul>
  <li class="product" data-id="1">Product 1</li>
  <li class="product" data-id="2">Product 2</li>
  <li class="product" data-id="3">Product 2</li>
  <li class="product" data-id="4">Product 3</li>
  <li class="product" data-id="5">Product 4</li>
  <button type="button" id="submit">Create Purchase</button>
</ul>

<h4>check logs by opening the console</h4>
Munim Munna
  • 17,178
  • 6
  • 29
  • 58
  • ok, so I can call `Product.serialize()` inside the `Purchase` object ?! I did solve this https://github.com/fabriziobertoglio1987/sprachspiel/commit/93a8819992d9a0397affbc623d895bf41ef4f95a but saving the `items` as a `Purchase.items` class variable, but your solution is dry and better so I'll try this in the next days and let you know. I am still using a lot of @Caffeinated.tech solution so I need to understand who helped me more and give him the bounty.. Thanks a lot ! – Fabrizio Bertoglio Apr 22 '18 at 16:59
  • Yes you can access it because it is declared as a variable in the outer scope. Read [this](https://scotch.io/tutorials/understanding-scope-in-javascript) to better understand JavaScript scopes, understanding scopes will give you a head-start in learning JavaScript. – Munim Munna Apr 22 '18 at 17:17
  • yes. I believe you are right. That is the key to the solution, understanding how this objects are connected, while still I wonder how I could access the `@purchase` from the `@product` instance. Is also that about `scopes` or should I actually send as input the `@purchase` variable in the `new Product(@purchase)` constructor..? Thanks a lot .. this is key to my understanding of this issue – Fabrizio Bertoglio Apr 22 '18 at 18:19
  • If you have have only a single `purchase` object, then scope is OK, if you have multiple of them, then you need to link `products` with `purchase` just the way you are thinking, and `purchase` should contain a list of products. And if you click on the same item twice they are being listed twice, you need to avoid it. Try it and post updates to the question if you get stuck. – Munim Munna Apr 22 '18 at 18:32
1

I've updated your coffeescript fiddle to work as per the questions in the code comments.

Here is my updated version.

I've changed your class structure so that there is no need for any static variables, which seems like a hack to get around a bad design in this case.

You have created your model structure as:

  • one purchase has many products
  • one product has many items

But your post data format requirement shows that:

  • one purchase has many items
  • one item belongs to one product (by reference id)

To get around this inconsistency I flatten the serialized data from products so that items_attributes is an array of serialized item objects:

class Purchase
  ...
  serialize: =>
    items = (product.serialize() for product in @products)
    # flatten array of array of items:
    items_attributes: [].concat.apply([], items)

This cryptic looking line [].concat.apply([], items) is a shorthand for flattening one level deep of nested arrays (taken from this answer).

And each instance of product now saves an array of items on itself rather than statically on the class.

class Product  
  constructor: (product) ->
    @product = $(product)
    @id = @product.data("id")
    @submit = $('#submit')
    @items = []
    @registerEvents()

  addItem: =>
    console.log "added item #{@id}"
    @items.push new Item(@id) 

  registerEvents: ->
    @product.click @addItem

  serialize: =>
    (item.serialize() for item in @items)

I think a better redesign of this class structure would be to remove either the Product or Item class, as there is only a product id, and as far as I can tell, items are like a counter of how many units of a product are being purchased. Instead of having a class for this, you could keep an integer value on the product:

As a fiddle

class Purchase
  constructor: () -> 
    # on $(document).ready a new Purchase is created
    @submit = $('#submit')
    # for each div.product a new Product instance is created
    @products = $.map $('.product'), (product, i) -> 
      new Product(product)
    @registerEvents()

  onSubmit: => 
    console.log "send to server..."
    console.log JSON.stringify(@serialize(), null, 2)

  registerEvents: -> 
    @submit.click @onSubmit

  serialize: =>
    items_attributes: (product.serialize() for product in @products when product.amount isnt 0)

class Product  
  constructor: (product) ->
    @product = $(product)
    @id = @product.data("id")
    @submit = $('#submit')
    @amount = 0
    @registerEvents()

  addItem: =>
    console.log "added item #{@id}"
    @amount++

  registerEvents: ->
    @product.click @addItem

  serialize: =>
    product_id: @id
    amount: @amount

The output now looks different but is IMO cleaner:

new:

{
  "items_attributes": [
    {
      "product_id": 1,
      "amount": 1
    },
    {
      "product_id": 2,
      "amount": 3
    }
  ]
}

old:

{
  "items_attributes": [
    {
      "product_id": "1"
    },
    {
      "product_id": "2"
    },
    {
      "product_id": "2"
    },
    {
      "product_id": "2"
    }
  ]
}

But this may not work well with your current backend implementation, depending on how duplicates are currently handled, so disregard this last part if any legacy constraints cannot be changed.


Lastly, I wanted to add that this "object-oriented" method of attaching event listeners and logic to the DOM is a more structured way than typical jquery functions executed on load. But I've used it in the past, and keeping both DOM structure and code updated is a pain and often leads to bugs due to code changes in one not being mirrored on the other.

As an alternative, I would strongly suggest looking into reactjs or a similar DOM-abstraction type library. These allow you to strongly couple your logic to the view elements which they depend upon.

While usually used with JSX, it couples well with Coffeescript, but there are very few resources on this. Arkency write a good blog about react + coffeescript and I wrote a short post comparing coffeescript to jsx too.

caffeinated.tech
  • 6,428
  • 1
  • 21
  • 40