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).
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).
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: Use your browser-tools to check the following paragraph (search for "magic-blue").
Then, (a) edit the paragraph's text,
and (b) copy/paste the above three javascript lines from within
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 the non-empty string "false" is true-ish!
On the other hand, for html "boolean" attributes like "selected",
the html selected='selected' turns into a DOM node where selected
is the boolean value true.
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!
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 asssociate 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') ).
1To
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).
↩
2Javascript
error tools like Firebug or Chrome Tools' javascript console window help.
↩
3In 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.↩