Reputation: 5942
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.
Purchase
, Product
and Item
Purchase
has many Products
, One Product
has many Items
Purchase
object sets a click event handler
on the $('submit')
and onClick()
will post the items
data to my backend apiThis 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
Upvotes: 2
Views: 344
Reputation: 6548
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:
But your post data format requirement shows that:
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.
Upvotes: 1
Reputation: 17556
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>
Upvotes: 2
Reputation: 6548
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])
Upvotes: 2