4
\$\begingroup\$

var addToTeam = document.getElementsByClassName('addToTeam'),
    parentElement = document.getElementsByClassName('playerTypeBlock'),
    playerName = document.getElementsByClassName('heading'),
    totalVal = document.getElementById('totalvalue'),
    tableBody = document.querySelector('.totalPlayers');

var updatePlayers = function (evt) {

    evt.preventDefault();

    var amount = totalVal.innerHTML,
        productId = this.parentElement.getAttribute('data-player-type-id'),
        productPrice = this.parentElement.getAttribute('data-player-value'),
        playerName = this.parentNode.querySelector('.heading').innerHTML,
        playerId = $(this).parent().data('playerTypeId'),
        updateTotal = parseFloat(amount) + parseFloat(productPrice);

    var quantity = parseInt($(this).data('click'), 10) || 0;
    quantity++;
    $(this).data('click', quantity);

    totalVal.innerHTML = Math.round(updateTotal * 100) / 100;

    if (quantity > 1) {
            $('table .totalPlayers').find('#player-' + playerId + ' .quantity').text(quantity);

    }
    else {
        $('table .totalPlayers').append('<tr id="player-' + playerId + '"><td>' + playerName + '</td><td class="quantity">' + quantity + ' </td></tr>');
    }

};

