Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

addEventListener and `this` not working as expected

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:

  1. 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.

  2. 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?

like image 482
RBarryYoung Avatar asked Jan 08 '23 11:01

RBarryYoung


1 Answers

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>
like image 80
altocumulus Avatar answered Jan 10 '23 01:01

altocumulus