Handling the DOM in Dart

15 min read

(For more resources related to this topic, see here.)

A Dart web application runs inside the browser (HTML) page that hosts the app; a single-page web app is more and more common. This page may already contain some HTML elements or nodes, such as <div> and <input>, and your Dart code will manipulate and change them, but it can also create new elements. The user interface may even be entirely built up through code. Besides that, Dart is responsible for implementing interactivity with the user (the handling of events, such as button-clicks) and the dynamic behavior of the program, for example, fetching data from a server and showing it on the screen. We explored some simple examples of these techniques. Compared to JavaScript, Dart has simplified the way in which code interacts with the collection of elements on a web page (called the DOM tree). This article teaches you this new method using a number of simple examples, culminating with a Ping Pong game. The following are the topics:

  • Finding elements and changing their attributes
  • Creating and removing elements
  • Handling events
  • Manipulating the style of page elements
  • Animating a game
  • Ping Pong using style(s)
  • How to draw on a canvas – Ping Pong revisited

Finding elements and changing their attributes

All web apps import the Dart library dart:html; this is a huge collection of functions and classes needed to program the DOM (look it up at api.dartlang.org). Let’s discuss the base classes, which are as follows:

  • The Navigator class contains info about the browser running the app, such as the product (the name of the browser), its vendor, the MIME types supported by the installed plugins, and also the geolocation object.
  • Every browser window corresponds to an object of the Window class, which contains, amongst many others, a navigator object, the close, print, scroll and moveTo methods, and a whole bunch of event handlers, such as onLoad, onClick, onKeyUp, onMouseOver, onTouchStart, and onSubmit. Use an alert to get a pop-up message in the web page, such as in todo_v2.dart:

    window.onLoad.listen( (e) =>
    window.alert("I am at your disposal") );

  • If your browser has tabs, each tab opens in a separate window. From the Window class, you can access local storage or IndexedDB to store app data on the client
  • The Window object also contains an object document of the Document class, which corresponds to the HTML document. It is used to query for, create, and manipulate elements within the document. The document also has a list of stylesheets (objects of the StyleSheet class)—we will use this in our first version of the Ping Pong game.
  • Everything that appears on a web page can be represented by an object of the Node class; so, not only are tags and their attributes nodes, but also text, comments, and so on. The Document object in a Window class contains a List<Node> element of the nodes in the document tree (DOM) called childNodes.
  • The Element class, being a subclass of Node, represents web page elements (tags, such as <p>, <div>, and so on); it has subclasses, such as ButtonElement, InputElement, TableElement, and so on, each corresponding to a specific HTML tag, such as <button>, <input>, <table>, and so on. Every element can have embedded tags, so it contains a List<Element> element called children.

Let us make this more concrete by looking at todo_v2.dart, solely for didactic purposes—the HTML file contains an <input> tag with the id value task, and a <ul> tag with the id value list:

<div><input id="task" type="text" placeholder="What do you want to do?"/> <p id="para">Initial paragraph text</p> </div> <div id="btns"> <button class="backgr">Toggle background color of header</button> <button class="backgr">Change text of paragraph</button> <button class="backgr">Change text of placeholder in input
field and the background color of the buttons</button> </div> <div><ul id="list"/> </div>

In our Dart code, we declare the following objects representing them:

InputElement task; UListElement list;

The following list object contains objects of the LIElement class, which are made in addItem():

var newTask = new LIElement();

You can see the different elements and their layout in the following screenshot:

The screen of todo_v2

Finding elements