for (var i = 0; i < addToTeam.length; i++) {
    addToTeam[i].addEventListener('click', updatePlayers);
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<div class="playerTypeBlock" data-player-type-id="1" data-player-value="10.00">
    <div class="heading">Ninja</div>
    <div class="price">$10</div> <a class="addToTeam" href="#">Add player</a>

</div>
<hr>
<div class="playerTypeBlock" data-player-type-id="2" data-player-value="20.25">
    <div class="heading">Samurai</div>
    <div class="price">$20.25</div> <a class="addToTeam" href="#">Add player</a>

</div>
<hr>
<table class="teams" border="1">
    <thead>
        <tr>
            <th>Players</th>
            <th>Quantity</th>
        </tr>
    </thead>
    <tbody class="totalPlayers"></tbody>
</table>
<div class="total"> 
 <span>Total: $</span>
 <span class="amount" id="totalvalue">0.00</span>
</div>

It looks like any additional functionality I want to add would have to go through updatePlayers function like for instance if I wanted to remove a player that removePlayer function would need to be inside the updatePlayers function too and If I had a hundred players on the page I would be attaching event listeners to each of them.

Although this works fine, I want to know if there are any OOP techniques I can use to make my code more scalable and extensible?

SEE DEMO

\$\endgroup\$
3
  • 4
    \$\begingroup\$ It is recommended to have the title of your post be the function of your code and have all improvement desires in the body of your post. \$\endgroup\$
    – SirPython
    Commented Jan 14, 2015 at 22:57
  • \$\begingroup\$ Why are you picking up the data from DOM? Where did you get that HTML from? Server-side? \$\endgroup\$
    – Pavlo
    Commented Jan 27, 2015 at 21:52
  • \$\begingroup\$ Not getting the data from server-side, I'll have to pick it up from the DOM \$\endgroup\$
    – zadubz
    Commented Jan 28, 2015 at 21:31

1 Answer 1

2
+50
\$\begingroup\$

The main problem is to mix javascript objects with DOM elements. It's conceptual wrong to do this. However this has some advantages, first of all the execution speed.

The alternative, indeed, is to calculate everything with pure javascript and, when something change, rewrite all the DOM elements. This solution, when the data are very large and the elements change too frequently, could be not optimal.

Said this, according with your approach, I try to separate as much possible the DOM with the Javascript with some class/method bridge between them.

First rule: works always with data-* when search values on DOM. Because the css class could be change during a refactory by other developers (after several years maybe). It's also possibile that what you want see don't match what you want manipulate. For example: the quantity of Ninjas could be "1000" and you would write "1k". So it's better to store this value in a data-attribute.


Premise: This is only a point to start.

Here we go.

For link a DOM element with a Javascript Object I've added a data attribute data-object

<div class="playerTypeBlock" data-player-type-id="1" data-player-value="10.00" data-object="player">

and of course here

<div class="playerTypeBlock" data-player-type-id="2" data-player-value="20.25" data-object="player">

and here

<tbody class="totalPlayers" data-object="playerList"></tbody>

Now we have the object Player

function Player(id, name, value) {
    this.id = id;
    this.name = name;
    this.value = value;
}

We need a Factory of players starting from DOM. Because, if something change on your HTML you should modify only this method:

Player.getPlayerFromDOM = function (domElement) {
    var $dom = $(domElement);
    var playerType = $dom.attr("data-player-type-id");
    var player = {
        id    : +$dom.attr("data-player-type-id"),
        value : +$dom.attr("data-player-value"),
        name  : $dom.find('.heading').text() 
    }
    return new Player(player.id, player.name, player.value);
}

Another thing to do is to write a function that fetch the schema of players (how much cost a ninja? How much a samurai?) I think that the better solution is to inject a json with this values, but I don't know if you want/can. So fetching it by DOM.

Player.getSchema= function() {
    var players = {};
    $('[data-object="player"]').each(function() {
        var player =  Player.getPlayerFromDOM(this);
        players[player.id] = player;
    });
    return players;
} 

For players we have all. There is now the player list:

function PlayersList($table) {
    var $tbody = $table.find("totalPlayers");
    // be sure if there is the totalPlayers class
    if($tbody.length == 0) {
        $tbody = $table.append($('<tbody/>').addClass("totalPlayers"));
    }
    this.$playersContainer = $tbody;
    this.$table = $table;
}

PlayersList.prototype.addRow = function(cells) {
    var $tr = $('<tr/>');
    for(var i = 0, clen = cells.length; i < clen; i++) {
        var $td = $('<td/>');
        if(cells[i].hasOwnProperty('className')) {
            $td.addClass(cells[i].className);
        }
        if(cells[i].hasOwnProperty('data')) {
            for(var dataAttr in cells[i].data) {
                $td.attr("data-"+dataAttr, cells[i].data[dataAttr]);
            }
        }
        $tr.append($td.html(cells[i].value))
    }
    this.$playersContainer.append($tr);
    return $tr;
}

It's for a simple table managment. Method AddRow add a row with some class or some data-attribute. It could be a simple method of a superclass more generic for table management. So, when a player is added, the table must be updated with new values.

PlayersList.prototype.addPlayer = function(player) {
    var $table = this.$table;
    var $row = $table.find("tr[data-player-type-id='"+player.id+"']");
    if($row.length === 0) {
        $row = this.addRow(
            [
                {value: player.name},
                {value: 1, className: 'quantity', data:{quantity:1}}
            ]
        ).attr("data-player-type-id", player.id);
    }else {
        var newValue = +$row.find("[data-quantity]").attr("data-quantity")+1;
        $row.find("[data-quantity]").attr("data-quantity", newValue).text(newValue);
    }
    this.$table.trigger('ninja.table-updated')
}

Nothing of special. Just note the trigger of custom event ninja.table-updated that notice the listeners that something is changed (in this case, we will add a listener for total calculation).

For totalcalculation, let's create a method with compute the total with a given schema (maybe the schema could change, or could depends by other things)

PlayersList.prototype.calcTotal = function(schema) {
    var total = 0;
    this.$table.find('tbody').find("tr").each(function() {
        var playerId = $(this).attr("data-player-type-id")
        total += schema[playerId].value*$(this).find("[data-quantity]").text();
    });
    return total;
}

So we have all that we need:

var playersSchema = Player.getSchema();
var $teams = $('.teams');
var playerTable = new PlayersList($teams); 


$('body').on('click', '.addToTeam', function() {
    var player = Player.getPlayerFromDOM($(this).parents('[data-player-type-id]'));
    playerTable.addPlayer(player);
});

$teams.on('ninja.table-updated', function() {
    $('#totalvalue').text(playerTable.calcTotal(playersSchema));
});

function Player(id, name, value) {
	this.id = id;
	this.name = name;
	this.value = value;
}

Player.getPlayerFromDOM = function (domElement) {

	var $dom = $(domElement);
	var playerType = $dom.attr("data-player-type-id");
	var player = {
		id    : +$dom.attr("data-player-type-id"),
		value : +$dom.attr("data-player-value"),
		name  : $dom.find('.heading').text() 
	}
	return new Player(player.id, player.name, player.value);
}

Player.getSchema= function() {
	var players = {};
	$('[data-object="player"]').each(function() {

		var player =  Player.getPlayerFromDOM(this);
		players[player.id] = player;
	});
	return players;
} 
function PlayersList($table) {
	var $tbody = $table.find("totalPlayers");
	// be sure if there is the totalPlayers class
	if($tbody.length == 0) {
		$tbody = $table.append($('<tbody/>').addClass("totalPlayers"));
	}
	this.$playersContainer = $tbody;
	this.$table = $table;
}

PlayersList.prototype.addPlayer = function(player) {
	var $table = this.$table;
	var $row = $table.find("tr[data-player-type-id='"+player.id+"']");
	if($row.length === 0) {
		$row = this.addRow(
			[
				{value: player.name},
				{value: 1, className: 'quantity', data:{quantity:1}}
			]
		).attr("data-player-type-id", player.id);
	}else {
		var newValue = +$row.find("[data-quantity]").attr("data-quantity")+1;
		$row.find("[data-quantity]").attr("data-quantity", newValue).text(newValue);
	}
	this.$table.trigger('ninja.table-updated')
}

PlayersList.prototype.addRow = function(cells) {
	var $tr = $('<tr/>');
	for(var i = 0, clen = cells.length; i < clen; i++) {
		var $td = $('<td/>');
		if(cells[i].hasOwnProperty('className')) {
			$td.addClass(cells[i].className);
		}
		if(cells[i].hasOwnProperty('data')) {
			for(var dataAttr in cells[i].data) {
				$td.attr("data-"+dataAttr, cells[i].data[dataAttr]);
			}
		}
		$tr.append($td.html(cells[i].value))
	}
	this.$playersContainer.append($tr);
	return $tr;
}

PlayersList.prototype.calcTotal = function(schema) {
	var total = 0;
	this.$table.find('tbody').find("tr").each(function() {
		var playerId = $(this).attr("data-player-type-id")
		total += schema[playerId].value*$(this).find("[data-quantity]").text();
	});
	return total;
}
var playersSchema = Player.getSchema();
var $teams = $('.teams');
var playerTable = new PlayersList($teams); 


$('body').on('click', '.addToTeam', function() {
	var player = Player.getPlayerFromDOM($(this).parents('[data-player-type-id]'));
	playerTable.addPlayer(player);
});

$teams.on('ninja.table-updated', function() {
	$('#totalvalue').text(playerTable.calcTotal(playersSchema));
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="playerTypeBlock" data-player-type-id="1" data-player-value="10.00" data-object="player">
    <div class="heading">Ninja</div>
    <div class="price">$10</div> <a class="addToTeam" href="#">Add player</a>

</div>
<hr>
<div class="playerTypeBlock" data-player-type-id="2" data-player-value="20.25" data-object="player">
    <div class="heading">Samurai</div>
    <div class="price">$20.25</div> <a class="addToTeam" href="#">Add player</a>

</div>
<hr>
<table class="teams" border="1">
    <thead>
        <tr>
            <th>Players</th>
            <th>Quantity</th>
        </tr>
    </thead>
    <tbody class="totalPlayers" data-object="playerList"></tbody>
</table>
<div class="total"> 
 <span>Total: $</span>
 <span class="amount" id="totalvalue">0.00</span>
</div>

\$\endgroup\$
1
  • \$\begingroup\$ This is great! I'm going to try adding a remove player function and update the total \$\endgroup\$
    – zadubz
    Commented Feb 5, 2015 at 12:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.