I am an experienced programmer, but somewhat new to web programming. I am trying to learn Javascript, HTML5 and SVG using VS2010 by writing an HTML page that plays Tic-Tac-Toe with Javascript.
I am successfully creating each of the nine squares as SVG <rect...>
elements, however I am having trouble with the click event handlers for each square.
Here's the base SVG elements as they exist in the HTML file:
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"
id="svgTTT" width="150" height="150" viewBox="0 0 300 300" >
<rect width="3" height="300" x="99" fill="#008d46" />
<rect width="3" height="300" x="199" fill="#000000" />
<rect width="300" height="3" y="99" fill="#008d46" />
<rect width="300" height="3" y="199" fill="#d2232c" />
</svg>
These static <rect>
elements draw the for cross-hash lines of the TicTacToe board. The nine board squares are created in a javascript function called from the windows load event (below).
Here's the javascript (in-line in a <script>
element in the HTML body):
<script type="text/javascript">
function getEl(s) { return document.getElementById(s); }
var svg; // get the TTT svg
// execute after HTML has rendered
window.onload = function () {
svg = getEl("svgTTT");
tttSetupSquares(svg);
alert("click-squares are setup.");
}
var cells = new Array(3);
// setup the click squares
function tttSetupSquares(brd) {
for (var i = 0; i < 3; i++) {
cells[i] = new Array(3);
for (var j = 0; j < 3; j++) {
var x = 3 + (i * 100);
var y = 3 + (j * 100);
var newId = "svgTTTsqr" + i.toString() + j.toString();
// add in new rect with html
brd.innerHTML += "<rect id='"+newId+"' width='93' height='93' "
+ "fill='#00DD00' x='" + x.toString() + "' y ='" + y.toString() + "'"
+ " />";
//find it using the newId
var rect = document.getElementById(newId);
//make a cell object to contain all of this
var cell = {
col: i,
row: j,
pSvg: svg,
rect: rect,
handleClick: function (event) {
try {
// this line fails because `this` is the target, not the cell
var svgNS = this.pSvg.namespaceURI;
var line = document.createElementNS(svgNS, 'line');
this.pSvg.appendChild(line);
}
catch (err) {
alert("handlClick err: " + err);
}
}
}
//add a click handler for the rect/cell
cell.rect.addEventListener("click", cell.handleClick, false);
//(only seems to work the last time it is executed)
//save the cell object for later use
cells[i][j] = cell;
}
}
}
</script>
(I can supply the full page source, but it's just the HTML elements that contain these.)
The problem is two-fold:
Only the last addEventListener
seems to work. Clicking on all of the other squares does nothing. Clicking on the last square (svgTTTsqr22) does run the cell.handleClick
but leads to problem 2 (below). Chrome Developer Tools (F12) shows all of the <rect>
elements except the last as having no event listener.
When the cell.handleClick
does run, it fails on the fist line (var svgNS = this.pSvg.namespaceURI;
) with an error like "undefined object does not have a property named "namespaceURI" Inspection in Devleoper Tools shows that it is failing because this
is not set to the cell
object but rather to the SVG <rect>
element that was clicked.
So my questions are:
A. What am I doing wrong here, and
B. How can/should I be doing this?
1. Missing event handlers
Using innerHTML
to alter an element's internal structure will cause all of its children to be removed and the element's DOM sub-tree to be rebuilt by re-parsing the HTML content. By removing the child elements all previously registered event listener will get lost and will not automatically be restored when rebuilding the DOM from HTML. To circumvent this behaviour it's good practice to avoid innerHTML
, if possible, and use direct DOM manipulation instead. You could use something like this to insert your <rect>
s:
// Use DOM manipulation instead of innerHTML
var rect = document.createElementNS(svg.namespaceURI, 'rect');
rect.setAttributeNS(null, "id", newId);
rect.setAttributeNS(null, "fill", "#00DD00");
rect.setAttributeNS(null, "width", "93");
rect.setAttributeNS(null, "height", "93");
rect.setAttributeNS(null, "x", x);
rect.setAttributeNS(null, "y", y);
svg.appendChild(rect);
2. this
context inside of event handlers
Whenever an event listener gets called this
will get bound to the element where the event got triggered by. In your code, however, you won't need this
because all information is available by the parameter brd
which gets passed in to function .tttSetupSquares()
.
handleClick: function (event) {
try {
var svgNS = brd.namespaceURI;
var line = document.createElementNS(svgNS, 'line');
brd.appendChild(line);
}
catch (err) {
alert("handlClick err: " + err);
}
}
See the following snippet for a working example:
function getEl(s) { return document.getElementById(s); }
var svg; // get the TTT svg
var cells = new Array(3);
// execute after HTML has rendered
!(function () {
svg = getEl("svgTTT");
tttSetupSquares(svg);
alert("click-squares are setup.");
}());
// setup the click squares
function tttSetupSquares(brd) {
for (var i = 0; i < 3; i++) {
cells[i] = new Array(3);
for (var j = 0; j < 3; j++) {
var x = 3 + (i * 100);
var y = 3 + (j * 100);
var newId = "svgTTTsqr" + i.toString() + j.toString();
// Use DOM manipulation instead of innerHTML
var rect = document.createElementNS(svg.namespaceURI, 'rect');
rect.setAttributeNS(null, "id", newId);
rect.setAttributeNS(null, "fill", "#00DD00");
rect.setAttributeNS(null, "width", "93");
rect.setAttributeNS(null, "height", "93");
rect.setAttributeNS(null, "x", x);
rect.setAttributeNS(null, "y", y);
svg.appendChild(rect);
//make a cell object to contain all of this
var cell = {
col: i,
row: j,
pSvg: brd,
rect: rect,
handleClick: function (event) {
try {
//console.log(this);
var svgNS = brd.namespaceURI;
var line = document.createElementNS(svgNS, 'line');
line.setAttributeNS(null, "x1", this.x.baseVal.value);
line.setAttributeNS(null, "y1", this.y.baseVal.value);
line.setAttributeNS(null, "x2", this.x.baseVal.value + this.width.baseVal.value);
line.setAttributeNS(null, "y2", this.y.baseVal.value + this.height.baseVal.value);
brd.appendChild(line);
}
catch (err) {
alert("handlClick err: " + err);
}
}
}
//add a click handler for the rect/cell
cell.rect.addEventListener("click", cell.handleClick, false);
//save the cell object for later use
cells[i][j] = cell;
}
}
}
line {
stroke: red;
}
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"
id="svgTTT" width="150" height="150" viewBox="0 0 300 300" >
<rect width="3" height="300" x="99" fill="#008d46" />
<rect width="3" height="300" x="199" fill="#000000" />
<rect width="300" height="3" y="99" fill="#008d46" />
<rect width="300" height="3" y="199" fill="#d2232c" />
</svg>
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