Warning: This is a long read as there is a lot of information pertaining to the problem in question, so prepare yourself, and thanks for taking the time to go through this if you do!
I'm in the process of writing a very large WebApp for a client and have built numerous interactive applications for this using jQuery, but one which I have developed has left me wondering: is there a standard way to do this, or perhaps just a better way?
This is where I come to you guys, to gain some insight on whether there is a certain way that this should be done, as I'm always looking to build optimal applications.
The Task I had to complete
The app I'm building requires the user to be able to build an invoice of products. This invoice can have any number of products added to it via a search box. Each product is pulled from the database and stored inside that search box, and each product can be added multiple times.
The way the user adds a product is by typing the name of the product in the search box, and pressing an Add Product button. This prepends a processed template to the list of products. Pretty simple right?
The Problem
The big problem I came across was that not only was the invoice able to contain a list of products, but each of these products contained varying customizable details. This meant that each product had a number of it's own fields on it, which the user could edit, which modified a number of things. For instance, one type of product has a quantity, which needs to be multiplied by the cost price of that product to return a total price. Another product has a length, which needs to be multiplied by the width, which then needs to be multiplied by the cost price of that product to return a total price.
As you can imagine, there are many different types of products with many different fields with many different calculations to be made, which, at the end of creation, need to be parsed to a PHP page whilst maintaining full modifiability where the user can add/remove/edit products on the same invoice page without jumping around.
This, to me, was a nightmare, as I needed to have a way to keep this clean and have a bunch of different inputs/jQuery objects not colliding with each other one way or another. I also had to differentiate loaded from newly created products, as some would have grabbed information from the database
The Solution I Built
So when a product is prepended to the list, the product contains a number of fields, a number of details, and a remove button. The first thing I did was make templates with replaceable tokens for each different type of product. That was easy and worked perfectly fine, and when a product was prepended to the list, to every element that was related to that product, I added a rel attribute to it (which I called a "hook") so that I could add handlers that only modified elements with that specific hook.
For the inputs I used input arrays, in the form of <input name="Products[%HOOK%][%PRODUCTDATAKEY%]" value="%PRODUCTDATAVALUE%" />
. Where my main problem arised was where I had to differentiate the products loaded that had already been saved in the database, from the newly added products that had just been added. The way I did this was by adding [Loaded] before all of the input name arrays, so that my PHP script would be able to deal with the [Loaded] array separately.
What is my concern?
My largest concern is how messy everything ended up looking, from the markup to the code. Hooks flying all over the place, Extremely long input names, A couple of thousand lines of code and a headache of different keys and tokens to be replaced. It works perfectly fine, it just feels horrible when I look at it as a developer. Take this "added product" for example:
<div class="item product special-order" rel="7" style="">
<input type="hidden" name="products[7][id]" value="17">
<input type="hidden" name="products[7][item_id]" value="">
<input type="hidden" name="products[7][type]" value="Special Order Flooring">
<input type="hidden" name="retail_price" class="invoice-base-cost" value="18.49">
<h4>Carpet Brand<a name="action" rel="7" class="btn btn-mini btn-danger remove-product initialized" style=""><i class="icon-remove"></i> Remove</a>
</h4>
<div class="item-details">
<div class="detail-column">
<div class="detail">
<span class="detail-label detail-label-medium"> Type:</span>Carpet
</div>
<div class="detail">
<span class="detail-label detail-label-medium">Range:</span> Carpet Brand
</div>
<div class="detail">
<span class="detail-label detail-label-medium">
Colour:</span><input required="" class="detail-input-large left-align" value="" type="text" name="products[7][colour]">
</div>
<div class="detail">
<span class="detail-label detail-label-medium">
Length:</span><input type="text" name="products[7][length]" rel="7" class="length-field initialized" placeholder="Enter the length..." value="1.000"><span class="detail-unit">m</span>
</div>
</div>
<div class="detail-column">
<div class="detail">
<span class="detail-label detail-label-large">Manufacturer:</span>
<select class="detail-input-large" required="" name="products[7][manufacturer_id]">
<option value="14">Manufacturer 1</option>
<option value="16">Manufacturer 2</option>
</select>
</div>
<div class="detail">
<span class="detail-label detail-label-large">Width supplied:</span><input type="text" name="products[7][width]" class="width-field" value="5.000"><span class="detail-unit">m</span>
</div>
<div class="detail">
<span class="detail-label detail-label-large">Retail price per/m:</span> £18.49
</div>
<div class="detail">
<span class="detail-label detail-label-large">Room:</span><input class="detail-input-large left-align" type="text" name="products[7][room]" value="" required="required">
</div>
</div>
<div class="item-cost">
<h5>Item cost</h5>
£<span class="invoice-cost" rel="7">92.45</span>
</div>
</div>
</div>
Is that not overkill? Could I have not mapped these to JSON objects to make it much cleaner somehow? Or is this the right way to do this?
That product had about 5 different event binds on it too, which was messy due to the fact that if the length field was changed, the price had to be recalculated by the width and the base price, and the same with the width just backwards, and this was all done by looking for fields with the same rel (hook)... It just looks like a mess to me. It'd be great to get an expert opinion on how this should have been approached.
I second the first two answers : use a framework meant to handle this.
Before going there however, here is a first step towards modularizing your code : from what you describe, you seem to have a list of .special-order
items, and each of these items has some life of its own.
Try to reflect this in your markup and your code :
1- markup : use meaningful class names to identify the items which will have an active role (you seem to already have : .special-order
, .length-field
, .remove-button
...). Be sure to have one single node which wraps your whole html "bundle" (in your case : .special-order
seems to play this role).
2- event hooks : use these class names to bind your hooks :
$('.remove-button').click(function(){ ... });
$('.length-field').change(function(){ ... });
3- If your items' list is dynamic (e.g, you can add/remove items through javascript without reloading the whole page), use event delegation to bind your events :
$('.special-order-list').on('click', '.remove-button', function(){ ... });
$('.special-order-list').on('change', '.length-field', function(){ ... });
4- Inside your handlers, follow the modularity represented in your markup. Two nodes are related if they are the children of the same .special-order
node. That way you can represent a relation between nodes, which does not depend on the explicit value of a "rel" field :
$('.special-order-list').on('change', '.length-field', function(){
var $masterNode = $(this).closest('.special-order');
var basePrice = $masterNode.find('.base-price').val();
var length = $(this).val();
$masterNode.find('.total-price').val( basePrice * length );
});
5- Obviously, use functions where appropriate :
function recomputePrice($masterNode){
var basePrice = $masterNode.find('.base-price').val();
var length = $masterNode.find('.length-field').val();
$masterNode.find('.total-price').val( basePrice * length );
});
$('.special-order-list').on('change', '.length-field, .base-price', function(){
var $masterNode = $(this).closest('.special-order');
recomputePrice( $masterNode );
});
Organizing your page into modules and submodules (some will call them components, or widgets ...) is considered a "best practice" for page layout. Is is mentionned in SmaCSS, and it underlies several css/js frameworks, such as Bootstrap or many others.
As spez86 suggested; you should make use of framework to generate your HTML as well as to bind events to the elements present within your each HTML section. I will suggest you to take a look at backbonejs and handlebarsjs. When you use these 2 in combination, you need put minimal effort to achieve your goal.
You can refer following URL to know more about their usage.
I will recommend you to first work on UI, i.e. create separate templates for your different-different kind of product and then on your Add Product button click try appending backbone view to your product list container.
Once you are able to successfully add HTML onto page then you can proceed with binding of event to child element present within product template. This can be done inside Backbone.View class's events
function. The event handler registered here will only gets attached to elements present within this View and not to any other element present within page DOM having same CSS class. So you will get rid of [rel]
mechanism and also from long input names. Hope this will help you a bit.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With