Now we must bind these objects to the corresponding HTML elements. For that, we use the top-level functions querySelector and querySelectorAll; for example, the InputElement task is bound to the <input> tag with the id value task using: task = querySelector(‘#task’); .

Both functions take a string (a CSS selector) that identifies the element, where the id value task will be preceded by #. CSS selectors are patterns that are used in .css files to select elements that you want to style. There are a number of them, but, generally, we only need a few basic selectors (for an overview visit http://www.w3schools.com/cssref/css_selectors.asp).

  • If the element has an id attribute with the value abc, use querySelector(‘#abc’)
  • If the element has a class attribute with value abc, use querySelector(‘.abc’)
  • To get a list of all elements with the tag <button>, use querySelectorAll(‘button’)
  • To get a list of all text elements, use querySelectorAll(‘input[type=”text”]’) and all sorts of combinations of selectors; for example, querySelectorAll(‘#btns .backgr’) will get a list of all elements with the backgr class that are inside a tag with the id value btns

These functions are defined on the document object of the web page, so in code you will also see document.querySelector() and document.querySelectorAll().

Changing the attributes of elements

All objects of the Element class have properties in common, such as classes, hidden, id, innerHtml, style, text, and title; specialized subclasses have additional properties, such as value for a ProgressElement method. Changing the value of a property in an element makes the browser re-render the page to show the changed user interface. Experiment with todo_v2.dart:

import 'dart:html'; InputElement task; UListElement list; Element header; List<ButtonElement> btns; main() { task = querySelector('#task'); list = querySelector('#list'); task.onChange.listen( (e) => addItem() ); // find the h2 header element: header = querySelector('.header'); (1) // find the buttons: btns = querySelectorAll('button'); (2) // attach event handler to 1st and 2nd buttons: btns[0].onClick.listen( (e) => changeColorHeader() ); (3) btns[1].onDoubleClick.listen( (e) => changeTextPara() ); (4) // another way to get the same buttons with class backgr: var btns2 = querySelectorAll('#btns .backgr'); (5) btns2[2].onMouseOver.listen( (e) => changePlaceHolder() );(6) btns2[2].onClick.listen((e) => changeBtnsBackColor() ); (7) addElements(); } changeColorHeader() => header.classes.toggle('header2'); (8) changeTextPara() => querySelector('#para').text =
"You changed my text!"; (9) changePlaceHolder() => task.placeholder =
'Come on, type something in!'; (10) changeBtnsBackColor() => btns.forEach( (b)
=> b.classes.add('btns_backgr')); (11) void addItem() { var newTask = new LIElement(); (12) newTask.text = task.value; (13) newTask.onClick.listen( (e) => newTask.remove()); task.value = ''; list.children.add(newTask); (14) } addElements() { var ch1 = new CheckboxInputElement(); (15) ch1.checked = true; document.body.children.add(ch1); (16) var par = new Element.tag('p'); (17) par.text = 'I am a newly created paragraph!'; document.body.children.add(par); var el = new Element.html('<div><h4><b>
A small divsection</b></h4></div>'); (18) document.body.children.add(el); var btn = new ButtonElement(); btn.text = 'Replace'; btn.onClick.listen(replacePar); document.body.children.add(btn); var btn2 = new ButtonElement(); btn2.text = 'Delete all list items'; btn2.onClick.listen( (e) => list.children.clear() ); (19) document.body.children.add(btn2); } replacePar(Event e) { var el2 = new Element.html('<div><h4><b>
I replaced this div!</b></h4></div>'); el.replaceWith(el2); (20) }

Comments for the numbered lines are as follows:

  1. We find the <h2> element via its class.
  2. We get a list of all the buttons via their tags.
  3. We attach an event handler to the Click event of the first button, which toggles the class of the <h2> element, changing its background color at each click (line (8)).
  4. We attach an event handler to the DoubleClick event of the second button, which changes the text in the <p> element (line (9)).
  5. We get the same list of buttons via a combination of CSS selectors.
  6. We attach an event handler to the MouseOver event of the third button, which changes the placeholder in the input field (line (10)).
  7. We attach a second event handler to the third button; clicking on it changes the background color of all buttons by adding a new CSS class to their classes collection (line (11)).

Every HTML element also has an attribute Map where the keys are the attribute names; you can use this Map to change an attribute, for example:

btn.attributes['disabled'] = 'true';

Please refer to the following document to see which attributes apply to which element:


Creating and removing elements

The structure of a web page is represented as a tree of nodes in the Document Object Model (DOM). A web page can start its life with an initial DOM tree, marked up in its HTML file, and then the tree can be changed using code; or, it can start off with an empty tree, which is then entirely created using code in the app, that is every element is created through a constructor and its properties are set in code. Elements are subclasses of Node; they take up a rectangular space on the web page (with a width and height). They have, at most, one parent Element in which they are enclosed and can contain a list of Elements—their children (you can check this with the function hasChildNodes() that returns a bool function). Furthermore, they can receive events. Elements must first be created before they can be added to the list of a parent element. Elements can also be removed from a node. When elements are added or removed, the DOM tree is changed and the browser has to re-render the web page.

An Element object is either bound to an existing node with the querySelector method of the document object or it can be created with its specific constructor, such as that in line (12) (where newTask belongs to the class LIElement—List Item element) or line (15). If useful, we could also specify the id in the code, such as in newTask.id = ‘newTask’;

If you need a DOM element in different places in your code, you can improve the performance of your app by querying it only once, binding it to a variable, and then working with that variable.

After being created, the element properties can be given a value such as that in line (13). Then, the object (let’s name it elem) is added to an existing node, for example, to the body node with document.body.children.add(elem), as in line (16), or to an existing node, as list in line (14). Elements can also be created with two named constructors from the Element class:

  1. Like Element.tag(‘tagName’) in line (17), where tagName is any valid HTML tag, such as <p>, <div>, <input>, <select>, and so on.
  2. Like Element.html(‘htmlSnippet’) in line (18), where htmlSnippet is any valid combination of HTML tags.

Use the first constructor if you want to create everything dynamically at runtime; use the second constructor when you know what the HTML for that element will be like and you won’t need to reference its child elements in your code (but by using the querySelector method, you can always find them if needed).

You can leave the type of the created object open using var, or use the type Element, or use the specific class name (such as InputElement)—use the latter if you want your IDE to give you more specific code completion and warnings/errors against the possible misuse of types.

When hovering over a list item, the item changes color and the cursor becomes a hand icon; this could be done in code (try it), but it is easier to do in the CSS file:

#list li:hover { color: aqua; font-size:20 px; font-weight: bold; cursor: pointer; }

To delete an Element elem from the DOM tree, use elem.remove(). We can delete list items by clicking on them, which is coded with only one line:

newTask.onClick.listen( (e) => newTask.remove() );

To remove all the list items, use the List function clear(), such as in line (19). Replace elem with another element elem2 using elem.replaceWith(elem2), such as in line (20).

Handling events

When the user interacts with the web form, such as when clicking on a button or filling in a text field, an event fires; any element on the page can have events. The DOM contains hooks for these events and the developer can write code (an event handler) that the browser must execute when the event fires. How do we add an event handler to an element (which is also called registering an event handler)?. The general format is:

element.onEvent.listen( event_handler )

(The spaces are not needed, but can be used to make the code more readable). Examples of events are Click, Change, Focus, Drag, MouseDown, Load, KeyUp, and so on. View this as the browser listening to events on elements and, when they occur, executing the indicated event handler. The argument that is passed to the listen() method is a callback function and has to be of the type EventListener; it has the signature: void EventListener(Event e)

The event handler gets passed an Event parameter, succinctly called e or ev, that contains more specific info on the event, such as which mouse button should be pressed in case of a mouse event, on which object the event took place using e.target, and so on. If an event is not handled on the target object itself, you can still write the event handler in its parent, or its parent’s parent, and so on up the DOM tree, where it will also get executed; in such a situation, the target property can be useful in determining the original event object. In todo_v2.dart, we examine the various event-coding styles. Using the general format, the Click event on btns2[2] can be handled using the following code:

btns2[2].onClick.listen( changeBtnsBackColor );

where changeBtnsBackColor is either the event handler or callback function. This function is written as:

changeBtnsBackColor(Event e) => btns.forEach( (b) => b.classes.add('btns_backgr'));

Another, shorter way to write this (such as in line (7)) is:

btns2[2].onClick.listen( (e) => changeBtnsBackColor() ); changeBtnsBackColor() => btns.forEach( (b) =>

When a Click event occurs on btns2[2], the handler changeBtnsBackColor is called.

In case the event handler needs more code lines, use the brace syntax as follows:

changeBtnsBackColor(Event e) { btns.forEach( (b) => b.classes.add('btns_backgr')); // possibly other code }

Familiarize yourself with these different ways of writing event handlers.

Of course, when the handler needs only one line of code, there is no need for a separate method, as in the following code:

newTask.onClick.listen( (e) => newTask.remove() );

For clarity, we use the function expression syntax => whenever possible, but you can inline the event handler and use the brace syntax along with an anonymous function, thus avoiding a separate method. So instead of executing the following code:

task.onChange.listen( (e) => addItem() );

we could have executed:

task.onChange.listen( (e) { var newTask = new LIElement(); newTask.text = task.value; newTask.onClick.listen( (e) => newTask.remove()); task.value = ''; list.children.add(newTask); } );

JavaScript developers will find the preceding code very familiar, but it is also used frequently in Dart code, so make yourself acquainted with the code pattern ( (e) {…} );. The following is an example of how you can respond to key events (in this case, on the window object) using the keyCode and ctrlKey properties of the event e:

window.onKeyPress .listen( (e) { if (e.keyCode == KeyCode.ENTER) { window.alert("You pressed ENTER"); } if (e.ctrlKey && e.keyCode == CTRL_ENTER) { window.alert("You pressed CTRL + ENTER"); } });

In this code, the constant const int CTRL_ENTER = 10; is used.

(The list of keyCodes can be found at http://www.cambiaresearch.com/articles/15/javascript-char-codes-key-codes).

Manipulating the style of page elements

CSS style properties can be changed in the code as well: every element elem has a classes property, which is a set of CSS classes. You can add a CSS class as follows:

elem.classes.add ('cssclass');

as we did in changeBtnsBackColor (line (11)); by adding this class, the new style is immediately applied to the element. Or, we can remove it to take away the style:

elem.classes.remove ('cssclass');

The toggle method (line (8)) elem.classes.toggle(‘cssclass’); is a combination of both: first the cssclass is applied (added), the next time it is removed, and, the time after that, it is applied again, and so on.

Working with CSS classes is the best way to change the style, because the CSS definition is separated from the HTML markup. If you do want to change the style of an element directly, use its style property elem.style, where the cascade style of coding is very appropriate, for example:

newTask.style ..fontWeight = 'bold' ..fontSize = '3em' ..color = 'red';


Please enter your comment!
Please enter your name here