We'll look at using javascript to modify the DOM in response to events.
Also, we'll consider an extended example of refactoring some javascript
(in the process, illustrating some standard ways of modifying the DOM).
HTML vs. DOM
You have already talked about javascript code and the DOM in WebI;
we'll review those concepts in the context of improving an actual web page.
We think of HTML as being a tree.
(Or, a way of representing a document's tree-structure as a flat string full of angle-brackets.)
The HTML gives content-information associated with nodes of the tree;
CSS further provides presentation-information associated with nodes.
And the browser even tracks more information with each node:
all its default-styles and spacing, and more we'll see below.
Barland's definition: DOM (“Document Object Model”):
The tree, as it exists in-memory, representing the web-page;
it can be modified after the page is loaded.
Do not attempt to write any significant javascript
without a decent debugging tool
like Firebug or Chrome Tools' javascript console window help.
At least, not if you value your sanity.
In particular:
Chrome » ⋮ » More Tools » Developer Tools, and
From that same panel Elements » Properties.
When you select a part of the HTML on the left pane,
on the right pane you'll see the corresponding DOM object's class (e.g. “em”),
as well as its superclass-ancestors (e.g. “HTMLElement”, “Node”, etc.).
We'll focus on just the most specific class (we can use the others to figure out where it's
inheriting an attribute from).
pro-tip (Chrome): if you (a) select something up in the main page view, and then
down in the tools (b) click on the upper-left-corner “square-with-pointer” icon,
it’ll warp straight to the DOM-node corresponding to your upper-window-selection!
Javascript in a nutshell:
Objects have fields and methods, accessed via “.” (just like Java).
When running javascript inside a browser, the browser
gives you a global-variable “document”
which refers to the DOM (the tree).
The method getElementById is the
preferred way to access an element inside document.
(This means that if you are going to access a node with javascript,
then your HTML should include an id attribute for it.)
Note that all HTML attributes become DOM corresponding attributes:
if you have “<table lang="en" id='sample' contentEditable='true'>…<p>” then
the corresponding DOM node
will have those attributes:
var someNode = document.getElementById("sample");
var isBarlandSpeakingTruth = (someNode.lang==="en" && someNode.innerText==="magic-blue");
someNode.style.color = (isBarlandSpeakingTruth ? "blue" : "orange");
TODO: Open the Javascript console for this page
Then, on this page, edit the following paragraph's text to “magic-blue”.
Finally, copy/paste the above three javascript lines from within
the javascript console (More Tools » Developer Tools » Console),
and see the text change color.
not green!
Weirdnesses/inconsistencies:
Every HTML attribute turns into a javascript attribute for the corresponding node of the DOM.
But, hyphenated style-attributes turn into camelcase (HTML <span id="blah" style="border-width: 2px;">
turns into an javascript attribute document.getAttributeById("blah").style.borderWidth.)
And one attribute changes its name entirely:
HTML attribute “class” becomes DOM-attribute
“className”1.
If you provide an invalid node-attribute in the HTML, it doesn't get attached to the DOM.
However, once the DOM is created, its nodes (like any javascript object) can have any new
attribute you like assigned to them.
The attribute “contentEditable" can have value "true","false","" or (in html5 but not in
xhtml) no-value (which gives it the boolean true, in the DOM?).
Beware javascript that just tests if (someNode.contentEditable) …,
since in javascript non-empty string strings are true-ish, including (naturally) the string "false".
On the other hand, for html "boolean" attributes like checked,
the html checked='checked' turns into a DOM node where checked
is the boolean value true,
like this:
(check document.getElementById("cb").checked in the javascript Console window).
Notice what the DOM's text-nodes look like, when there was an entity in the original HTML:
If our html-source has
<span id="text-with-entities"><Here></span>
,
what will (in the DOM/js) is
document.getElementById("text-with-entities").length evaluate to?
(That is, what is the length of the string/text, which contains entities inside it?)
[Alternately: you can also visually-inspect the contents of that node:
In Chrome
select <Here>,
then click on the square-with-pointer icon in upper-left of developer-tools.
At first, you might think
“the body of that span tag is the text ><Here>,
which is clearly twelve characters.
But by the time we're in the DOM, it's six!
What gives?
Chrome reads the "flat" string of HTML with brackets-to-indicate-elements, and constructs the DOM tree
from that text.
After the tree's been built, there's no reason to be confused if you
happened to see text (as the leaf of the DOM)
which contains ‘<’
we know that character wouldn't be talking about an open-tag because the tree is already built.
So, Chrome goes through the DOM, and if it sees any "<" in a string,
it literally replaces them with "<", and it's no longer confusable as an open-tag.
(Similarly with other entities.)
So that's why string-length returned the value it did (six not twelve),
and why the inspector shows a literal ‘<’ character.
Btw, this difference between source-file and internalized-representation
is reminiscent of how, in Java, the string literal "ab\ncd" looks
like it has 6 characters in the source-code,
but really once the compiler parses the file it's actually a string-object containing exactly 5 characters.
A Case Study
We'll show-source, and discuss evolution of the javascript program.
For each version:
discuss how much work it would be to add another category (animal)?
(And: what is the least conceivable amount of work/info required?)
js-hiding-parts-v2-oops.html: Debugging your javascript.
youtube (10m10s):
Take note that javascript's for-in construct loops over indices,
not contents,
so you still need to use square-brackets inside the loop.
(This is different from java's foreach and php's foreach).
js-hiding-parts-v3a.html
Generalize the entire navbar.
We do tree surgery: create new nodes which we splice onto the DOM entirely at run-time. A+.
youtube (14m31s):
As seen in the video, the javascript methods-of-interest are
createElement (and its friend createTextNode) to create a element(tag),
appendChild to add that node into the existing DOM,
and
setAttribute to add attributes to the element(tag).
Pitfall:
js-hiding-parts-v3b.html
As before, but placing the paragraph in a different place
suddenly breaks the code?!
youtube (09m33s):
It breaks because the processing happens ASAP,
before the named id has been parsed by the browser.
An error message like “no such id” would have been very nice!2
Had my first attempt suffered from this bug, I would have given up assuming
my whole approach didn't work.
See also: attribute defer="defer" for tag script,
which waits for the entire page to load before running.
(Or, less helpfully, the onload attribute which can be placed in
body and just a few other tags).
js-hiding-parts-v3c.html,
Minor, final touch: Pulling the javascript into its own file, so that it can now be re-used between many pages.
Reminder (see preceding video):
When writing a script tag,
never use the self-closing notation
(“<script src="…" />”),
since the browser might ignore it entirely. Grrr!
alternate video:
If you don't like the above videos,
here is a long (less wieldy) version of the same info, from
2017-Mar-14 distance section (57m05s):
introduction to the DOM and javascript to modify it.
In-class example:
Applying the above:
How can/should we write javascript for
a Bingo card?
(initial
and
final
versions).
Note: here's
an on-line javascript interpreter.
We'll use it to note that:
arrays are objects;
any object can have properties accessed via square-brackets;
but you should still iterate over arrays by indexing over [0,data.count).
You can also use “for…in” to
iterate over a general object's properties.
For example, .childnodes returns a list, which can be
iterated over via “for…in”.
arrays vs. objects, in javascript
Arrays in javascript are not associative -- the indices are only numeric.
However: any object (array or string or DOM-node or whatever) can have
properties;
properties are key/value pairs.
So I guess every object could be used as an asssociative array incidentally.
This is discouraged, because other code might add properties to your object.
Just to muddle things thoroughly, properties can be assigned/retrieve
either through dot-notation (“a.color = "true"”)
or through array-brackets (“a['color'] = "true"”)
even though they are not arrays.
var a;
a = [71,72,73];
a.foo = "huh";
writeln( "The length of a: " + a.length );
writeln( "The length of a: " + a['length'] );
writeln( "Using a regular for-loop, to loop over numeric indices:" );
for (var i = 0; i < a.length; ++i ) {
writeln("at index i=" + i + ", a[i]=" + a[i] );
}
writeln( "Using for-in, to loop over all *enumerable* properties:" );
for (var i in a) {
writeln("at index i=" + i + ", a[i]=" + a[i] );
}
writeln( "Note: the for-in loop will also include properties from the object's "
+ "parent object, and it's parent, etc." );
writeln( "Note: `length` is a property of the object, but the `for` loop doesn't "
+ "enumerate over it. That's because some properties can be tagged as "
+ "'non-enumerable' -- a concept invented just for for-each loops?" );
Tips for minimizing the difference between php and javascript code:
You are allowed to have javascript variables start with "$"
(weird, but allowed3)
Grabbing input from a form:
in php:
$_POST['fname']
in javascript:
document.getElementById('fname')
You can abstract these into a function
getDocElement('fname')
and then make getDocElement a (1-line wrapper)
in both php and in javascript.
This has the advantage that you can provide a default argument,
to use when the element doesn't exist [especially helpful in
php handling checkboxes, where if no checkboxes were selected
the lookup $_POST would give a warning,
and you can have your wrapper
avoid the warning with isset,
and instead have that function allow an optional second input:
a value to use when the item isn't in the array:
getDocElement('dietaryRestrictions', 'no restrictions');
or even
getDocElement('billingAddr', getDocElement('shippingAddr') ).
1 To
add to the confusion:
As we just said, if you want to set a DOM node's html-attribute “class”, you
should assign
to the DOM-field “className”.
However, if you are using the method setAttribute,
pass it the html-version:
someNode.setAttribute("class","important-info").
Sigh — this inconsistency definitely violates the principle of least surprise
(though there may not be any better solution, given the html/DOM mismatch at the heart of the issue).
↩
2 Javascript
error tools like Firebug or Chrome Tools' javascript console window help.
↩
3 In fact, jQuery is a javascript library which
has a variable named “$”,
with its apply-a-function operator “()” overloaded — thus
$("a").click(…)
is taking the $ object, calling its () operator
passing it "a", which apparently returns an object
with a method named “click”.
…More specifically,
“$” is a property that jQuery adds to the window object;
it can also be accessed through the name “jQuery”.
See more.↩