Lesson 10 - Table editor in JavaScript
In the previous lesson, DOM manipulation in JavaScript, we learned how to work with DOM and change the webpage contents. We already know everything we need to create a true web application. In today's tutorial, we're going to program a table editor as an example on which we're going to learn a few new things.
The table is a very interesting and specific website element. The table has
rows (<tr>
) and cells (<td>
).
<td>
belong to <tr>
and
<tr>
belong to <table>
if the table
doesn't have a more complex structure.
HTML
Let's create a page, a CSS file, and a JS file. We'll import the CSS and JS
files in the <head>
and our HTML page is done. We can leave
the <body>
element empty since we're going to generate all
the content from the script. This approach is even better for us because if we
decide to put the application on a different page, we won't have to modify its
HTML code at all. Since we're going to work on this project, let's make our
codes match. The HTML page should look like this. Notice the filenames.
<!DOCTYPE html> <html lang="en"> <head> <title>Table editor</title> <meta charset="utf-8" /> <script src="table-editor.js"></script> <link href="table-editor.css" rel="stylesheet" /> </head> <body> </body> </html>
JavaScript
We'll create three variables in the table-editor.js
file. The
table
variable will contain the table itself and then we'll have
the variables defaultSizeX
and defaultSizeY
for the
default table size. We'll leave table
without a value and set some
2 reasonable numbers to the size variables. For example:
let table; let defaultSizeX = 5; let defaultSizeY = 3;
Later, we're going to need one more variable, but let's not bother with it now. We'll create functions to generate control buttons and the table itself. We'll create the function to generatel control buttons later.
The function to generate the table will be named
generateDefaultTable()
. It'll create a <table>
element and append it to the <body>
. We'll generate its cells
using 2 nested loops. To make the cells editable, we'll insert classic text
<input>
s into them. Because we're going to need to create
cells multiple times in this application, we'll create a
createCell()
function to do so. It'll create the
<td>
and <input>
elements. It'll insert
the <input>
into the <td>
and return the
cell. Since we're going to program functions which assume there's a selected
cell in the table, we'll store the selected cell somewhere when it's selected.
There's the focus
event which is triggered when the user selects an
element, no matter if by clicking on it or using Tab. Therefore, when
creating a cell, we'll handle its focus
event as well and store the
cell to the activeCell
variable (declared above the function) if
the cell is selected. It's important to remember that the this
keyword will contain the element which triggered the event in the event's
callback. So we'll store this
to the activeCell
variable (it'll contain the <input>
element because this
element will trigger the event).
let activeCell; function createCell() { let td = document.createElement("td"); let tdInput = document.createElement("input"); tdInput.type = "text"; tdInput.onfocus = function () { activeCell = this; } td.appendChild(tdInput); return td; }
Let's get back to the function that generates the default table. First, we're going to create the table itself.
function generateDefaultTable() { table = document.createElement("table"); document.body.appendChild(table); }
We'll store the newly created <table>
to the
table
variable and append it into the <body>
element. Note that if we want to get to <body>
, we don't have
to get use methods such as getElementsByTagName()
, but the element
is stored in the document.body
property.
Now, let's focus on the loops. First, we'll create the
<tr>
elements (the Y
loop) and append each new
row to the table (it's always better to append the element as soon as we can to
avoid forgetting to do it later). Then we'll create the row cells using a nested
loop and append them to the row. We'll use the createCell()
method
which we've created just a while ago. We'll set the upper bounds of the loops to
the defaultSizeX
and defaultSizeY
variables so the
correct number of rows and cells is generated.
function generateDefaultTable() { table = document.createElement("table"); document.body.appendChild(table); for (let y = 0; y < defaultSizeY; y++) { let tr = document.createElement("tr"); table.appendChild(tr); for (let x = 0; x < defaultSizeX; x++) { tr.appendChild(createCell()); } } }
The core of the app is done now. Let's call the function in the
load
event handler of the window
object:
window.onload = function () { generateDefaultTable(); }
This kind of event handling should already be well known to you. It's an anonymous function assigned to the event handler of the event which is triggered when the page is loaded. When you start the app, you'll see a table (probably with no borders) and the inputs in it.
CSS
Congratulations, you created your first application that generated a table. Before we proceed to the next part, let's polish the table a little bit to make it look better. I won't describe the following CSS. If you don't understand any part of it, check our manuals for a detailed explanation.
table { border-spacing:0; border: 1px solid black; width: 500px; } table, table td { padding:0; margin: 0; } table td { border: 1px solid black; } table td input { padding:0; margin: 0; width: 100%; border: 0px solid transparent; height: 100%; }
The table will look like this:
Adding features
We'll now implement features to our editor. We're going to do it step by step. To make our users able to use the features, we'll have to add some controls for them. Actually, I wanted to say we're going to need buttons , but theoretically, we could use some other control elements as well.
Because there is going to be multiple features, it'd make no sense to write
...createElement("button")
all over again. Therefore, we'll create
the addButton()
function for it. The function will accept 2
parameters - label
and parent
. It'll create a
<button>
, set its text, append it to the parent and return
it. We only return the button to be able to set its event handler.
Theoretically, we could pass the callback (the handler) through the third
parameter, however, the source code might become confusing.
function addButton(label, parent) { let button = document.createElement("button"); button.textContent = label; parent.appendChild(button); return button; }
Now, we'll create concrete buttons for each feature. The goal of this project
is to program the functions to insert a column and a row according to the
selected cell (we have it stored in the activeCell
variable) and to
remove a column and a row. We'll call the method to create a button in the
function. We don't even have to store the buttons since we can assign the event
handler straight to the return value. For now, we'll keep the buttons as
follows:
function generateControlButtons() { addButton("Insert row below", document.body); addButton("Insert row above", document.body); addButton("Insert column to left", document.body); addButton("Insert column to right", document.body); addButton("Remove row", document.body); addButton("Remove column", document.body); }
We'll call the function in the window.onload
event handler:
window.onload = function () { generateControlButtons(); generateDefaultTable(); }
Adding rows
Let's start with the simplest feature - adding a row. Because we'll have two
editor features which somehow create a row and insert it somewhere, we'll write
a function that creates the <tr>
element and inserts as many
cells into it as is in some of the existing rows. It'll be easiest to take the
number of cells right from the first row.
function createRow() { let newRow = document.createElement("tr") for (let i = 0; i < table.firstElementChild.childNodes.length; i++) { newRow.appendChild(createCell()); } return newRow; }
It's important to realize what is stored in which variable. As the maximum
value for the i
loop is
table.firstElementChild.childNodes.length
, at first glance, we can
only say that it has something to do with the table and the length. The rest of
the expression is not that clear. It's ideal if we use comments to describe
complex expressions such as this one or if we use multiple variables for the
expression parts to make it more readable.
The <table>
element contains <tr>
elements. So we know that table.firstElementChild
is the
<tr>
element. childNodes
are all the
<td>
elements, so an array of them. And length
carries the length of that array. This results in a fact that
table.firstElementChild.childNodes.length
contains the number of
cells of the first table row. It's only up to you if you use multiple variables
to make the expression more clear:
let firstRow = table.firstElementChild; let firstRowCells = firstRow.childNodes; let firstRowCellsCount = firstRowCells.length;
Or you can use a simple comment block to note what the expression means, so you'd be able to read it when you look at your code a year later:
/*
* table = <TABLE>
* table.firstElementChild = <TR>
* table.firstElementChild.childNodes = [<TD>]
* table.firstElementChild.childNodes.length = number
*
* table. firstElementChild. childNodes .length
* <TABLE>. <TR>. [<TD>] .length
*/
If you want, you can try practicing orientation in the code, like trying to figure out what's going to happen if we have the following condition (we're really going to add it in the next lesson):
if (table.childNodes[i].childNodes[indexOfSelected] == table.childNodes[i].lastElementChild) { }
We'll continue in the next lesson, Finishing the table editor in JavaScript.
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 47x (1.78 kB)
Application includes source codes in language JavaScript