Lesson 11 - Finishing the table editor in JavaScript
In the previous lesson, Table editor in JavaScript, we created the core of a table editor. In today's JavaScript tutorial, we're going to complete this web application.
Active cell's row index
In order to practice orienting in DOM variables, we'll implement 2 functions
which we're going to use later. One of them is an
activeCellRowIndex()
function, which returns the index of the row
(zero-based) in which the user's selected cell is located
(activeCell
). First, you need to realize where we get this index.
We'll look between the rows of the table. The table is represented by the
<table>
element and the rows are right in it (cells are in
rows, which can be sometimes confusing, keep it in mind).
We're going to look into: table.childNodes
And to define what we're looking for will be a little more complicated. We're
looking for a row, but we know that activeCell
is not the
<td>
element but <input>
which is in
<td>
. To access <td>
, we need to get the
parentElement
out of the cell and even that's not enough for
us.
We don't need the cell, but the row, and we'll get it using
parentElement
once more.
We're looking for:
activeCell.parentElement.parentElement
NodeList
and
indexOf()
If you remember, in the previous lesson I mentioned that DOM manipulating
functions never return an array, but a NodeList
or similar
specialized collections. And that these objects are only (practically) simpler
arrays. Unfortunately, NodeList
has no indexOf()
method which would return the element's index. The're 2 solutions. The first is
to iterate through it by ourselves and the second is to use the
indexOf()
method on the Array
object and call it
within the NodeList
context. You'll understand the second part of
the previous sentence more clearly in the object-oriented programming course.
For now, we just need to know that we'll call indexOf()
on
Array
, but we'll "smuggle" a NodeList
to it. This
works because JavaScript doesn't care what it's working with as long it provides
the required interface which NodeList
does. When we want to call a
method with a different context, we'll find it in the object's prototype (in our
case it's Array.prototype
) and we call it using the
call()
method. As the first parameter, we pass the new context and
then we pass the method's parameters.
The resulting code could look like this:
function activeCellRowIndex() { let searchLocation = table.childNodes; let elementToFind = activeCell.parentElement.parentElement; return Array.prototype.indexOf.call(searchLocation, elementToFind); }
In searchLocation
we have in what we're searching. In
elementToFind
we have what we're looking for.
searchLocation
is a NodeList
and
elementToFind
is the <tr>
element. Then we
return the value returned by indexOf()
with the context changed to
the NodeList
and with the <tr>
element passed as
it's regular parameter.
Active cell's column index
It'll be a little easier to get the selected cell's column index. We need to
get to the row where the cell is located and get its position. Again, we'll get
the cell using parentElement
of the activeCell
and the
row that it belongs in using parentElement.parentElement
.
function activeCellColumnIndex() { let rowCells = activeCell.parentElement.parentElement.childNodes; let td = activeCell.parentElement; return Array.prototype.indexOf.call(rowCells, td); }
It may have been a bit boring so far, because nothing interesting happened. Now it's going to be more enjoyable. Finally, we're going to start programming the application control functions.
Inserting rows above
The method that creates a new row (the <tr>
element) is
already done. We can get the index of the row in which the selected cell is
located, and we can also create a new row. For the sake of clarity, we'll store
a new row into a variable, the row's index as well, and again we'll start
manipulating DOM. The rows (<tr>
) are nested directly in the
table element (<table>
), so it's going to be easy. We'll call
the insertBefore()
method on the table. The first parameter is
clear, it's the new row. The second parameter must be the
<tr>
in which the selected cell is located.
There's no need to go further, rows are nested straight in the table,
therefore we'll just select the row from the NodeList
according to
the active cells's row index.
function insertRowAbove() { let row = createRow(); let activeIndex = activeCellRowIndex(); table.insertBefore(row, table.childNodes[activeIndex]); }
In the function that creates the control buttons, we'll add the
onclick
event handler to the "Insert row above" button.
addButton("Insert row above", document.body).onclick = insertRowAbove;
Now we're going to go a little deeper, however, it still won't be anything we haven't done yet.
Inserting rows below
Here we'd proceed practically the same. Unfortunatelly, there's no
insertAfter()
method in JavaScript so we'll have to insert the row
before the current + 1 to get it at the right place. We should remember we need
to prevent inserting the row this way after the last one since there would be no
current + 1 row there. We'll use a condition to check whether the last element
of the table (table.lastChild
) is the row containing the selected
cell. If it is, we just append the new row using the appendChild()
method. Otherwise, we'll insert the row using insertBefore()
before
the row after the one containing the selected cell.
function insertRowBelow() { let row = createRow(); let selectedIndex = activeCellRowIndex(); if (table.lastChild == table.childNodes[selectedIndex]) { table.appendChild(row) } else { table.insertBefore(row, table.childNodes[selectedIndex + 1]) } }
Set the event handler to the appropriate button:
addButton("Insert row below", document.body).onclick = insertRowBelow;
Now we can add rows to the table as we want.
Inserting columns to the left
Adding columns will be a little bit more complicated. Tables in HTML contain rows with cells in them. Hey, there're no columns in the code! We're going to have to add a column by adding a cell to each of the existing rows. We'll iterate through all the table rows and insert a new cell to each row before the index of the selected cell.
Now it's time to realize what is where and what we'll need to select. We'll
insert elements into <tr>
of the <table>
element. The table.childNodes
expression contains an array of
<tr>
elements and we need to use a loop to insert a new cell
into each of them.
We're going to insert into:
table.childNodes[i]
We're inserting a new cell.
We're inserting: createCell()
And before what are we inserting? The referential element (the one before
which we're inserting) must be inside the element we're inserting into. We
simply start by passing the same element we're inserting into. Since we know
it's a child element, we're going to find it in the NodeList
of the
childNodes
and we know these childNodes
are an
<td>
array (Hooray! We're looking for these!). So the index
(which is constant for the loop) is going to be the cell index in the rows. If
you got lost in the description, try going through the code below.
We're inserting before:
table.childNodes[i].childNodes[selectedIndex]
It'll all look as following:
function insertColumnLeft() { let selectedIndex = activeCellColumnIndex(); for (let i = 0; i < table.childNodes.length; i++) { table.childNodes[i].insertBefore(createCell(), table.childNodes[i].childNodes[selectedIndex]); } }
If you find it complicated or confusing, before you go into the next section (because it's going to be even more complex), add appropriate comments to your code or split the code into multiple variables with clear descriptive names.
Inserting columns to the right
As you probably know, it'll be similar, but we have to bypass the missing
insertAfter()
method again as we did in the
insertRowBelow()
method. The only difference (again) is that we
have to check whether we're not inserting at the end. Logically, we'll always
end up checking the element we're inserting with the last element of the parent
of the element we're inserting.
function insertColumnRight() { let selectedIndex = activeCellColumnIndex(); for (let i = 0; i < table.childNodes.length; i++) { if (table.childNodes[i].childNodes[selectedIndex] == table.childNodes[i].lastElementChild) { table.childNodes[i].appendChild(createCell()); } else { table.childNodes[i].insertBefore(createCell(), table.childNodes[i].childNodes[selectedIndex + 1]); } } }
Let's add event handlers to the buttons and try it out.
addButton("Insert column to left", document.body).onclick = insertColumnLeft; addButton("Insert column to right", document.body).onclick = insertColumnRight;
Try it out. After hard work, you can play the Gumoku game in your table. Simply insert a lot of columns and rows
We may encounter a problem while using the application. The table may have more rows or columns than we wanted. We'll implement removing columns and rows.
Removing rows
We'll remove a row from the table. We'll get the appropriate element to
remove by taking all elements of the table (childNodes
) and
selecting the one with the index of the selected cell.
function removeRow() { let selectedIndex = activeCellRowIndex(); table.removeChild(table.childNodes[selectedIndex]); }
Removing columns
We'll remove a column by removing a cell in each row. The cell's index will be the index of the selected cell.
function removeColumn() { let selectedIndex = activeCellColumnIndex(); for (let i = 0; i < table.childNodes.length; i++) { table.childNodes[i].removeChild(table.childNodes[i].childNodes[selectedIndex]); } }
We'll make the buttons work and we're done.
addButton("Remove row", document.body).onclick = removeRow; addButton("Remove column", document.body).onclick = removeColumn;
After this example, you should already be able to work with DOM in
JavaScript. All the magic from creating the table to the Gumoku game was just
playing with a few elements that we'd normally write in HTML. Notice that at the
beginning of everything, we have HTML code that does not have a single element
in its <body>
.
In the next lesson, More on Conditions in JavaScript, we'll work with images.
In the next lesson, More on Conditions in JavaScript, we will deepen our knowledge of data types and show other constructs for creating conditions.
Did you have a problem with anything? Download the sample application below and compare it with your project, you will find the error easily.
Download
By downloading the following file, you agree to the license terms
Downloaded 31x (2.19 kB)
Application includes source codes in language JavaScript